@@ -4,48 +4,20 @@ const { koaBody } = require('koa-body');
44const tinify = require ( 'tinify' ) ;
55const path = require ( 'path' ) ;
66const fs = require ( 'fs' ) ;
7- const mime = require ( 'mime-types' ) ;
87const sharp = require ( 'sharp' ) ;
98const { checkAndCreateTable } = require ( './utils/checkAndCreateTable' ) ;
109const pool = require ( './utils/db' ) ;
1110const { appendSuffixToFilename } = require ( './utils/appendSuffixToFilename' ) ;
1211const { v4 : uuidv4 } = require ( 'uuid' ) ;
12+ const { detectFileType } = require ( './utils/detectFileType' ) ;
13+ const { imageMimeTypes, tinifySupportedMimeTypes} = require ( './constants/file' )
1314require ( 'dotenv' ) . config ( { path : '.env.local' } ) ;
1415
1516const app = new Koa ( ) ;
1617const router = new Router ( ) ;
1718
1819tinify . key = process . env . TINIFY_KEY ;
1920
20- const tinifySupportedMimeTypes = [ 'image/jpeg' , 'image/png' , 'image/webp' ] ;
21- const imageMimeTypes = [
22- 'image/jpeg' ,
23- 'image/png' ,
24- 'image/webp' ,
25- 'image/gif' ,
26- 'image/bmp' ,
27- 'image/tiff' ,
28- 'image/x-icon' ,
29- 'image/svg+xml'
30- ] ;
31-
32- function getMimeType ( filePath ) {
33- const ext = path . extname ( filePath ) . toLowerCase ( ) ;
34- switch ( ext ) {
35- case '.jpg' :
36- case '.jpeg' :
37- return 'image/jpeg' ;
38- case '.png' :
39- return 'image/png' ;
40- case '.gif' :
41- return 'image/gif' ;
42- case '.webp' :
43- return 'image/webp' ;
44- default :
45- return 'application/octet-stream' ;
46- }
47- }
48-
4921app . use ( require ( 'koa-static' ) ( path . join ( __dirname , 'public' ) ) ) ;
5022
5123const createDirectories = ( ) => {
@@ -86,7 +58,6 @@ router.post('/upload', async (ctx) => {
8658 const responseType = ctx . query . type ;
8759
8860 for ( const file of fileList ) {
89- const mimeType = mime . lookup ( file . filepath ) ;
9061 const fileId = uuidv4 ( ) ; // 生成文件唯一ID
9162
9263 const outputFilePath = path . join (
@@ -96,8 +67,10 @@ router.post('/upload', async (ctx) => {
9667 fileId + path . extname ( file . filepath ) // 使用UUID作为文件名称
9768 ) ;
9869
70+ const { mime, ext } = await detectFileType ( file . filepath , file ) ;
71+
9972 let outputFileThumbPath = null ;
100- if ( isThumb && imageMimeTypes . includes ( mimeType ) ) {
73+ if ( isThumb && imageMimeTypes . includes ( mime ) ) {
10174 const fileThumbName = `${ fileId } _thumb${ path . extname ( file . filepath ) } ` ; // 缩略图文件名称
10275
10376 outputFileThumbPath = path . join (
@@ -110,9 +83,22 @@ router.post('/upload', async (ctx) => {
11083 await sharp ( file . filepath )
11184 . resize ( 200 , 200 ) // 调整图像大小为200x200像素
11285 . 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 ;
11399 }
114100
115- if ( compress && tinifySupportedMimeTypes . includes ( mimeType ) ) {
101+ if ( compress && tinifySupportedMimeTypes . includes ( mime ) ) {
116102 await tinify . fromFile ( file . filepath ) . toFile ( outputFilePath ) ;
117103 } else {
118104 // 如果不支持压缩或者不要求压缩,保留临时文件则复制文件,否则移动文件
@@ -128,8 +114,20 @@ router.post('/upload', async (ctx) => {
128114
129115 await connection . execute (
130116 `INSERT INTO files (
131- id, filename, filesize, filelocation, real_file_location, created_by, is_public, thumb_location, is_thumb, is_delete, real_file_thumb_location
132- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ,
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ,
133131 [
134132 fileId , // 使用UUID作为ID
135133 path . basename ( outputFilePath ) ,
@@ -141,11 +139,13 @@ router.post('/upload', async (ctx) => {
141139 thumb_location ,
142140 isThumb ,
143141 0 ,
144- outputFileThumbPath
142+ outputFileThumbPath ,
143+ mime ,
144+ ext
145145 ]
146146 ) ;
147147
148- if ( responseType === 'md' && imageMimeTypes . includes ( mimeType ) ) {
148+ if ( responseType === 'md' && imageMimeTypes . includes ( mime ) ) {
149149 responses . push ( {
150150 filepath : ``
151151 } ) ;
@@ -173,11 +173,53 @@ router.get('/files', async (ctx) => {
173173 try {
174174 const limit = parseInt ( ctx . query . limit , 10 ) || 10 ; // 每页数量,默认为 10
175175 const offset = parseInt ( ctx . query . offset , 10 ) || 0 ; // 偏移量,默认为 0
176+ const type = ctx . query . type ?? '' ; // 获取查询参数中的类型
176177
178+ const types = {
179+ image : 'image' ,
180+ video : 'video' ,
181+ all : '' ,
182+ }
183+
184+ const excludedTypes = [ 'image' , 'video' ] ; // 要排除的类型
185+
186+ let mimeCondition = '' ; // 初始化mime条件
187+
188+ // 构建 mime 条件
189+ if ( type === 'file' ) {
190+ mimeCondition = excludedTypes . map ( t => `mime NOT LIKE '%${ t } %'` ) . join ( ' AND ' ) ;
191+ } else if ( types [ type ] ) {
192+ mimeCondition = `mime LIKE '%${ types [ type ] } %'` ;
193+ }
194+
195+ // 构建完整的 SQL 语句
196+ const sql = `
197+ SELECT
198+ created_by,
199+ created_at,
200+ public_by,
201+ public_expiration,
202+ updated_at,
203+ updated_by,
204+ filesize,
205+ filename,
206+ filelocation,
207+ thumb_location,
208+ is_public
209+ FROM
210+ files
211+ WHERE
212+ is_delete = 0
213+ AND is_public = 1
214+ ${ mimeCondition ? `AND ${ mimeCondition } ` : '' }
215+ LIMIT ? OFFSET ?` ;
216+
217+ // 执行查询
177218 const [ rows ] = await connection . execute (
178- `SELECT created_by, created_at, public_by, public_expiration, updated_at, updated_by, filesize, filename, filelocation, thumb_location, is_public FROM files WHERE is_delete = 0 AND is_public = 1 LIMIT ? OFFSET ?` ,
219+ sql ,
179220 [ String ( limit ) , String ( offset ) ]
180221 ) ;
222+
181223
182224 ctx . body = rows ;
183225 } catch ( error ) {
@@ -196,12 +238,22 @@ router.get('/files/:id', async (ctx) => {
196238 try {
197239 // 查询文件数据,只获取必要字段
198240 const [ rows ] = await connection . execute (
199- `SELECT filename, is_delete, is_public, public_expiration, real_file_location, real_file_thumb_location, is_thumb
200- FROM files
201- WHERE id = ?
202- AND is_delete = 0
203- AND (is_public = 1 AND (public_expiration IS NULL OR public_expiration > NOW()))` ,
204- [ id ]
241+ `
242+ SELECT
243+ filename,
244+ is_delete,
245+ is_public,
246+ public_expiration,
247+ real_file_location,
248+ real_file_thumb_location,
249+ is_thumb,
250+ mime,
251+ ext
252+ FROM files
253+ WHERE id = ?
254+ AND is_delete = 0
255+ AND (is_public = 1 AND (public_expiration IS NULL OR public_expiration > NOW()))` ,
256+ [ id ]
205257 ) ;
206258
207259 if ( rows . length === 0 ) {
@@ -224,9 +276,9 @@ router.get('/files/:id', async (ctx) => {
224276 ctx . body = { message : 'File not found' } ;
225277 return ;
226278 }
227-
279+ const { mime } = await detectFileType ( fileLocation ) ;
228280 // 设置响应头
229- ctx . set ( 'Content-Type' , getMimeType ( fileLocation ) ) ;
281+ ctx . set ( 'Content-Type' , mime ) ;
230282 ctx . set ( 'Content-Disposition' , `inline; filename="${ file . filename } "` ) ;
231283
232284 // 返回文件流
0 commit comments