Media upload for avatars and banners, more work, fix tests

This commit is contained in:
Jesse Wierzbinski 2023-10-19 09:53:59 -10:00
parent 16cfd5d900
commit 460b68c381
No known key found for this signature in database
GPG key ID: F9A1E418934E40B0
17 changed files with 360 additions and 139 deletions

View file

@ -5,7 +5,7 @@ import {
S3Client,
} from "@aws-sdk/client-s3";
import { ConfigType } from "@config";
import sharp from "sharp";
class MediaBackend {
backend: string;
@ -16,26 +16,98 @@ class MediaBackend {
/**
* Adds media to the media backend
* @param media
* @returns The hash of the file in SHA-256 (hex format)
* @returns The hash of the file in SHA-256 (hex format) with the file extension added to it
*/
async addMedia(media: File) {
const hash = new Bun.SHA256()
.update(await media.arrayBuffer())
.digest("hex");
return hash;
return `${hash}.${media.name.split(".").pop()}`;
}
async convertMedia(media: File, config: ConfigType) {
const sharpCommand = sharp(await media.arrayBuffer());
// Rename ".jpg" files to ".jpeg" to avoid sharp errors
let name = media.name;
if (media.name.endsWith(".jpg")) {
name = media.name.replace(".jpg", ".jpeg");
}
const fileFormatToConvertTo = config.media.conversion.convert_to;
switch (fileFormatToConvertTo) {
case "png":
return new File(
[(await sharpCommand.png().toBuffer()).buffer],
// Replace the file extension with PNG
name.replace(/\.[^/.]+$/, ".png"),
{
type: "image/png",
}
);
case "webp":
return new File(
[(await sharpCommand.webp().toBuffer()).buffer],
// Replace the file extension with WebP
name.replace(/\.[^/.]+$/, ".webp"),
{
type: "image/webp",
}
);
case "jpeg":
return new File(
[(await sharpCommand.jpeg().toBuffer()).buffer],
// Replace the file extension with JPEG
name.replace(/\.[^/.]+$/, ".jpeg"),
{
type: "image/jpeg",
}
);
case "avif":
return new File(
[(await sharpCommand.avif().toBuffer()).buffer],
// Replace the file extension with AVIF
name.replace(/\.[^/.]+$/, ".avif"),
{
type: "image/avif",
}
);
// Needs special build of libvips
case "jxl":
return new File(
[(await sharpCommand.jxl().toBuffer()).buffer],
// Replace the file extension with JXL
name.replace(/\.[^/.]+$/, ".jxl"),
{
type: "image/jxl",
}
);
case "heif":
return new File(
[(await sharpCommand.heif().toBuffer()).buffer],
// Replace the file extension with HEIF
name.replace(/\.[^/.]+$/, ".heif"),
{
type: "image/heif",
}
);
default:
return media;
}
}
/**
* Retrieves element from media backend by hash
* @param hash The hash of the element in SHA-256 hex format
* @param extension The extension of the file
* @returns The file as a File object
*/
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
async getMediaByHash(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
hash: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
extension: string
hash: string
): Promise<File | null> {
return new File([], "test");
}
@ -45,35 +117,29 @@ class MediaBackend {
* S3 Backend, stores files in S3
*/
export class S3Backend extends MediaBackend {
endpoint: string;
bucket: string;
region: string;
accessKey: string;
secretKey: string;
publicUrl: string;
client: S3Client;
config: ConfigType;
constructor(config: ConfigType) {
super("s3");
this.endpoint = config.s3.endpoint;
this.bucket = config.s3.bucket_name;
this.region = config.s3.region;
this.accessKey = config.s3.access_key;
this.secretKey = config.s3.secret_access_key;
this.publicUrl = config.s3.public_url;
this.config = config;
this.client = new S3Client({
endpoint: this.endpoint,
region: this.region || "auto",
endpoint: this.config.s3.endpoint,
region: this.config.s3.region || "auto",
credentials: {
accessKeyId: this.accessKey,
secretAccessKey: this.secretKey,
accessKeyId: this.config.s3.access_key,
secretAccessKey: this.config.s3.secret_access_key,
},
});
}
async addMedia(media: File): Promise<string> {
if (this.config.media.conversion.convert_images) {
media = await this.convertMedia(media, this.config);
}
const hash = await super.addMedia(media);
if (!hash) {
@ -81,10 +147,7 @@ export class S3Backend extends MediaBackend {
}
// Check if file is already present
const existingFile = await this.getMediaByHash(
hash,
media.name.split(".").pop() || ""
);
const existingFile = await this.getMediaByHash(hash);
if (existingFile) {
// File already exists, so return the hash without uploading it
@ -92,7 +155,7 @@ export class S3Backend extends MediaBackend {
}
const command = new PutObjectCommand({
Bucket: this.bucket,
Bucket: this.config.s3.bucket_name,
Key: hash,
Body: Buffer.from(await media.arrayBuffer()),
ContentType: media.type,
@ -111,12 +174,9 @@ export class S3Backend extends MediaBackend {
return hash;
}
async getMediaByHash(
hash: string,
extension: string
): Promise<File | null> {
async getMediaByHash(hash: string): Promise<File | null> {
const command = new GetObjectCommand({
Bucket: this.bucket,
Bucket: this.config.s3.bucket_name,
Key: hash,
});
@ -138,7 +198,7 @@ export class S3Backend extends MediaBackend {
throw new Error("Failed to get file");
}
return new File([body], `${hash}.${extension}`, {
return new File([body], hash, {
type: response.ContentType,
});
}
@ -148,11 +208,19 @@ export class S3Backend extends MediaBackend {
* Local backend, stores files on filesystem
*/
export class LocalBackend extends MediaBackend {
constructor() {
config: ConfigType;
constructor(config: ConfigType) {
super("local");
this.config = config;
}
async addMedia(media: File): Promise<string> {
if (this.config.media.conversion.convert_images) {
media = await this.convertMedia(media, this.config);
}
const hash = await super.addMedia(media);
await Bun.write(Bun.file(`${process.cwd()}/uploads/${hash}`), media);
@ -160,18 +228,41 @@ export class LocalBackend extends MediaBackend {
return hash;
}
async getMediaByHash(
hash: string,
extension: string
): Promise<File | null> {
async getMediaByHash(hash: string): Promise<File | null> {
const file = Bun.file(`${process.cwd()}/uploads/${hash}`);
if (!(await file.exists())) {
return null;
}
return new File([await file.arrayBuffer()], `${hash}.${extension}`, {
return new File([await file.arrayBuffer()], `${hash}`, {
type: file.type,
});
}
}
export const uploadFile = (file: File, config: ConfigType) => {
const backend = config.media.backend;
if (backend === "local") {
return new LocalBackend(config).addMedia(file);
} else if (backend === "s3") {
return new S3Backend(config).addMedia(file);
}
};
export const getFile = (
hash: string,
extension: string,
config: ConfigType
) => {
const backend = config.media.backend;
if (backend === "local") {
return new LocalBackend(config).getMediaByHash(hash);
} else if (backend === "s3") {
return new S3Backend(config).getMediaByHash(hash);
}
return null;
};