* chore: migrate from s3 sdk v2 to v3 * import signature-v4-crt * downgrade minor version * Add s3-presigned-post manually * Change s3 mock * Update server/storage/files/S3Storage.ts * docs * Upgrade aws-sdk --------- Co-authored-by: Tom Moor <tom.moor@gmail.com>
147 lines
3.4 KiB
TypeScript
147 lines
3.4 KiB
TypeScript
import { Blob } from "buffer";
|
|
import { mkdir, unlink, rmdir } from "fs/promises";
|
|
import path from "path";
|
|
import { Readable } from "stream";
|
|
import fs from "fs-extra";
|
|
import invariant from "invariant";
|
|
import JWT from "jsonwebtoken";
|
|
import safeResolvePath from "resolve-path";
|
|
import env from "@server/env";
|
|
import { ValidationError } from "@server/errors";
|
|
import Logger from "@server/logging/Logger";
|
|
import BaseStorage from "./BaseStorage";
|
|
|
|
export default class LocalStorage extends BaseStorage {
|
|
public async getPresignedPost(
|
|
key: string,
|
|
acl: string,
|
|
maxUploadSize: number,
|
|
contentType = "image"
|
|
) {
|
|
return Promise.resolve({
|
|
url: this.getUrlForKey(key),
|
|
fields: {
|
|
key,
|
|
acl,
|
|
maxUploadSize,
|
|
contentType,
|
|
},
|
|
} as any);
|
|
}
|
|
|
|
public getUploadUrl() {
|
|
return "/api/files.create";
|
|
}
|
|
|
|
public getUrlForKey(key: string): string {
|
|
return `/api/files.get?key=${key}`;
|
|
}
|
|
|
|
public store = async ({
|
|
body,
|
|
key,
|
|
}: {
|
|
body: string | fs.ReadStream | Buffer | Uint8Array | Blob;
|
|
contentLength?: number;
|
|
contentType?: string;
|
|
key: string;
|
|
acl?: string;
|
|
}) => {
|
|
const exists = await fs.pathExists(this.getFilePath(key));
|
|
if (exists) {
|
|
throw ValidationError(`File already exists at ${key}`);
|
|
}
|
|
|
|
await mkdir(this.getFilePath(path.dirname(key)), {
|
|
recursive: true,
|
|
});
|
|
|
|
let src: NodeJS.ReadableStream;
|
|
if (body instanceof fs.ReadStream) {
|
|
src = body;
|
|
} else if (body instanceof Blob) {
|
|
src = Readable.from(Buffer.from(await body.arrayBuffer()));
|
|
} else {
|
|
src = Readable.from(body);
|
|
}
|
|
|
|
const filePath = this.getFilePath(key);
|
|
|
|
// Create the file on disk first
|
|
await fs.createFile(filePath);
|
|
|
|
return new Promise<string>((resolve, reject) => {
|
|
const dest = fs
|
|
.createWriteStream(filePath)
|
|
.on("error", reject)
|
|
.on("finish", () => resolve(this.getUrlForKey(key)));
|
|
|
|
src
|
|
.on("error", (err) => {
|
|
dest.end();
|
|
reject(err);
|
|
})
|
|
.pipe(dest);
|
|
});
|
|
};
|
|
|
|
public async deleteFile(key: string) {
|
|
const filePath = this.getFilePath(key);
|
|
try {
|
|
await unlink(filePath);
|
|
} catch (err) {
|
|
Logger.warn(`Couldn't delete ${filePath}`, err);
|
|
return;
|
|
}
|
|
|
|
const directory = path.dirname(filePath);
|
|
try {
|
|
await rmdir(directory);
|
|
} catch (err) {
|
|
if (err.code === "ENOTEMPTY") {
|
|
return;
|
|
}
|
|
Logger.warn(`Couldn't delete directory ${directory}`, err);
|
|
}
|
|
}
|
|
|
|
public getSignedUrl = async (
|
|
key: string,
|
|
expiresIn = LocalStorage.defaultSignedUrlExpires
|
|
) => {
|
|
const sig = JWT.sign(
|
|
{
|
|
key,
|
|
type: "attachment",
|
|
},
|
|
env.SECRET_KEY,
|
|
{
|
|
expiresIn,
|
|
}
|
|
);
|
|
return Promise.resolve(`${env.URL}/api/files.get?sig=${sig}`);
|
|
};
|
|
|
|
public async getFileHandle(key: string) {
|
|
return {
|
|
path: this.getFilePath(key),
|
|
cleanup: async () => {
|
|
// no-op, as we're reading the canonical file directly
|
|
},
|
|
};
|
|
}
|
|
|
|
public getFileStream(key: string) {
|
|
return Promise.resolve(fs.createReadStream(this.getFilePath(key)));
|
|
}
|
|
|
|
private getFilePath(key: string) {
|
|
invariant(
|
|
env.FILE_STORAGE_LOCAL_ROOT_DIR,
|
|
"FILE_STORAGE_LOCAL_ROOT_DIR is required"
|
|
);
|
|
|
|
return safeResolvePath(env.FILE_STORAGE_LOCAL_ROOT_DIR, key);
|
|
}
|
|
}
|