Files
code-center/src/routes-simple/resources/chunk.ts
2026-01-15 18:02:49 +08:00

237 lines
7.1 KiB
TypeScript

import { useFileStore } from '@kevisual/use-config';
import { checkAuth, error, router, writeEvents, getKey, getTaskId } from '../router.ts';
import Busboy from 'busboy';
import { app, oss } from '@/app.ts';
import { getContentType } from '@/utils/get-content-type.ts';
import { User } from '@/models/user.ts';
import fs from 'fs';
import { ConfigModel } from '@/routes/config/models/model.ts';
import { validateDirectory } from './util.ts';
import path from 'path';
import { createWriteStream } from 'fs';
import { pipeBusboy } from '@/modules/fm-manager/index.ts';
const cacheFilePath = useFileStore('cache-file', { needExists: true });
router.get('/api/s1/resources/upload/chunk', async (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Upload API is ready');
});
// /api/s1/resources/upload
router.post('/api/s1/resources/upload/chunk', async (req, res) => {
const { tokenUser, token } = await checkAuth(req, res);
if (!tokenUser) return;
const url = new URL(req.url || '', 'http://localhost');
const share = !!url.searchParams.get('public');
const noCheckAppFiles = !!url.searchParams.get('noCheckAppFiles');
const taskId = getTaskId(req);
const finalFilePath = `${cacheFilePath}/${taskId}`;
if (!taskId) {
res.end(error('taskId is required'));
return;
}
// 使用 busboy 解析 multipart/form-data
const busboy = Busboy({ headers: req.headers, preservePath: true });
const fields: any = {};
let file: any = null;
let tempPath = '';
let filePromise: Promise<void> | null = null;
busboy.on('field', (fieldname, value) => {
fields[fieldname] = value;
});
busboy.on('file', (fieldname, fileStream, info) => {
const { filename, encoding, mimeType } = info;
tempPath = path.join(cacheFilePath, `${Date.now()}-${Math.random().toString(36).substring(7)}`);
const writeStream = createWriteStream(tempPath);
filePromise = new Promise<void>((resolve, reject) => {
fileStream.pipe(writeStream);
writeStream.on('finish', () => {
file = {
filepath: tempPath,
originalFilename: filename,
mimetype: mimeType,
};
resolve();
});
writeStream.on('error', (err) => {
reject(err);
});
});
});
busboy.on('finish', async () => {
// 等待文件写入完成
if (filePromise) {
try {
await filePromise;
} catch (err) {
console.error(`File write error: ${err.message}`);
res.end(error(`File write error: ${err.message}`));
return;
}
}
const clearFiles = () => {
if (tempPath && fs.existsSync(tempPath)) {
fs.unlinkSync(tempPath);
}
if (fs.existsSync(finalFilePath)) {
fs.unlinkSync(finalFilePath);
}
};
if (!file) {
res.end(error('No file uploaded'));
return;
}
// Handle chunked upload logic here
let { chunkIndex, totalChunks, appKey, version, username, directory } = getKey(fields, [
'chunkIndex',
'totalChunks',
'appKey',
'version',
'username',
'directory',
]);
if (!chunkIndex || !totalChunks) {
res.end(error('chunkIndex, totalChunks is required'));
clearFiles();
return;
}
const relativePath = file.originalFilename;
const writeStream = fs.createWriteStream(finalFilePath, { flags: 'a' });
const readStream = fs.createReadStream(tempPath);
readStream.pipe(writeStream);
writeStream.on('finish', async () => {
fs.unlinkSync(tempPath); // 删除临时文件
// Write event for progress tracking
const progress = ((parseInt(chunkIndex) + 1) / parseInt(totalChunks)) * 100;
writeEvents(req, {
progress,
message: `Upload progress: ${progress}%`,
});
if (parseInt(chunkIndex) + 1 === parseInt(totalChunks)) {
let uid = tokenUser.id;
if (username) {
const user = await User.getUserByToken(token);
const has = await user.hasUser(username, true);
if (!has) {
res.end(error('username is not found'));
clearFiles();
return;
}
const _user = await User.findOne({ where: { username } });
uid = _user?.id || '';
}
if (!appKey || !version) {
const config = await ConfigModel.getUploadConfig({ uid });
if (config) {
appKey = config.config?.data?.key || '';
version = config.config?.data?.version || '';
}
}
if (!appKey || !version) {
res.end(error('appKey or version is not found, please check the upload config.'));
clearFiles();
return;
}
const { code, message } = validateDirectory(directory);
if (code !== 200) {
res.end(error(message));
clearFiles();
return;
}
const minioPath = `${username || tokenUser.username}/${appKey}/${version}${directory ? `/${directory}` : ''}/${relativePath}`;
const metadata: any = {};
if (share) {
metadata.share = 'public';
}
const bucketName = oss.bucketName;
// All chunks uploaded, now upload to MinIO
await oss.client.fPutObject(bucketName, minioPath, finalFilePath, {
'Content-Type': getContentType(relativePath),
'app-source': 'user-app',
'Cache-Control': relativePath.endsWith('.html') ? 'no-cache' : 'max-age=31536000, immutable',
...metadata,
});
// Clean up the final file
fs.unlinkSync(finalFilePath);
const downloadBase = '/api/s1/share';
const uploadResult = {
name: relativePath,
path: `${downloadBase}/${minioPath}`,
appKey,
version,
username,
};
if (!noCheckAppFiles) {
// Notify the app
const r = await app.call({
path: 'app',
key: 'detectVersionList',
payload: {
token: token,
data: {
appKey,
version,
username,
},
},
});
const data: any = {
code: r.code,
data: {
app: r.body,
upload: [uploadResult],
},
};
if (r.message) {
data.message = r.message;
}
console.log('upload data', data);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
code: 200,
message: 'Chunk uploaded successfully',
data: { chunkIndex, totalChunks, upload: [uploadResult] },
}),
);
}
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
code: 200,
message: 'Chunk uploaded successfully',
data: {
chunkIndex,
totalChunks,
},
}),
);
}
});
});
pipeBusboy(req, res, busboy);
});