|
| 1 | +// app.js |
1 | 2 | const Koa = require('koa'); |
2 | | -const Router = require('koa-router'); |
3 | 3 | const { koaBody } = require('koa-body'); |
4 | | -const tinify = require('tinify'); |
5 | 4 | const path = require('path'); |
6 | 5 | const fs = require('fs'); |
7 | | -const sharp = require('sharp'); |
8 | | -const { checkAndCreateTable } = require('./utils/checkAndCreateTable'); |
9 | | -const pool = require('./utils/db'); |
10 | | -const { appendSuffixToFilename } = require('./utils/appendSuffixToFilename'); |
11 | | -const { v4: uuidv4 } = require('uuid'); |
12 | | -const { detectFileType } = require('./utils/detectFileType'); |
13 | | -const { imageMimeTypes, tinifySupportedMimeTypes} = require('./constants/file') |
| 6 | +const sequelize = require('./utils/dbInstance'); // 确保路径正确 |
| 7 | +const filesRouter = require('./routers/files'); // 确保路径正确 |
| 8 | + |
14 | 9 | require('dotenv').config({ path: '.env.local' }); |
15 | 10 |
|
16 | 11 | const app = new Koa(); |
17 | | -const router = new Router(); |
18 | | - |
19 | | -tinify.key = process.env.TINIFY_KEY; |
20 | 12 |
|
21 | 13 | app.use(require('koa-static')(path.join(__dirname, 'public'))); |
22 | 14 |
|
@@ -44,256 +36,11 @@ app.use( |
44 | 36 | }) |
45 | 37 | ); |
46 | 38 |
|
47 | | -router.post('/upload', async (ctx) => { |
48 | | - const connection = await pool.getConnection(); |
49 | | - try { |
50 | | - const files = ctx.request.files.file; |
51 | | - const fileList = Array.isArray(files) ? files : [files]; |
52 | | - const responses = []; |
53 | | - |
54 | | - const compress = ctx.query.compress !== 'false'; // 默认压缩 |
55 | | - const keepTemp = ctx.query.keepTemp === 'true'; // 默认不保留临时文件 |
56 | | - const isThumb = Number(ctx.query.isThumb === 'true'); |
57 | | - const isPublic = Number(ctx.query.isPublic === 'true'); |
58 | | - const responseType = ctx.query.type; |
59 | | - |
60 | | - for (const file of fileList) { |
61 | | - const fileId = uuidv4(); // 生成文件唯一ID |
62 | | - |
63 | | - const outputFilePath = path.join( |
64 | | - __dirname, |
65 | | - 'public', |
66 | | - 'files', |
67 | | - fileId + path.extname(file.filepath) // 使用UUID作为文件名称 |
68 | | - ); |
69 | | - |
70 | | - const { mime, ext } = await detectFileType(file.filepath, file); |
71 | | - |
72 | | - let outputFileThumbPath = null; |
73 | | - if (isThumb && imageMimeTypes.includes(mime)) { |
74 | | - const fileThumbName = `${fileId}_thumb${path.extname(file.filepath)}`; // 缩略图文件名称 |
75 | | - |
76 | | - outputFileThumbPath = path.join( |
77 | | - __dirname, |
78 | | - 'public', |
79 | | - 'files', |
80 | | - fileThumbName |
81 | | - ); |
82 | | - |
83 | | - await sharp(file.filepath) |
84 | | - .resize(200, 200) // 调整图像大小为200x200像素 |
85 | | - .toFile(outputFileThumbPath); |
86 | | - } else if(isThumb) { |
87 | | - const back_thumbs = { |
88 | | - video: path.join(__dirname, 'public', 'icons', 'video.png'), |
89 | | - sheet: path.join(__dirname, 'public', 'icons', 'xlsx.png'), |
90 | | - pdf: path.join(__dirname, 'public', 'icons', 'pdf.png'), |
91 | | - document: path.join(__dirname, 'public', 'icons', 'doc.png'), |
92 | | - } |
93 | | - |
94 | | - const unknown = path.join(__dirname, 'public', 'icons', 'unknown_file_types.png'); |
95 | | - |
96 | | - const thumb = Object.keys(back_thumbs).find(key => mime.includes(key)); |
97 | | - |
98 | | - outputFileThumbPath = back_thumbs[thumb] ?? unknown; |
99 | | - } |
100 | | - |
101 | | - if (compress && tinifySupportedMimeTypes.includes(mime)) { |
102 | | - await tinify.fromFile(file.filepath).toFile(outputFilePath); |
103 | | - } else { |
104 | | - // 如果不支持压缩或者不要求压缩,保留临时文件则复制文件,否则移动文件 |
105 | | - if (keepTemp) { |
106 | | - fs.copyFileSync(file.filepath, outputFilePath); |
107 | | - } else { |
108 | | - fs.renameSync(file.filepath, outputFilePath); |
109 | | - } |
110 | | - } |
111 | | - |
112 | | - const fileUrl = `${process.env.PUBLIC_NETWORK_DOMAIN}/files/${fileId}`; |
113 | | - const thumb_location = outputFileThumbPath ? `${process.env.PUBLIC_NETWORK_DOMAIN}/files/${fileId}?type=thumb` : null; |
114 | | - |
115 | | - await connection.execute( |
116 | | - `INSERT INTO files ( |
117 | | - id, |
118 | | - filename, |
119 | | - filesize, |
120 | | - filelocation, |
121 | | - real_file_location, |
122 | | - created_by, |
123 | | - is_public, |
124 | | - thumb_location, |
125 | | - is_thumb, |
126 | | - is_delete, |
127 | | - real_file_thumb_location, |
128 | | - mime, |
129 | | - ext |
130 | | - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, |
131 | | - [ |
132 | | - fileId, // 使用UUID作为ID |
133 | | - path.basename(outputFilePath), |
134 | | - fs.statSync(outputFilePath).size, |
135 | | - fileUrl, |
136 | | - outputFilePath, // 存储实际文件路径 |
137 | | - ctx.query.createdBy || 'anonymous', |
138 | | - isPublic, |
139 | | - thumb_location, |
140 | | - isThumb, |
141 | | - 0, |
142 | | - outputFileThumbPath, |
143 | | - mime, |
144 | | - ext |
145 | | - ] |
146 | | - ); |
147 | | - |
148 | | - if (responseType === 'md' && imageMimeTypes.includes(mime)) { |
149 | | - responses.push({ |
150 | | - filepath: `` |
151 | | - }); |
152 | | - } else { |
153 | | - responses.push({ filepath: fileUrl }); |
154 | | - } |
155 | | - |
156 | | - if (!keepTemp && fs.existsSync(file.filepath)) { |
157 | | - fs.unlinkSync(file.filepath); |
158 | | - } |
159 | | - } |
160 | | - |
161 | | - ctx.body = fileList.length > 1 ? responses : responses[0]; |
162 | | - } catch (error) { |
163 | | - ctx.status = 500; |
164 | | - ctx.body = 'Error processing your request: ' + error.message; |
165 | | - } finally { |
166 | | - connection.release(); |
167 | | - } |
168 | | -}); |
169 | | - |
170 | | -router.get('/files', async (ctx) => { |
171 | | - const connection = await pool.getConnection(); |
172 | | - try { |
173 | | - const limit = parseInt(ctx.query.limit, 10) || 10; // 每页数量,默认为 10 |
174 | | - const offset = parseInt(ctx.query.offset, 10) || 0; // 偏移量,默认为 0 |
175 | | - const type = ctx.query.type ?? ''; // 获取查询参数中的类型 |
176 | | - |
177 | | - const types = { |
178 | | - image: 'image', |
179 | | - video: 'video', |
180 | | - all: '', |
181 | | - } |
182 | | - |
183 | | - const excludedTypes = ['image', 'video']; // 要排除的类型 |
184 | | - |
185 | | - let mimeCondition = ''; // 初始化mime条件 |
186 | | - |
187 | | - // 构建 mime 条件 |
188 | | - if (type === 'file') { |
189 | | - mimeCondition = excludedTypes.map(t => `mime NOT LIKE '%${t}%'`).join(' AND '); |
190 | | - } else if (types[type]) { |
191 | | - mimeCondition = `mime LIKE '%${types[type]}%'`; |
192 | | - } |
193 | | - |
194 | | - // 构建完整的 SQL 语句 |
195 | | - const sql = ` |
196 | | - SELECT |
197 | | - created_by, |
198 | | - created_at, |
199 | | - public_by, |
200 | | - public_expiration, |
201 | | - updated_at, |
202 | | - updated_by, |
203 | | - filesize, |
204 | | - filename, |
205 | | - filelocation, |
206 | | - thumb_location, |
207 | | - is_public |
208 | | - FROM |
209 | | - files |
210 | | - WHERE |
211 | | - is_delete = 0 |
212 | | - AND is_public = 1 |
213 | | - ${mimeCondition ? `AND ${mimeCondition}` : ''} |
214 | | - LIMIT ? OFFSET ?`; |
215 | | - |
216 | | - // 执行查询 |
217 | | - const [rows] = await connection.execute( |
218 | | - sql, |
219 | | - [String(limit), String(offset)] |
220 | | - ); |
221 | | - |
222 | | - |
223 | | - ctx.body = rows; |
224 | | - } catch (error) { |
225 | | - ctx.status = 500; |
226 | | - ctx.body = 'Error retrieving files: ' + error.message; |
227 | | - } finally { |
228 | | - connection.release(); |
229 | | - } |
230 | | -}); |
231 | | - |
232 | | -router.get('/files/:id', async (ctx) => { |
233 | | - const { id } = ctx.params; |
234 | | - const { type } = ctx.query; // 获取查询参数 'type',可以是 'thumb' 或 'original' |
235 | | - const connection = await pool.getConnection(); |
236 | | - |
237 | | - try { |
238 | | - // 查询文件数据,只获取必要字段 |
239 | | - const [rows] = await connection.execute( |
240 | | - ` |
241 | | - SELECT |
242 | | - filename, |
243 | | - is_delete, |
244 | | - is_public, |
245 | | - public_expiration, |
246 | | - real_file_location, |
247 | | - real_file_thumb_location, |
248 | | - is_thumb, |
249 | | - mime, |
250 | | - ext |
251 | | - FROM files |
252 | | - WHERE id = ? |
253 | | - AND is_delete = 0 |
254 | | - AND (is_public = 1 AND (public_expiration IS NULL OR public_expiration > NOW()))`, |
255 | | - [id] |
256 | | - ); |
257 | | - |
258 | | - if (rows.length === 0) { |
259 | | - ctx.status = 404; |
260 | | - ctx.body = { message: 'File not found or not accessible' }; |
261 | | - return; |
262 | | - } |
263 | | - |
264 | | - const file = rows[0]; |
265 | | - |
266 | | - let fileLocation = file.real_file_location; |
267 | | - // 根据查询参数 'type' 决定返回原图或缩略图 |
268 | | - if(file.is_thumb && type === 'thumb') { |
269 | | - fileLocation = file.real_file_thumb_location; |
270 | | - } |
271 | | - |
272 | | - // 检查文件是否存在 |
273 | | - if (!fs.existsSync(fileLocation)) { |
274 | | - ctx.status = 404; |
275 | | - ctx.body = { message: 'File not found' }; |
276 | | - return; |
277 | | - } |
278 | | - const { mime } = await detectFileType(fileLocation); |
279 | | - // 设置响应头 |
280 | | - ctx.set('Content-Type', mime); |
281 | | - ctx.set('Content-Disposition', `inline; filename="${file.filename}"`); |
282 | | - |
283 | | - // 返回文件流 |
284 | | - ctx.body = fs.createReadStream(fileLocation); |
285 | | - } catch (error) { |
286 | | - ctx.status = 500; |
287 | | - ctx.body = { message: 'Internal server error', error: error.message }; |
288 | | - } finally { |
289 | | - connection.release(); // 释放连接 |
290 | | - } |
291 | | -}); |
292 | | - |
293 | | -app.use(router.routes()).use(router.allowedMethods()); |
| 39 | +// 挂载文件路由 |
| 40 | +app.use(filesRouter.routes()).use(filesRouter.allowedMethods()); |
294 | 41 |
|
295 | 42 | app.listen(process.env.SERVER_PORT, async () => { |
| 43 | + await sequelize.sync(); |
296 | 44 | console.log(`Server is running on ${process.env.INTERNAL_NETWORK_DOMAIN}`); |
297 | 45 | console.log(`Server is running on ${process.env.PUBLIC_NETWORK_DOMAIN}`); |
298 | | - await checkAndCreateTable(); |
299 | 46 | }); |
0 commit comments