@@ -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 } ) ;
@@ -167,17 +167,58 @@ router.post('/upload', async (ctx) => {
167167 }
168168} ) ;
169169
170-
171170router . get ( '/files' , async ( ctx ) => {
172171 const connection = await pool . getConnection ( ) ;
173172 try {
174173 const limit = parseInt ( ctx . query . limit , 10 ) || 10 ; // 每页数量,默认为 10
175174 const offset = parseInt ( ctx . query . offset , 10 ) || 0 ; // 偏移量,默认为 0
175+ const type = ctx . query . type ?? '' ; // 获取查询参数中的类型
176176
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+ // 执行查询
177217 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 ?` ,
218+ sql ,
179219 [ String ( limit ) , String ( offset ) ]
180220 ) ;
221+
181222
182223 ctx . body = rows ;
183224 } catch ( error ) {
@@ -196,12 +237,22 @@ router.get('/files/:id', async (ctx) => {
196237 try {
197238 // 查询文件数据,只获取必要字段
198239 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 ]
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 ]
205256 ) ;
206257
207258 if ( rows . length === 0 ) {
@@ -224,9 +275,9 @@ router.get('/files/:id', async (ctx) => {
224275 ctx . body = { message : 'File not found' } ;
225276 return ;
226277 }
227-
278+ const { mime } = await detectFileType ( fileLocation ) ;
228279 // 设置响应头
229- ctx . set ( 'Content-Type' , getMimeType ( fileLocation ) ) ;
280+ ctx . set ( 'Content-Type' , mime ) ;
230281 ctx . set ( 'Content-Disposition' , `inline; filename="${ file . filename } "` ) ;
231282
232283 // 返回文件流
0 commit comments