mirror of
https://github.com/diced/zipline.git
synced 2026-04-28 02:33:07 -07:00
feat: instantaneous thumb generation
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" ADD COLUMN "featuresThumbnailsInstantaneous" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -58,9 +58,10 @@ model Zipline {
|
||||
featuresOauthRegistration Boolean @default(false)
|
||||
featuresDeleteOnMaxViews Boolean @default(true)
|
||||
|
||||
featuresThumbnailsEnabled Boolean @default(true)
|
||||
featuresThumbnailsNumberThreads Int @default(4)
|
||||
featuresThumbnailsFormat String @default("jpg")
|
||||
featuresThumbnailsEnabled Boolean @default(true)
|
||||
featuresThumbnailsNumberThreads Int @default(4)
|
||||
featuresThumbnailsFormat String @default("jpg")
|
||||
featuresThumbnailsInstantaneous Boolean @default(false)
|
||||
|
||||
featuresMetricsEnabled Boolean @default(true)
|
||||
featuresMetricsAdminOnly Boolean @default(false)
|
||||
|
||||
@@ -2,11 +2,13 @@ import { Response } from '@/lib/api/response';
|
||||
import {
|
||||
Anchor,
|
||||
Button,
|
||||
Divider,
|
||||
LoadingOverlay,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Switch,
|
||||
TextInput,
|
||||
Title,
|
||||
@@ -35,6 +37,7 @@ export default function Features({
|
||||
featuresThumbnailsEnabled: true,
|
||||
featuresThumbnailsNumberThreads: 4,
|
||||
featuresThumbnailsFormat: 'jpg',
|
||||
featuresThumbnailsInstantaneous: false,
|
||||
featuresMetricsEnabled: true,
|
||||
featuresMetricsAdminOnly: false,
|
||||
featuresMetricsShowUserSpecific: true,
|
||||
@@ -61,6 +64,7 @@ export default function Features({
|
||||
featuresThumbnailsEnabled: data.settings.featuresThumbnailsEnabled ?? true,
|
||||
featuresThumbnailsNumberThreads: data.settings.featuresThumbnailsNumberThreads ?? 4,
|
||||
featuresThumbnailsFormat: data.settings.featuresThumbnailsFormat ?? 'jpg',
|
||||
featuresThumbnailsInstantaneous: data.settings.featuresThumbnailsInstantaneous ?? false,
|
||||
featuresMetricsEnabled: data.settings.featuresMetricsEnabled ?? true,
|
||||
featuresMetricsAdminOnly: data.settings.featuresMetricsAdminOnly ?? false,
|
||||
featuresMetricsShowUserSpecific: data.settings.featuresMetricsShowUserSpecific ?? true,
|
||||
@@ -76,7 +80,7 @@ export default function Features({
|
||||
<Title order={2}>Features</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<Stack gap='xs' mt='xs'>
|
||||
<Switch
|
||||
label='Image Compression'
|
||||
description='Allows the ability for users to compress images.'
|
||||
@@ -130,12 +134,21 @@ export default function Features({
|
||||
description='Shows metrics specific to each user, for all users.'
|
||||
{...form.getInputProps('featuresMetricsShowUserSpecific', { type: 'checkbox' })}
|
||||
/>
|
||||
<div />
|
||||
<Switch
|
||||
label='Enable Thumbnails'
|
||||
description='Enables thumbnail generation for images. Requires a server restart.'
|
||||
{...form.getInputProps('featuresThumbnailsEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||
<Switch
|
||||
label='Enable Thumbnails'
|
||||
description='Enables thumbnail generation for images. Requires a server restart.'
|
||||
{...form.getInputProps('featuresThumbnailsEnabled', { type: 'checkbox' })}
|
||||
/>
|
||||
<Switch
|
||||
label='Instantaneous Thumbnails'
|
||||
description='Generates thumbnails immediately after a file is uploaded, instead of waiting for the task to run.'
|
||||
{...form.getInputProps('featuresThumbnailsInstantaneous', { type: 'checkbox' })}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
<NumberInput
|
||||
label='Thumbnails Number Threads'
|
||||
@@ -157,13 +170,14 @@ export default function Features({
|
||||
{...form.getInputProps('featuresThumbnailsFormat')}
|
||||
/>
|
||||
|
||||
<div />
|
||||
<Divider />
|
||||
|
||||
<Switch
|
||||
label='Version Checking'
|
||||
description='Enable version checking for the server. This will check for updates and display the status on the sidebar to all users.'
|
||||
{...form.getInputProps('featuresVersionChecking', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Version API URL'
|
||||
description={
|
||||
@@ -182,7 +196,7 @@ export default function Features({
|
||||
placeholder='https://zipline-version.diced.sh/'
|
||||
{...form.getInputProps('featuresVersionAPI')}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||
Save
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { Button, LoadingOverlay, Paper, SimpleGrid, Text, TextInput, Title } from '@mantine/core';
|
||||
import { Button, Code, LoadingOverlay, Paper, SimpleGrid, Text, TextInput, Title } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
@@ -49,7 +49,7 @@ export default function Tasks({
|
||||
<Title order={2}>Tasks</Title>
|
||||
|
||||
<Text c='dimmed' size='sm'>
|
||||
All options require a restart to take effect.
|
||||
All options require a restart to take effect. Setting a value of <Code>0</Code> will disable the task.
|
||||
</Text>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
|
||||
@@ -47,6 +47,7 @@ export const DATABASE_TO_PROP = {
|
||||
featuresThumbnailsEnabled: 'features.thumbnails.enabled',
|
||||
featuresThumbnailsNumberThreads: 'features.thumbnails.num_threads',
|
||||
featuresThumbnailsFormat: 'features.thumbnails.format',
|
||||
featuresThumbnailsInstantaneous: 'features.thumbnails.instantaneous',
|
||||
|
||||
featuresMetricsEnabled: 'features.metrics.enabled',
|
||||
featuresMetricsAdminOnly: 'features.metrics.adminOnly',
|
||||
|
||||
@@ -80,6 +80,7 @@ export const ENVS = [
|
||||
env('features.thumbnails.enabled', 'FEATURES_THUMBNAILS_ENABLED', 'boolean', true),
|
||||
env('features.thumbnails.num_threads', 'FEATURES_THUMBNAILS_NUM_THREADS', 'number', true),
|
||||
env('features.thumbnails.format', 'FEATURES_THUMBNAILS_FORMAT', 'string', true),
|
||||
env('features.thumbnails.instantaneous', 'FEATURES_THUMBNAILS_INSTANTANEOUS', 'boolean', true),
|
||||
|
||||
env('features.metrics.enabled', 'FEATURES_METRICS_ENABLED', 'boolean', true),
|
||||
env('features.metrics.adminOnly', 'FEATURES_METRICS_ADMIN_ONLY', 'boolean', true),
|
||||
|
||||
@@ -204,6 +204,7 @@ export const schema = z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
num_threads: z.number().default(4),
|
||||
format: z.enum(['jpg', 'png', 'webp']).default('jpg'),
|
||||
instantaneous: z.boolean().default(false),
|
||||
}),
|
||||
metrics: z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
|
||||
@@ -169,4 +169,8 @@ export class Tasks {
|
||||
|
||||
return this.tasks[len - 1] as WorkerTask<Data>;
|
||||
}
|
||||
|
||||
public workersBy(starting: string): WorkerTask[] {
|
||||
return this.tasks.filter((x) => 'worker' in x && x.id.startsWith(starting)) as WorkerTask[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
import { IntervalTask, WorkerTask } from '..';
|
||||
|
||||
export function runThumbnailWorkers(workers: WorkerTask[], files: string[]) {
|
||||
const thumbToWorker: { id: string; worker: number }[] = [];
|
||||
|
||||
let workerIndex = 0;
|
||||
for (const file of files) {
|
||||
thumbToWorker.push({
|
||||
id: file,
|
||||
worker: workerIndex,
|
||||
});
|
||||
|
||||
workerIndex = (workerIndex + 1) % workers.length;
|
||||
}
|
||||
|
||||
const ids = workers.map((_, i) => thumbToWorker.filter((x) => x.worker === i).map((x) => x.id));
|
||||
|
||||
for (let i = 0; i !== workers.length; ++i) {
|
||||
if (!ids[i].length) continue;
|
||||
|
||||
workers[i].worker!.postMessage({
|
||||
type: 0,
|
||||
data: ids[i],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function thumbnails(prisma: typeof globalThis.__db__) {
|
||||
return async function (this: IntervalTask, rerun = false) {
|
||||
const thumbnailWorkers = this.tasks.tasks.filter(
|
||||
@@ -23,27 +48,9 @@ export default function thumbnails(prisma: typeof globalThis.__db__) {
|
||||
|
||||
this.logger.debug(`found ${thumbnailNeeded.length} files that need thumbnails`);
|
||||
|
||||
const thumbToWorker: { id: string; worker: number }[] = [];
|
||||
|
||||
let workerIndex = 0;
|
||||
for (const file of thumbnailNeeded) {
|
||||
thumbToWorker.push({
|
||||
id: file.id,
|
||||
worker: workerIndex,
|
||||
});
|
||||
|
||||
workerIndex = (workerIndex + 1) % thumbnailWorkers.length;
|
||||
}
|
||||
|
||||
const ids = thumbnailWorkers.map((_, i) => thumbToWorker.filter((x) => x.worker === i).map((x) => x.id));
|
||||
|
||||
for (let i = 0; i !== thumbnailWorkers.length; ++i) {
|
||||
if (!ids[i].length) continue;
|
||||
|
||||
thumbnailWorkers[i].worker!.postMessage({
|
||||
type: 0,
|
||||
data: ids[i],
|
||||
});
|
||||
}
|
||||
runThumbnailWorkers(
|
||||
thumbnailWorkers,
|
||||
thumbnailNeeded.map((x) => x.id),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,10 +55,12 @@ const zMs = zStringTrimmed.refine(
|
||||
);
|
||||
const zBytes = zStringTrimmed.refine((value) => bytes(value) > 0, 'Value must be greater than 0');
|
||||
|
||||
const zIntervalMs = zMs.refine(
|
||||
(value) => ms(value as StringValue) <= MAX_SAFE_TIMEOUT_MS,
|
||||
`Value must be less than or equal to ${MAX_SAFE_TIMEOUT_MS}ms`,
|
||||
);
|
||||
const zIntervalMs = zStringTrimmed
|
||||
.refine((value) => ms((value ?? '0') as StringValue) >= 0, 'Value must be greater than or equal to 0')
|
||||
.refine(
|
||||
(value) => ms(value as StringValue) <= MAX_SAFE_TIMEOUT_MS,
|
||||
`Value must be less than or equal to ${MAX_SAFE_TIMEOUT_MS}ms`,
|
||||
);
|
||||
|
||||
const discordEmbed = z
|
||||
.union([
|
||||
@@ -227,6 +229,7 @@ export default typedPlugin(
|
||||
'Number of threads must be less than or equal to the number of CPUs: ' + cpus().length,
|
||||
),
|
||||
featuresThumbnailsFormat: z.enum(['jpg', 'png', 'webp']),
|
||||
featuresThumbnailsInstantaneous: z.boolean(),
|
||||
|
||||
featuresMetricsEnabled: z.boolean(),
|
||||
featuresMetricsAdminOnly: z.boolean(),
|
||||
|
||||
@@ -10,6 +10,7 @@ import { fileSelect } from '@/lib/db/models/file';
|
||||
import { sanitizeFilename } from '@/lib/fs';
|
||||
import { removeGps } from '@/lib/gps';
|
||||
import { log } from '@/lib/logger';
|
||||
import { runThumbnailWorkers } from '@/lib/tasks/run/thumbnails';
|
||||
import { parseHeaders, UploadHeaders } from '@/lib/uploader/parseHeaders';
|
||||
import { onUpload } from '@/lib/webhooks';
|
||||
import { Prisma } from '@/prisma/client';
|
||||
@@ -251,6 +252,19 @@ export default typedPlugin(
|
||||
.type('text/plain')
|
||||
.send(response.files.map((x) => x.url).join(','));
|
||||
|
||||
if (config.features.thumbnails.instantaneous) {
|
||||
logger.debug('running thumbnail workers immediately due to configuration', {
|
||||
files: response.files.length,
|
||||
});
|
||||
|
||||
const fileIds = response.files.map((x) => x.id);
|
||||
|
||||
const thumbnailWorkers = server.tasks.workersBy('thumbnail');
|
||||
if (!thumbnailWorkers.length) return;
|
||||
|
||||
runThumbnailWorkers(thumbnailWorkers, fileIds);
|
||||
}
|
||||
|
||||
return res.send(response);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user