forked from atums.world/backend
add thumbnails for images and videos, fix delete route not seing query, fix not using reservation in user info route and add file and folder count
This commit is contained in:
parent
f917849f4e
commit
6a55f9f5a9
11 changed files with 296 additions and 19 deletions
|
@ -146,6 +146,6 @@ export function supportsExif(mimeType: string, extension: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
export function supportsThumbnails(mimeType: string): boolean {
|
||||
export function supportsThumbnail(mimeType: string): boolean {
|
||||
return /^(image\/(?!svg+xml)|video\/)/i.test(mimeType);
|
||||
}
|
||||
|
|
|
@ -130,17 +130,17 @@ class Logger {
|
|||
}
|
||||
|
||||
public error(
|
||||
message: string | Error | (string | Error)[],
|
||||
message: string | Error | ErrorEvent | (string | Error)[],
|
||||
breakLine: boolean = false,
|
||||
): void {
|
||||
const stack: string = new Error().stack || "";
|
||||
const { filename, timestamp } = this.getCallerInfo(stack);
|
||||
|
||||
const messages: (string | Error)[] = Array.isArray(message)
|
||||
const messages: (string | Error | ErrorEvent)[] = Array.isArray(message)
|
||||
? message
|
||||
: [message];
|
||||
const joinedMessage: string = messages
|
||||
.map((msg: string | Error): string =>
|
||||
.map((msg: string | Error | ErrorEvent): string =>
|
||||
typeof msg === "string" ? msg : msg.message,
|
||||
)
|
||||
.join(" ");
|
||||
|
|
208
src/helpers/workers/thumbnails.ts
Normal file
208
src/helpers/workers/thumbnails.ts
Normal file
|
@ -0,0 +1,208 @@
|
|||
import { dataType } from "@config/environment.ts";
|
||||
import { logger } from "@helpers/logger.ts";
|
||||
import { type BunFile, s3, sql } from "bun";
|
||||
import ffmpeg from "fluent-ffmpeg";
|
||||
import imageThumbnail from "image-thumbnail";
|
||||
import { join, resolve } from "path";
|
||||
|
||||
declare var self: Worker;
|
||||
|
||||
async function generateVideoThumbnail(
|
||||
filePath: string,
|
||||
thumbnailPath: string,
|
||||
): Promise<ArrayBuffer | null> {
|
||||
return new Promise(
|
||||
(
|
||||
resolve: (value: ArrayBuffer) => void,
|
||||
reject: (reason: Error) => void,
|
||||
): void => {
|
||||
ffmpeg(filePath)
|
||||
.videoFilters("thumbnail")
|
||||
.frames(1)
|
||||
.format("mjpeg")
|
||||
.output(thumbnailPath)
|
||||
.on("error", (error: Error) => {
|
||||
logger.error([
|
||||
"failed to generate thumbnail",
|
||||
error as Error,
|
||||
]);
|
||||
reject(error);
|
||||
})
|
||||
|
||||
.on("end", async () => {
|
||||
const thumbnailFile: BunFile = Bun.file(thumbnailPath);
|
||||
const file: BunFile = Bun.file(filePath);
|
||||
const thumbnailArraybuffer: ArrayBuffer =
|
||||
await thumbnailFile.arrayBuffer();
|
||||
|
||||
await thumbnailFile.unlink();
|
||||
await file.unlink();
|
||||
|
||||
resolve(thumbnailArraybuffer);
|
||||
})
|
||||
.run();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function generateImageThumbnail(
|
||||
filePath: string,
|
||||
thumbnailPath: string,
|
||||
): Promise<ArrayBuffer> {
|
||||
return new Promise(
|
||||
async (
|
||||
resolve: (value: ArrayBuffer) => void,
|
||||
reject: (reason: Error) => void,
|
||||
) => {
|
||||
try {
|
||||
const options: {
|
||||
responseType: "buffer";
|
||||
height: number;
|
||||
jpegOptions: {
|
||||
force: boolean;
|
||||
quality: number;
|
||||
};
|
||||
} = {
|
||||
height: 320,
|
||||
responseType: "buffer",
|
||||
jpegOptions: {
|
||||
force: true,
|
||||
quality: 60,
|
||||
},
|
||||
};
|
||||
|
||||
const thumbnailBuffer: Buffer = await imageThumbnail(
|
||||
filePath,
|
||||
options,
|
||||
);
|
||||
|
||||
await Bun.write(thumbnailPath, thumbnailBuffer.buffer);
|
||||
resolve(await Bun.file(thumbnailPath).arrayBuffer());
|
||||
|
||||
await Bun.file(filePath).unlink();
|
||||
await Bun.file(thumbnailPath).unlink();
|
||||
} catch (error) {
|
||||
reject(error as Error);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function createThumbnails(files: FileEntry[]): Promise<void> {
|
||||
const { type, path } = dataType;
|
||||
|
||||
for (const file of files) {
|
||||
const { id, mime_type } = file;
|
||||
|
||||
let fileArrayBuffer: ArrayBuffer | null = null;
|
||||
const isVideo: boolean = mime_type.startsWith("video/");
|
||||
const fileName: string = `${id}.${file.extension || ""}`;
|
||||
|
||||
if (type === "local") {
|
||||
const filePath: string | undefined = join(path as string, fileName);
|
||||
|
||||
try {
|
||||
fileArrayBuffer = await Bun.file(filePath).arrayBuffer();
|
||||
} catch {
|
||||
logger.error([
|
||||
"Could not generate thumbnail for file:",
|
||||
fileName,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
fileArrayBuffer = await s3.file(fileName).arrayBuffer();
|
||||
} catch {
|
||||
logger.error([
|
||||
"Could not generate thumbnail for file:",
|
||||
fileName,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileArrayBuffer) {
|
||||
logger.error(["Could not generate thumbnail for file:", fileName]);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tempFilePath: string = resolve("temp", `${id}.tmp`);
|
||||
const tempThumbnailPath: string = resolve("temp", `${id}.jpg`);
|
||||
|
||||
try {
|
||||
await Bun.write(tempFilePath, fileArrayBuffer, {
|
||||
createPath: true,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error([
|
||||
"Could not write file to temp path:",
|
||||
fileName,
|
||||
error as Error,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const thumbnailArrayBuffer: ArrayBuffer | null = isVideo
|
||||
? await generateVideoThumbnail(tempFilePath, tempThumbnailPath)
|
||||
: await generateImageThumbnail(tempFilePath, tempThumbnailPath);
|
||||
|
||||
if (!thumbnailArrayBuffer) {
|
||||
logger.error([
|
||||
"Could not generate thumbnail for file:",
|
||||
fileName,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (type === "local") {
|
||||
const thumbnailPath: string = join(
|
||||
path as string,
|
||||
"thumbnails",
|
||||
`${id}.jpg`,
|
||||
);
|
||||
|
||||
await Bun.write(thumbnailPath, thumbnailArrayBuffer, {
|
||||
createPath: true,
|
||||
});
|
||||
} else {
|
||||
const thumbnailPath: string = `thumbnails/${id}.jpg`;
|
||||
|
||||
await s3.file(thumbnailPath).write(thumbnailArrayBuffer);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error([
|
||||
"Could not write thumbnail to storage:",
|
||||
fileName,
|
||||
error as Error,
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
await sql`UPDATE files SET thumbnail = true WHERE id = ${id}`;
|
||||
} catch (error) {
|
||||
logger.error([
|
||||
"Could not update file with thumbnail status:",
|
||||
fileName,
|
||||
error as Error,
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error([
|
||||
"An error occurred while generating thumbnail for file:",
|
||||
fileName,
|
||||
error as Error,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.onmessage = async (event: MessageEvent): Promise<void> => {
|
||||
await createThumbnails(event.data.files);
|
||||
};
|
||||
|
||||
self.onerror = (error: ErrorEvent): void => {
|
||||
logger.error(error);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue