From 8b3b32422f37dfe578928baca8c4cfc11fe76fb3 Mon Sep 17 00:00:00 2001 From: CrescentLeaf Date: Sat, 1 Nov 2025 01:12:50 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BD=BF=E7=94=A8=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E8=BF=9B=E8=A1=8C=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=EF=BC=81=20*=20=E5=8F=AF=E4=BB=A5=E4=B8=8A=E4=BC=A0=E5=A4=A7?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=95=A6=20*=20=E6=9C=80=E5=A4=A7=E9=99=90?= =?UTF-8?q?=E5=88=B6=202GB=20*=20=E5=90=8E=E7=AB=AF=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E9=87=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/api/Client.ts | 31 ++++++++++++ client/ui/chat/ChatFragment.tsx | 20 ++++++-- client/ui/dialog/MyProfileDialog.tsx | 15 ++++-- deno.jsonc | 3 +- server/api/ChatApi.ts | 15 +++--- server/api/UserApi.ts | 10 ++-- server/main.ts | 70 +++++++++++++++++++++++++--- 7 files changed, 134 insertions(+), 30 deletions(-) diff --git a/client/api/Client.ts b/client/api/Client.ts index 5744906..04e024c 100644 --- a/client/api/Client.ts +++ b/client/api/Client.ts @@ -116,6 +116,37 @@ class Client { } return re } + static async uploadFileLikeApi(fileName: string, fileData: ArrayBuffer | Blob | Response, chatId?: string) { + const form = new FormData() + form.append("file", + fileData instanceof ArrayBuffer + ? new File([fileData], fileName, { type: 'application/octet-stream' }) + : ( + fileData instanceof Blob ? fileData : + new File([await fileData.arrayBuffer()], fileName, { type: 'application/octet-stream' }) + ) + ) + form.append('file_name', fileName) + chatId && form.append('chat_id', chatId) + const re = await fetch('./upload_file', { + method: 'POST', + headers: { + "Token": data.access_token, + "Device-Id": data.device_id, + } as HeadersInit, + body: form, + credentials: 'omit', + }) + return { + ...await re.json(), + code: re.status, + } as ApiCallbackMessage + } + static async uploadFile(fileName: string, fileData: ArrayBuffer | Blob | Response, chatId?: string) { + const re = await this.uploadFileLikeApi(fileName, fileData, chatId) + if (re.code != 200) throw new Error(re.msg) + return re.data!.hash as string + } static async updateCachedProfile() { this.myUserProfile = (await Client.invoke("User.getMyInfo", { token: data.access_token diff --git a/client/ui/chat/ChatFragment.tsx b/client/ui/chat/ChatFragment.tsx index 0e8b761..827e634 100644 --- a/client/ui/chat/ChatFragment.tsx +++ b/client/ui/chat/ChatFragment.tsx @@ -167,12 +167,16 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC setIsMessageSending(true) for (const fileName of Object.keys(cachedFiles.current)) { if (text.indexOf(fileName) != -1) { - const re = await Client.invoke("Chat.uploadFile", { + /* const re = await Client.invoke("Chat.uploadFile", { token: data.access_token, file_name: fileName, target, data: cachedFiles.current[fileName], - }, 5000) + }, 5000) */ + const re = await Client.uploadFileLikeApi( + fileName, + cachedFiles.current[fileName] + ) if (checkApiSuccessOrSncakbar(re, `文件[${fileName}] 上传失败`)) return setIsMessageSending(false) text = text.replaceAll('(' + fileName + ')', '(tws://file?hash=' + re.data!.file_hash as string + ')') } @@ -225,16 +229,22 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC for (const file of files) { addFile(file.type, file.name, file) } - uploadChatAvatarRef.current!.value = '' + attachFileInputRef.current!.value = '' }) useEventListener(uploadChatAvatarRef, 'change', async (_e) => { const file = uploadChatAvatarRef.current!.files?.[0] as File if (file == null) return - const re = await Client.invoke("Chat.setAvatar", { + let re = await Client.uploadFileLikeApi( + 'avatar', + file + ) + if (checkApiSuccessOrSncakbar(re, "上传失败")) return + const hash = re.data!.file_hash + re = await Client.invoke("Chat.setAvatar", { token: data.access_token, target: target, - avatar: file + file_hash: hash, }) uploadChatAvatarRef.current!.value = '' diff --git a/client/ui/dialog/MyProfileDialog.tsx b/client/ui/dialog/MyProfileDialog.tsx index f6ef288..e240e06 100644 --- a/client/ui/dialog/MyProfileDialog.tsx +++ b/client/ui/dialog/MyProfileDialog.tsx @@ -21,14 +21,23 @@ export default function MyProfileDialog({ }: Refs) { const editAvatarButtonRef = React.useRef(null) const chooseAvatarFileRef = React.useRef(null) - useEventListener(editAvatarButtonRef, 'click', () => chooseAvatarFileRef.current!.click()) + useEventListener(editAvatarButtonRef, 'click', () => { + chooseAvatarFileRef.current!.value = '' + chooseAvatarFileRef.current!.click() + }) useEventListener(chooseAvatarFileRef, 'change', async (_e) => { const file = chooseAvatarFileRef.current!.files?.[0] as File if (file == null) return - const re = await Client.invoke("User.setAvatar", { + let re = await Client.uploadFileLikeApi( + 'avatar', + file + ) + if (checkApiSuccessOrSncakbar(re, "上传失败")) return + const hash = re.data!.file_hash + re = await Client.invoke("User.setAvatar", { token: data.access_token, - avatar: file + file_hash: hash }) if (checkApiSuccessOrSncakbar(re, "修改失败")) return diff --git a/deno.jsonc b/deno.jsonc index f5e5eb1..ced1114 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -9,6 +9,7 @@ "file-type": "npm:file-type@21.0.0", "express": "npm:express@5.1.0", "socket.io": "npm:socket.io@4.8.1", - "cookie-parser": "npm:cookie-parser@1.4.7" + "cookie-parser": "npm:cookie-parser@1.4.7", + "express-fileupload": "npm:express-fileupload@1.5.2" } } \ No newline at end of file diff --git a/server/api/ChatApi.ts b/server/api/ChatApi.ts index 9c5d10e..6266054 100644 --- a/server/api/ChatApi.ts +++ b/server/api/ChatApi.ts @@ -118,7 +118,7 @@ export default class ChatApi extends BaseApi { * @param file_name 文件名稱 * @param data 文件二進制數據 */ - this.registerEvent("Chat.uploadFile", async (args, { deviceId }) => { + /* this.registerEvent("Chat.uploadFile", async (args, { deviceId }) => { if (this.checkArgsMissing(args, ['token', 'target', 'data', 'file_name'])) return { msg: "参数缺失", code: 400, @@ -149,7 +149,7 @@ export default class ChatApi extends BaseApi { file_hash: file.getHash() }, } - }) + }) */ /** * ====================================================== * 加入对话申请 @@ -463,14 +463,14 @@ export default class ChatApi extends BaseApi { }) // 更新頭像 this.registerEvent("Chat.setAvatar", (args, { deviceId }) => { - if (this.checkArgsMissing(args, ['avatar', 'token'])) return { + if (this.checkArgsMissing(args, ['file_hash', 'token'])) return { msg: "参数缺失", code: 400, } - if (!(args.avatar instanceof Buffer)) return { + /* if (!(args.avatar instanceof Buffer)) return { msg: "参数不合法", code: 400, - } + } */ const token = TokenManager.decode(args.token as string) const user = User.findById(token.author) as User @@ -483,9 +483,10 @@ export default class ChatApi extends BaseApi { if (chat.bean.type == 'group') if (chat.checkUserIsAdmin(user.bean.id)) { - const avatar: Buffer = args.avatar as Buffer + chat.setAvatarFileHash(args.file_hash as string) + /* const avatar: Buffer = args.avatar as Buffer if (avatar) - chat.setAvatar(avatar) + chat.setAvatar(avatar) */ } else return { code: 403, diff --git a/server/api/UserApi.ts b/server/api/UserApi.ts index c79fec1..7869b8f 100644 --- a/server/api/UserApi.ts +++ b/server/api/UserApi.ts @@ -198,23 +198,19 @@ export default class UserApi extends BaseApi { */ // 更新頭像 this.registerEvent("User.setAvatar", (args, { deviceId }) => { - if (this.checkArgsMissing(args, ['avatar', 'token'])) return { + if (this.checkArgsMissing(args, ['file_hash', 'token'])) return { msg: "参数缺失", code: 400, } - if (!(args.avatar instanceof Buffer)) return { - msg: "参数不合法", - code: 400, - } const token = TokenManager.decode(args.token as string) if (!this.checkToken(token, deviceId)) return { code: 401, msg: "令牌无效", } - const avatar: Buffer = args.avatar as Buffer + // const avatar: Buffer = args.avatar as Buffer const user = User.findById(token.author) - user!.setAvatar(avatar) + user!.setAvatarFileHash(args.file_hash as string) //.setAvatar(avatar) return { msg: "成功", diff --git a/server/main.ts b/server/main.ts index 5af90b9..36b6992 100644 --- a/server/main.ts +++ b/server/main.ts @@ -16,32 +16,41 @@ import UserChatLinker from "./data/UserChatLinker.ts" import path from "node:path" import cookieParser from 'cookie-parser' import fs from 'node:fs/promises' +// @ts-types="npm:@types/express-fileupload" +import fileUpload from 'express-fileupload' const app = express() app.use('/', express.static(config.data_path + '/page_compiled')) app.use(cookieParser()) app.get('/uploaded_files/:hash', (req, res) => { const hash = req.params.hash as string - res.setHeader('Content-Type', 'text/plain') if (hash == null) { - res.status(404).send("404 Not Found") + res.status(404).send({ + msg: "404 Not Found", + }) return } const file = FileManager.findByHash(hash) if (file == null) { - res.status(404).send("404 Not Found") + res.status(404).send({ + msg: "404 Not Found", + }) return } if (file.getChatId() != null) { - const userToken = TokenManager.decode(req.headers['token'] || req.cookies.token) - if (!TokenManager.checkToken(userToken, req.headers['device_id'] || req.cookies.device_id)) { - res.status(401).send("401 UnAuthorized") + const userToken = TokenManager.decode(req.headers.token || req.cookies.token) + if (!TokenManager.checkToken(userToken, req.headers['device-id'] || req.cookies.device_id)) { + res.status(401).send({ + msg: "401 UnAuthorized", + }) return } if (!UserChatLinker.checkUserIsLinkedToChat(userToken.author, file.getChatId() as string)) { - res.status(403).send("403 Forbidden") + res.status(403).send({ + msg: "403 Forbidden", + }) return } } @@ -50,6 +59,53 @@ app.get('/uploaded_files/:hash', (req, res) => { res.setHeader('Content-Disposition', `inline; filename="${fileName}"`) res.setHeader('Content-Type', file!.getMime()) res.sendFile(path.resolve(file!.getFilePath())) + file.updateLastUsedTime() +}) + +await fs.mkdir(config.data_path + '/upload_cache', { recursive: true }) +app.use(fileUpload({ + limits: { fileSize: 2 * 1024 * 1024 * 1024 }, + useTempFiles: true, + tempFileDir: config.data_path + '/upload_cache', + abortOnLimit: true, +})) +app.post('/upload_file', async (req, res) => { + const userToken = TokenManager.decode(req.headers.token || req.cookies.token) + if (!TokenManager.checkToken(userToken, req.headers['device-id'] || req.cookies.device_id)) { + res.status(401).send({ + msg: "401 UnAuthorized", + }) + return + } + if (req.body.chat_id && !UserChatLinker.checkUserIsLinkedToChat(userToken.author, req.body.chat_id)) { + res.status(403).send({ + msg: "403 Forbidden", + }) + return + } + + const file = req.files?.file as fileUpload.UploadedFile + if (file?.data == null) { + res.status(400).send({ + msg: "No file was found or multiple files were uploaded", + }) + return + } + if (req.body.file_name == null) { + res.status(400).send({ + msg: "Filename is required", + }) + return + } + + const hash = (await FileManager.uploadFile(req.body.file_name, await fs.readFile(file.tempFilePath), req.body.chat_id)).getHash() + + res.status(200).send({ + msg: "success", + data: { + file_hash: hash, + }, + }) }) const httpServer: HttpServerLike = (