Skip to content

Commit 7aed4df

Browse files
authored
Merge pull request #2 from CrazyMrYan/feat/full/1.1.0
refactor: Refactoring a project using sequence ✨
2 parents a3d5445 + 8e00324 commit 7aed4df

File tree

12 files changed

+620
-355
lines changed

12 files changed

+620
-355
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ jspm_packages/
4646

4747
#
4848
public/files/*
49+
provisional
4950
provisional/*
5051
privacy/*
52+
resource
53+
resource/*
5154

5255
# Snowpack dependency directory (https://snowpack.dev/)
5356
web_modules/

README.md

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
nodejs 实现文件上传功能
33

44
### 支持功能
5+
> 目前均为免费版本,Plus 版本还在规划中,Lite 和 Full 版本永久免费。
56
67
| 功能 | Lite(缩减版) | Full(全量版) | Plus(增强版) |
78
|--------------|----------------|----------------|--------------|
@@ -15,22 +16,67 @@ nodejs 实现文件上传功能
1516
| 公开失效时间 ||||
1617
| 高级搜索功能 ||||
1718

19+
### 使用方法
20+
21+
```shell
22+
yarn install
23+
24+
yarn dev
25+
```
26+
27+
> 启动完成之后,会自动创建 resource 作为资源文件夹,而 provisional 会作为临时文件夹。后续会考虑开启定时任务进行清理。
28+
29+
### 目录结构
30+
31+
```shell
32+
├── LICENSE
33+
├── README.md
34+
├── constants
35+
│ └── file.js
36+
├── index.js
37+
├── models # 表模型
38+
│ └── files.js
39+
├── package.json
40+
├── public
41+
│ ├── icons
42+
│ │ ├── doc.png
43+
│ │ ├── document.png
44+
│ │ ├── folders.png
45+
│ │ ├── pdf.png
46+
│ │ ├── psd.png
47+
│ │ ├── shared_folders.png
48+
│ │ ├── unknown_file_types.png
49+
│ │ ├── video.png
50+
│ │ ├── xlsx.png
51+
│ │ └── zip.png
52+
│ └── index.html
53+
├── routers # 路由
54+
│ └── files.js
55+
├── utils
56+
│ ├── createPath.js
57+
│ ├── dbInstance.js
58+
│ ├── detectFileType.js
59+
│ └── responseFormatter.js
60+
└── yarn.lock
61+
```
1862

1963
### 环境变量
20-
可以创建一个 `.env.local` 文件,在里面配置对应的环境变量
64+
创建一个 `.env.local` 文件,在里面配置对应的环境变量
2165

22-
- TINIFY_KEY=
23-
- INTERNAL_NETWORK_DOMAIN=
24-
- PUBLIC_NETWORK_DOMAIN=
25-
- SERVER_PORT=
26-
- MYSQL_DATABASE=
27-
- MYSQL_HOST=
28-
- MYSQL_USER=
29-
- MYSQL_PASSWORD=
66+
TINIFY_KEY=
67+
INTERNAL_NETWORK_DOMAIN=http://localhost:3000
68+
PUBLIC_NETWORK_DOMAIN=http://localhost:3000
69+
SERVER_PORT=3000
70+
DIALECT=mysql
71+
MYSQL_DATABASE=
72+
MYSQL_HOST=
73+
MYSQL_USER=root
74+
MYSQL_PASSWORD=
75+
MYSQL_PORT=3306
3076

3177
### 创建表的字段说明
3278

33-
> 启动项目则会,自动创建,默认创建 `files` 表结构
79+
> 启动项目则会,自动创建创建 `files` 表结构
3480
3581
```sql
3682
CREATE TABLE files (

index.js

Lines changed: 8 additions & 261 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,21 @@
1+
// app.js
12
const Koa = require('koa');
2-
const Router = require('koa-router');
33
const { koaBody } = require('koa-body');
4-
const tinify = require('tinify');
54
const path = require('path');
65
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+
149
require('dotenv').config({ path: '.env.local' });
1510

1611
const app = new Koa();
17-
const router = new Router();
18-
19-
tinify.key = process.env.TINIFY_KEY;
2012

2113
app.use(require('koa-static')(path.join(__dirname, 'public')));
2214

2315
const 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: `![${path.basename(outputFilePath)}](${fileUrl})`
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

29542
app.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

Comments
 (0)