summary refs log tree commit diff
path: root/src/cdn/util/S3Storage.ts
blob: 6ed7cad7014c02272c8f4a2aa70035edddff0665 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/*
	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 endpoint: string,
        private basePath?: string,
    ) {
        const { S3 } = require("@aws-sdk/client-s3");
        this.client = new S3({ region: region, endpoint: endpoint });
    }

    /**
     * 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}`,
        });
    }
}