/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
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
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Readable } from "stream";
import { Storage } from "./Storage";
const readableToBuffer = (readable: Readable): Promise<Buffer> =>
new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
readable.on("data", (chunk) => chunks.push(chunk));
readable.on("error", reject);
readable.on("end", () => resolve(Buffer.concat(chunks)));
});
export class S3Storage implements Storage {
private client: unknown;
public constructor(
private region: string,
private bucket: string,
private basePath?: string,
) {
const { S3 } = require("@aws-sdk/client-s3");
this.client = new S3({ region });
}
/**
* Always return a string, to ensure consistency.
*/
get bucketBasePath() {
return this.basePath ?? "";
}
async set(path: string, data: Buffer): Promise<void> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
await this.client.putObject({
Bucket: this.bucket,
Key: `${this.bucketBasePath}${path}`,
Body: data,
});
}
async clone(path: string, newPath: string): Promise<void> {
// TODO: does this even work?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
await this.client.copyObject({
Bucket: this.bucket,
CopySource: `/${this.bucket}/${this.bucketBasePath}${path}`,
Key: `${this.bucketBasePath}${newPath}`,
});
}
async get(path: string): Promise<Buffer | null> {
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const s3Object = await this.client.getObject({
Bucket: this.bucket,
Key: `${this.bucketBasePath ?? ""}${path}`,
});
if (!s3Object.Body) return null;
const body = s3Object.Body;
return await readableToBuffer(<Readable>body);
} catch (err) {
console.error(`[CDN] Unable to get S3 object at path ${path}.`);
console.error(err);
return null;
}
}
async delete(path: string): Promise<void> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
await this.client.deleteObject({
Bucket: this.bucket,
Key: `${this.bucketBasePath}${path}`,
});
}
}