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
23
15
const createDirectories = ( ) => {
24
16
const dirs = [
25
17
path . join ( __dirname , 'provisional' ) ,
26
- path . join ( __dirname , 'public' , 'files ')
18
+ path . join ( __dirname , 'resource ' )
27
19
] ;
28
20
dirs . forEach ( ( dir ) => {
29
21
if ( ! fs . existsSync ( dir ) ) {
@@ -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