diff --git a/packages/var-proxy b/packages/var-proxy index f046853..c92f817 160000 --- a/packages/var-proxy +++ b/packages/var-proxy @@ -1 +1 @@ -Subproject commit f0468539dc53f89a509187a95ba9823702f62105 +Subproject commit c92f817d667e33e8fcdfad4d44f4a63df0534db5 diff --git a/src/lib/upload.ts b/src/lib/upload.ts index b3994ae..38859ab 100644 --- a/src/lib/upload.ts +++ b/src/lib/upload.ts @@ -3,13 +3,10 @@ import http from 'http'; import fs, { rm } from 'fs'; import path from 'path'; import { IncomingForm } from 'formidable'; -import { checkToken } from '@abearxiong/auth'; -import { useConfig } from '@abearxiong/use-config'; import { app, minioClient } from '@/app.ts'; import { bucketName } from '@/modules/minio.ts'; import { getContentType } from '@/utils/get-content-type.ts'; import { User } from '@/models/user.ts'; -const { tokenSecret } = useConfig<{ tokenSecret: string }>(); const filePath = useFileStore('upload', { needExists: true }); // curl -X POST http://localhost:4000/api/upload -F "file=@readme.md" // curl -X POST http://localhost:4000/api/upload \ @@ -24,76 +21,15 @@ export const uploadMiddleware = async (req: http.IncomingMessage, res: http.Serv res.end('Upload API is ready'); return; } - if (false && req.method === 'POST' && req.url === '/api/app/upload') { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - - // 检查 Content-Type 是否为 multipart/form-data - const contentType = req.headers['content-type']; - if (!contentType || !contentType.startsWith('multipart/form-data')) { - res.end('Invalid content type, expecting multipart/form-data'); - return; - } - // 提取 boundary (边界) 标识 - const boundary = contentType.split('boundary=')[1]; - if (!boundary) { - res.end('Invalid multipart/form-data format'); - return; - } - - // 将接收到的所有数据存入临时数组中 - let rawData = Buffer.alloc(0); - req.on('data', (chunk) => { - rawData = Buffer.concat([rawData, chunk]); - }); - - req.on('end', () => { - // 解析所有文件部分 - const parts = parseMultipartData(rawData, boundary); - - // 存储上传文件结果 - const uploadResults = []; - - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - if (part.filename) { - if (!fs.existsSync(filePath)) { - fs.mkdirSync(filePath, { recursive: true }); - } - const tempFilePath = path.join(filePath, part.filename); - fs.writeFileSync(tempFilePath, part.data); - uploadResults.push(`File ${part.filename} uploaded successfully.`); - // 上传到 MinIO - // minioClient.fPutObject(bucketName, part.filename, tempFilePath, {}, (err, etag) => { - // fs.unlinkSync(tempFilePath); // 删除临时文件 - // if (err) { - // uploadResults.push(`Upload error for ${part.filename}: ${err.message}`); - // } else { - // uploadResults.push(`File ${part.filename} uploaded successfully. ETag: ${etag}`); - // } - - // // 如果所有文件都处理完毕,返回结果 - // if (uploadResults.length === parts.length) { - // res.writeHead(200, { 'Content-Type': 'text/plain' }); - // res.end(uploadResults.join('\n')); - // } - // }); - } - } - res.end(uploadResults.join('\n')); - }); - } - if (req.method === 'POST' && req.url === '/api/app/upload') { - if (res.headersSent) return; // 如果响应已发送,不再处理 - - res.writeHead(200, { 'Content-Type': 'application/json' }); + const error = (msg: string) => { + return JSON.stringify({ code: 500, message: msg }); + }; + const checkAuth = async () => { const authroization = req.headers?.['authorization'] as string; - const error = (msg: string) => { - return JSON.stringify({ code: 500, message: msg }); - }; if (!authroization) { res.statusCode = 401; res.end(error('Invalid authorization')); - return; + return { tokenUser: null, token: null }; } const token = authroization.split(' ')[1]; let tokenUser; @@ -102,8 +38,57 @@ export const uploadMiddleware = async (req: http.IncomingMessage, res: http.Serv } catch (e) { res.statusCode = 401; res.end(error('Invalid token')); - return; + return { tokenUser: null, token: null }; } + return { tokenUser, token }; + }; + if (req.method === 'POST' && req.url === '/api/upload') { + if (res.headersSent) return; // 如果响应已发送,不再处理 + res.writeHead(200, { 'Content-Type': 'application/json' }); + const { tokenUser } = await checkAuth(); + if (!tokenUser) return; + // 使用 formidable 解析 multipart/form-data + const form = new IncomingForm({ + multiples: true, // 支持多文件上传 + uploadDir: filePath, // 上传文件存储目录 + }); + // 解析上传的文件 + form.parse(req, async (err, fields, files) => { + if (err) { + res.end(error(`Upload error: ${err.message}`)); + return; + } + // 逐个处理每个上传的文件 + const uploadedFiles = Array.isArray(files.file) ? files.file : [files.file]; + const uploadResults = []; + for (let i = 0; i < uploadedFiles.length; i++) { + const file = uploadedFiles[i]; + // @ts-ignore + const tempPath = file.filepath; // 文件上传时的临时路径 + const relativePath = file.originalFilename; // 保留表单中上传的文件名 (包含文件夹结构) + // 比如 child2/b.txt + const minioPath = `${tokenUser.username}/${relativePath}`; + // 上传到 MinIO 并保留文件夹结构 + const isHTML = relativePath.endsWith('.html'); + await minioClient.fPutObject(bucketName, minioPath, tempPath, { + 'Content-Type': getContentType(relativePath), + 'app-source': 'user-files', + 'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年 + }); + uploadResults.push({ + name: relativePath, + path: minioPath, + }); + fs.unlinkSync(tempPath); // 删除临时文件 + } + res.end(JSON.stringify({ code: 200, data: uploadResults })); + }); + } + if (req.method === 'POST' && req.url === '/api/app/upload') { + if (res.headersSent) return; // 如果响应已发送,不再处理 + res.writeHead(200, { 'Content-Type': 'application/json' }); + const { tokenUser, token } = await checkAuth(); + if (!tokenUser) return; // // 使用 formidable 解析 multipart/form-data const form = new IncomingForm({ @@ -170,6 +155,7 @@ export const uploadMiddleware = async (req: http.IncomingMessage, res: http.Serv }); fs.unlinkSync(tempPath); // 删除临时文件 } + // 受控 const r = await app.call({ path: 'app', key: 'uploadFiles', @@ -193,44 +179,3 @@ export const uploadMiddleware = async (req: http.IncomingMessage, res: http.Serv }); } }; - -/** - * 解析 multipart/form-data 格式数据,提取各个字段和文件内容 - * @param {Buffer} buffer - 完整的 HTTP 请求体数据 - * @param {string} boundary - multipart/form-data 的 boundary 标识符 - * @returns {Array} 返回包含各个部分数据的数组 - */ -function parseMultipartData(buffer, boundary) { - const parts = []; - const boundaryBuffer = Buffer.from(`--${boundary}`, 'utf-8'); - let start = buffer.indexOf(boundaryBuffer) + boundaryBuffer.length + 2; // Skip first boundary and \r\n - - while (start < buffer.length) { - // 查找下一个 boundary 的位置 - const end = buffer.indexOf(boundaryBuffer, start) - 2; // Subtract 2 to remove trailing \r\n - if (end <= start) break; - - // 提取单个 part 数据 - const part = buffer.slice(start, end); - start = end + boundaryBuffer.length + 2; // Move start to next part - - // 分割 part 头和内容 - const headerEndIndex = part.indexOf('\r\n\r\n'); - const headers = part.slice(0, headerEndIndex).toString(); - const content = part.slice(headerEndIndex + 4); // Skip \r\n\r\n - - // 解析 headers 以获取字段名称和文件信息 - const nameMatch = headers.match(/name="([^"]+)"/); - const filenameMatch = headers.match(/filename="([^"]+)"/); - - const partData = { - name: nameMatch ? nameMatch[1] : null, - filename: filenameMatch ? filenameMatch[1] : null, - data: content, - }; - - parts.push(partData); - } - - return parts; -} diff --git a/src/models/user.ts b/src/models/user.ts index 48af790..dc4bfcc 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -25,6 +25,7 @@ export class User extends Model { declare owner: string; declare orgId: string; declare email: string; + declare avatar: string; async createToken(uid?: string) { const { id, username, type } = this; const expireTime = 60 * 60 * 24 * 7; // 7 days @@ -85,6 +86,7 @@ export class User extends Model { description: this.description, needChangePassword: this.needChangePassword, type: this.type, + avatar: this.avatar, orgs, }; } @@ -131,6 +133,10 @@ User.init( type: DataTypes.STRING, allowNull: true, }, + avatar: { + type: DataTypes.STRING, + allowNull: true, + }, salt: { type: DataTypes.STRING, allowNull: true, diff --git a/src/routes/page/list.ts b/src/routes/page/list.ts index 327e96e..8a0e572 100644 --- a/src/routes/page/list.ts +++ b/src/routes/page/list.ts @@ -5,7 +5,13 @@ import { v4 as uuidv4 } from 'uuid'; import { ContainerModel } from '../container/models/index.ts'; import { Op } from 'sequelize'; import { getDeck } from './module/cache-file.ts'; - +export const clearBlank = (newStyle: any) => { + for (let key in newStyle) { + if (newStyle[key] === '' || newStyle[key] === undefined || newStyle[key] === null) { + delete newStyle[key]; + } + } +}; app .route({ path: 'page', @@ -82,6 +88,7 @@ app .route('page', 'updateNode') .define(async (ctx) => { const { id, nodeData } = ctx.query.data; + const force = ctx.query.force; if (!id) { throw new CustomError('id is required'); } @@ -98,16 +105,16 @@ app flag = true; const { data, ...rest } = nodeItem; const { style, ...restData } = data || {}; + let newStyle = force ? { ...style } : { ...item?.data?.style, ...style }; + clearBlank(newStyle); + console.log('newStyle', newStyle); const newNodeItem = { ...item, ...rest, data: { ...item?.data, ...restData, - style: { - ...item?.data?.style, - ...style, - }, + style: newStyle, }, }; console.log('newNodeItem', newNodeItem); diff --git a/src/routes/user/me.ts b/src/routes/user/me.ts index df3f539..7631718 100644 --- a/src/routes/user/me.ts +++ b/src/routes/user/me.ts @@ -67,7 +67,7 @@ app middleware: ['auth'], }) .define(async (ctx) => { - const { username, password, description } = ctx.query; + const { username, password, description, avatar, email } = ctx.query.data || {}; const state = ctx.state?.tokenUser || {}; const { id } = state; const user = await User.findByPk(id); @@ -83,6 +83,12 @@ app if (description) { user.description = description; } + if (avatar) { + user.avatar = avatar; + } + if (email) { + user.email = email; + } await user.save(); ctx.body = await user.getInfo(); })