From c167a7b5416057f43e0fd8240f59268b88fefac1 Mon Sep 17 00:00:00 2001 From: Rory& Date: Tue, 23 Sep 2025 23:09:47 +0200 Subject: [PATCH] Implement cloning cloud attachments into messages --- src/cdn/routes/attachments.ts | 24 ++++++++++++++++++++++++ src/cdn/util/FileStorage.ts | 13 ++++++++++++- src/cdn/util/S3Storage.ts | 11 ++++++++++- src/cdn/util/Storage.ts | 3 ++- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/cdn/routes/attachments.ts b/src/cdn/routes/attachments.ts index 2ab75224..23ca141a 100644 --- a/src/cdn/routes/attachments.ts +++ b/src/cdn/routes/attachments.ts @@ -189,4 +189,28 @@ router.delete("/:channel_id/:batch_id/:attachment_id/:filename", async (req: Req return res.status(404).send("Attachment not found"); }); +router.post("/:channel_id/:batch_id/:attachment_id/:filename/clone_to_message/:message_id", async (req: Request, res: Response) => { + if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature"); + + const { channel_id, batch_id, attachment_id, filename } = req.params; + const path = `attachments/${channel_id}/${batch_id}/${attachment_id}/${filename}`; + const newPath = `attachments/${channel_id}/${filename}`; + + const att = await CloudAttachment.findOne({ + where: { + uploadFilename: `${channel_id}/${batch_id}/${attachment_id}/${filename}`, + channelId: channel_id, + userAttachmentId: attachment_id, + userFilename: filename, + }, + }); + + if (att) { + await storage.clone(path, newPath); + return res.send({ success: true, new_path: newPath }); + } + + return res.status(404).send("Attachment not found"); +}); + export default router; diff --git a/src/cdn/util/FileStorage.ts b/src/cdn/util/FileStorage.ts index 10b36743..9f81f426 100644 --- a/src/cdn/util/FileStorage.ts +++ b/src/cdn/util/FileStorage.ts @@ -1,6 +1,6 @@ /* Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar Contributors + Copyright (C) 2025 Spacebar and Spacebar Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -51,6 +51,17 @@ export class FileStorage implements Storage { } } + async clone(path: string, newPath: string) { + path = getPath(path); + newPath = getPath(newPath); + + if (!fs.existsSync(dirname(newPath))) + fs.mkdirSync(dirname(newPath), { recursive: true }); + + // use reflink if possible, in order to not duplicate files at the block layer... + fs.copyFileSync(path, newPath, fs.constants.COPYFILE_FICLONE) + } + async set(path: string, value: Buffer) { path = getPath(path); if (!fs.existsSync(dirname(path))) diff --git a/src/cdn/util/S3Storage.ts b/src/cdn/util/S3Storage.ts index 81acd945..c3a8d5c5 100644 --- a/src/cdn/util/S3Storage.ts +++ b/src/cdn/util/S3Storage.ts @@ -1,6 +1,6 @@ /* Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar Contributors + Copyright (C) 2025 Spacebar and Spacebar Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -50,6 +50,15 @@ export class S3Storage implements Storage { }); } + async clone(path: string, newPath: string): Promise { + // TODO: does this even work? + await this.client.copyObject({ + Bucket: this.bucket, + CopySource: `/${this.bucket}/${this.bucketBasePath}${path}`, + Key: `${this.bucketBasePath}${newPath}`, + }); + } + async get(path: string): Promise { try { const s3Object = await this.client.getObject({ diff --git a/src/cdn/util/Storage.ts b/src/cdn/util/Storage.ts index 26289af6..53292b2a 100644 --- a/src/cdn/util/Storage.ts +++ b/src/cdn/util/Storage.ts @@ -1,6 +1,6 @@ /* Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar Contributors + Copyright (C) 2025 Spacebar and Spacebar Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -25,6 +25,7 @@ process.cwd(); export interface Storage { set(path: string, data: Buffer): Promise; + clone(path: string, newPath: string): Promise; get(path: string): Promise; delete(path: string): Promise; }