From 1681d04a4b1d53c63678fc66bf513b7542ee9028 Mon Sep 17 00:00:00 2001 From: cirroskais Date: Thu, 29 Aug 2024 06:24:27 -0400 Subject: [PATCH] THUMBNAILING --- .env.example | 10 ++++ docker-compose.dev.yml | 14 +++++ .../20240829084742_thumbnails/migration.sql | 12 +++++ .../20240829085742_thumbnails/migration.sql | 10 ++++ .../20240829095034_thumbnail_id/migration.sql | 11 ++++ prisma/schema.prisma | 16 ++++-- src/lib/components/Upload.svelte | 35 ++++++++++++ src/lib/server/database.ts | 3 +- src/lib/server/thumbnail.ts | 48 +++++++++++++++++ src/routes/(app)/uploads/+page.svelte | 20 ++----- src/routes/api/v1/thumbnail/[id]/+server.ts | 44 +++++++++++++++ src/routes/download/[id]/[ticket]/+server.ts | 53 +++++++++++++++++++ thumbor.Dockerfile | 3 ++ 13 files changed, 257 insertions(+), 22 deletions(-) create mode 100644 prisma/migrations/20240829084742_thumbnails/migration.sql create mode 100644 prisma/migrations/20240829085742_thumbnails/migration.sql create mode 100644 prisma/migrations/20240829095034_thumbnail_id/migration.sql create mode 100644 src/lib/components/Upload.svelte create mode 100644 src/lib/server/thumbnail.ts create mode 100644 src/routes/api/v1/thumbnail/[id]/+server.ts create mode 100644 src/routes/download/[id]/[ticket]/+server.ts create mode 100644 thumbor.Dockerfile diff --git a/.env.example b/.env.example index ce81892..a33877b 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,11 @@ +BASE_URL= DATABASE_URL= +BODY_SIZE_LIMIT= + +MINIO_URL= +MINIO_ACCESS_KEY= +MINIO_SECRET_KEY= +MINIO_BUCKET= + +THUMBOR_ENDPOINT= +THUMBOR_SECRET= \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 7048a51..47a66bb 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -7,3 +7,17 @@ services: MARIADB_ROOT_PASSWORD: development ports: - '3306:3306' + thumbor: + build: + dockerfile: thumbor.Dockerfile + restart: always + ports: + - '8888:80' + extra_hosts: + - 'host.docker.internal:host-gateway' + environment: + - 'SECURITY_KEY=development' + - 'ALLOW_UNSAFE_URL=False' + - 'STORAGE=thumbor.storages.no_storage' + - 'ENGINE=thumbor_video_engine.engines.video' + # - 'FILTERS=[thumbor_video_engine.filters.format,thumbor_video_engine.filters.still]' diff --git a/prisma/migrations/20240829084742_thumbnails/migration.sql b/prisma/migrations/20240829084742_thumbnails/migration.sql new file mode 100644 index 0000000..677c751 --- /dev/null +++ b/prisma/migrations/20240829084742_thumbnails/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE `Thumbnail` ( + `id` VARCHAR(191) NOT NULL, + `uploadId` VARCHAR(191) NOT NULL, + `placeholder` VARCHAR(191) NOT NULL, + + UNIQUE INDEX `Thumbnail_uploadId_key`(`uploadId`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `Thumbnail` ADD CONSTRAINT `Thumbnail_uploadId_fkey` FOREIGN KEY (`uploadId`) REFERENCES `Upload`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20240829085742_thumbnails/migration.sql b/prisma/migrations/20240829085742_thumbnails/migration.sql new file mode 100644 index 0000000..7889008 --- /dev/null +++ b/prisma/migrations/20240829085742_thumbnails/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the column `placeholder` on the `Thumbnail` table. All the data in the column will be lost. + - Added the required column `fileName` to the `Thumbnail` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `Thumbnail` DROP COLUMN `placeholder`, + ADD COLUMN `fileName` LONGTEXT NOT NULL; diff --git a/prisma/migrations/20240829095034_thumbnail_id/migration.sql b/prisma/migrations/20240829095034_thumbnail_id/migration.sql new file mode 100644 index 0000000..53d8bc2 --- /dev/null +++ b/prisma/migrations/20240829095034_thumbnail_id/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - The primary key for the `Thumbnail` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `id` on the `Thumbnail` table. The data in that column could be lost. The data in that column will be cast from `VarChar(191)` to `Int`. + +*/ +-- AlterTable +ALTER TABLE `Thumbnail` DROP PRIMARY KEY, + MODIFY `id` INTEGER NOT NULL AUTO_INCREMENT, + ADD PRIMARY KEY (`id`); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7775c27..1d9c495 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -53,11 +53,12 @@ model Upload { uploader User @relation(fields: [uploaderId], references: [id]) uploaderId Int - fileName String @db.LongText - internalName String @db.LongText + fileName String @db.LongText + internalName String @db.LongText size Int - public Boolean @default(true) - uploaded DateTime @default(now()) + public Boolean @default(true) + uploaded DateTime @default(now()) + thumbnail Thumbnail? } model APIKey { @@ -69,6 +70,13 @@ model APIKey { permissions Int } +model Thumbnail { + id Int @id @default(autoincrement()) + upload Upload @relation(fields: [uploadId], references: [id]) + uploadId String @unique + fileName String @db.LongText +} + enum Role { ADMINISTRATOR USER diff --git a/src/lib/components/Upload.svelte b/src/lib/components/Upload.svelte new file mode 100644 index 0000000..dc57959 --- /dev/null +++ b/src/lib/components/Upload.svelte @@ -0,0 +1,35 @@ + + + +
+ {#if image} + Thumbnail { + image = false; + }} + /> + {:else} + + {/if} +
+
+

+ {upload.fileName} +

+

{bytesToHumanReadable(upload.size)}

+
+
diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index f61e24f..d44e8e5 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -124,7 +124,8 @@ export async function getUpload(id: string) { internalName: true, public: true, uploaded: true, - uploader: true + uploader: true, + thumbnail: true } }); } diff --git a/src/lib/server/thumbnail.ts b/src/lib/server/thumbnail.ts new file mode 100644 index 0000000..351e362 --- /dev/null +++ b/src/lib/server/thumbnail.ts @@ -0,0 +1,48 @@ +import type { Upload } from '@prisma/client'; +import { env } from '$env/dynamic/private'; +import { createHmac, randomBytes } from 'node:crypto'; +import prisma from './database'; +import minio, { BUCKET } from './minio'; + +const processing = new Set(); + +export function createSignature(opts: string) { + return createHmac('SHA1', env.THUMBOR_KEY || 'development') + .update(opts) + .digest(); +} + +export async function createThumbnail(upload: Upload) { + if (processing.has(upload.id)) return false; + processing.add(upload.id); + + const ticket = randomBytes(16).toString('hex'); + const ticketSig = createSignature(ticket).toString('hex'); + + const url = `${env.BASE_URL}/download/${upload.id}/${ticket}.${ticketSig}`; + const options = '200x0/filters:format(webp):quality(50)'; + + const SIGNATURE = createSignature(`${options}/${url}`) + .toString('base64') + .replaceAll('+', '-') + .replaceAll('/', '_'); + + const response = await fetch(`${env.THUMBOR_ENDPOINT}/${SIGNATURE}/${options}/${url}`); + const arrayBuffer = await response.arrayBuffer(); + + const filePath = `thumbnails/${Date.now()}-${upload.id}.webp`; + + const record = await prisma.thumbnail.create({ + data: { + uploadId: upload.id, + fileName: filePath + } + }); + + await minio.putObject(BUCKET, filePath, Buffer.from(arrayBuffer), arrayBuffer.byteLength, { + 'Content-Type': 'image/webp' + }); + + processing.delete(upload.id); + return record; +} diff --git a/src/routes/(app)/uploads/+page.svelte b/src/routes/(app)/uploads/+page.svelte index dab13c8..c441137 100644 --- a/src/routes/(app)/uploads/+page.svelte +++ b/src/routes/(app)/uploads/+page.svelte @@ -1,11 +1,11 @@