1+ // app.js
12const Koa = require ( 'koa' ) ;
2- const Router = require ( 'koa-router' ) ;
33const { koaBody } = require ( 'koa-body' ) ;
4- const tinify = require ( 'tinify' ) ;
54const path = require ( 'path' ) ;
65const 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+
149require ( 'dotenv' ) . config ( { path : '.env.local' } ) ;
1510
1611const app = new Koa ( ) ;
17- const router = new Router ( ) ;
18-
19- tinify . key = process . env . TINIFY_KEY ;
2012
2113app . use ( require ( 'koa-static' ) ( path . join ( __dirname , 'public' ) ) ) ;
2214
2315const createDirectories = ( ) => {
2416 const dirs = [
2517 path . join ( __dirname , 'provisional' ) ,
26- path . join ( __dirname , 'public' , 'files ')
18+ path . join ( __dirname , 'resource ' )
2719 ] ;
2820 dirs . forEach ( ( dir ) => {
2921 if ( ! fs . existsSync ( dir ) ) {
@@ -44,256 +36,11 @@ app.use(
4436 } )
4537) ;
4638
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 ( ) ) ;
29441
29542app . listen ( process . env . SERVER_PORT , async ( ) => {
43+ await sequelize . sync ( ) ;
29644 console . log ( `Server is running on ${ process . env . INTERNAL_NETWORK_DOMAIN } ` ) ;
29745 console . log ( `Server is running on ${ process . env . PUBLIC_NETWORK_DOMAIN } ` ) ;
298- await checkAndCreateTable ( ) ;
29946} ) ;
0 commit comments