mirror of
https://github.com/diced/zipline.git
synced 2026-04-28 10:43:06 -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)
|
featuresOauthRegistration Boolean @default(false)
|
||||||
featuresDeleteOnMaxViews Boolean @default(true)
|
featuresDeleteOnMaxViews Boolean @default(true)
|
||||||
|
|
||||||
featuresThumbnailsEnabled Boolean @default(true)
|
featuresThumbnailsEnabled Boolean @default(true)
|
||||||
featuresThumbnailsNumberThreads Int @default(4)
|
featuresThumbnailsNumberThreads Int @default(4)
|
||||||
featuresThumbnailsFormat String @default("jpg")
|
featuresThumbnailsFormat String @default("jpg")
|
||||||
|
featuresThumbnailsInstantaneous Boolean @default(false)
|
||||||
|
|
||||||
featuresMetricsEnabled Boolean @default(true)
|
featuresMetricsEnabled Boolean @default(true)
|
||||||
featuresMetricsAdminOnly Boolean @default(false)
|
featuresMetricsAdminOnly Boolean @default(false)
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { Response } from '@/lib/api/response';
|
|||||||
import {
|
import {
|
||||||
Anchor,
|
Anchor,
|
||||||
Button,
|
Button,
|
||||||
|
Divider,
|
||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
Paper,
|
Paper,
|
||||||
Select,
|
Select,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
|
Stack,
|
||||||
Switch,
|
Switch,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
@@ -35,6 +37,7 @@ export default function Features({
|
|||||||
featuresThumbnailsEnabled: true,
|
featuresThumbnailsEnabled: true,
|
||||||
featuresThumbnailsNumberThreads: 4,
|
featuresThumbnailsNumberThreads: 4,
|
||||||
featuresThumbnailsFormat: 'jpg',
|
featuresThumbnailsFormat: 'jpg',
|
||||||
|
featuresThumbnailsInstantaneous: false,
|
||||||
featuresMetricsEnabled: true,
|
featuresMetricsEnabled: true,
|
||||||
featuresMetricsAdminOnly: false,
|
featuresMetricsAdminOnly: false,
|
||||||
featuresMetricsShowUserSpecific: true,
|
featuresMetricsShowUserSpecific: true,
|
||||||
@@ -61,6 +64,7 @@ export default function Features({
|
|||||||
featuresThumbnailsEnabled: data.settings.featuresThumbnailsEnabled ?? true,
|
featuresThumbnailsEnabled: data.settings.featuresThumbnailsEnabled ?? true,
|
||||||
featuresThumbnailsNumberThreads: data.settings.featuresThumbnailsNumberThreads ?? 4,
|
featuresThumbnailsNumberThreads: data.settings.featuresThumbnailsNumberThreads ?? 4,
|
||||||
featuresThumbnailsFormat: data.settings.featuresThumbnailsFormat ?? 'jpg',
|
featuresThumbnailsFormat: data.settings.featuresThumbnailsFormat ?? 'jpg',
|
||||||
|
featuresThumbnailsInstantaneous: data.settings.featuresThumbnailsInstantaneous ?? false,
|
||||||
featuresMetricsEnabled: data.settings.featuresMetricsEnabled ?? true,
|
featuresMetricsEnabled: data.settings.featuresMetricsEnabled ?? true,
|
||||||
featuresMetricsAdminOnly: data.settings.featuresMetricsAdminOnly ?? false,
|
featuresMetricsAdminOnly: data.settings.featuresMetricsAdminOnly ?? false,
|
||||||
featuresMetricsShowUserSpecific: data.settings.featuresMetricsShowUserSpecific ?? true,
|
featuresMetricsShowUserSpecific: data.settings.featuresMetricsShowUserSpecific ?? true,
|
||||||
@@ -76,7 +80,7 @@ export default function Features({
|
|||||||
<Title order={2}>Features</Title>
|
<Title order={2}>Features</Title>
|
||||||
|
|
||||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
|
<Stack gap='xs' mt='xs'>
|
||||||
<Switch
|
<Switch
|
||||||
label='Image Compression'
|
label='Image Compression'
|
||||||
description='Allows the ability for users to compress images.'
|
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.'
|
description='Shows metrics specific to each user, for all users.'
|
||||||
{...form.getInputProps('featuresMetricsShowUserSpecific', { type: 'checkbox' })}
|
{...form.getInputProps('featuresMetricsShowUserSpecific', { type: 'checkbox' })}
|
||||||
/>
|
/>
|
||||||
<div />
|
|
||||||
<Switch
|
<Divider />
|
||||||
label='Enable Thumbnails'
|
|
||||||
description='Enables thumbnail generation for images. Requires a server restart.'
|
<SimpleGrid cols={{ base: 1, md: 2 }} spacing='lg'>
|
||||||
{...form.getInputProps('featuresThumbnailsEnabled', { type: 'checkbox' })}
|
<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
|
<NumberInput
|
||||||
label='Thumbnails Number Threads'
|
label='Thumbnails Number Threads'
|
||||||
@@ -157,13 +170,14 @@ export default function Features({
|
|||||||
{...form.getInputProps('featuresThumbnailsFormat')}
|
{...form.getInputProps('featuresThumbnailsFormat')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div />
|
<Divider />
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
label='Version Checking'
|
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.'
|
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' })}
|
{...form.getInputProps('featuresVersionChecking', { type: 'checkbox' })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label='Version API URL'
|
label='Version API URL'
|
||||||
description={
|
description={
|
||||||
@@ -182,7 +196,7 @@ export default function Features({
|
|||||||
placeholder='https://zipline-version.diced.sh/'
|
placeholder='https://zipline-version.diced.sh/'
|
||||||
{...form.getInputProps('featuresVersionAPI')}
|
{...form.getInputProps('featuresVersionAPI')}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</Stack>
|
||||||
|
|
||||||
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
<Button type='submit' mt='md' loading={isLoading} leftSection={<IconDeviceFloppy size='1rem' />}>
|
||||||
Save
|
Save
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Response } from '@/lib/api/response';
|
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 { useForm } from '@mantine/form';
|
||||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@@ -49,7 +49,7 @@ export default function Tasks({
|
|||||||
<Title order={2}>Tasks</Title>
|
<Title order={2}>Tasks</Title>
|
||||||
|
|
||||||
<Text c='dimmed' size='sm'>
|
<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>
|
</Text>
|
||||||
|
|
||||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export const DATABASE_TO_PROP = {
|
|||||||
featuresThumbnailsEnabled: 'features.thumbnails.enabled',
|
featuresThumbnailsEnabled: 'features.thumbnails.enabled',
|
||||||
featuresThumbnailsNumberThreads: 'features.thumbnails.num_threads',
|
featuresThumbnailsNumberThreads: 'features.thumbnails.num_threads',
|
||||||
featuresThumbnailsFormat: 'features.thumbnails.format',
|
featuresThumbnailsFormat: 'features.thumbnails.format',
|
||||||
|
featuresThumbnailsInstantaneous: 'features.thumbnails.instantaneous',
|
||||||
|
|
||||||
featuresMetricsEnabled: 'features.metrics.enabled',
|
featuresMetricsEnabled: 'features.metrics.enabled',
|
||||||
featuresMetricsAdminOnly: 'features.metrics.adminOnly',
|
featuresMetricsAdminOnly: 'features.metrics.adminOnly',
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export const ENVS = [
|
|||||||
env('features.thumbnails.enabled', 'FEATURES_THUMBNAILS_ENABLED', 'boolean', true),
|
env('features.thumbnails.enabled', 'FEATURES_THUMBNAILS_ENABLED', 'boolean', true),
|
||||||
env('features.thumbnails.num_threads', 'FEATURES_THUMBNAILS_NUM_THREADS', 'number', true),
|
env('features.thumbnails.num_threads', 'FEATURES_THUMBNAILS_NUM_THREADS', 'number', true),
|
||||||
env('features.thumbnails.format', 'FEATURES_THUMBNAILS_FORMAT', 'string', 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.enabled', 'FEATURES_METRICS_ENABLED', 'boolean', true),
|
||||||
env('features.metrics.adminOnly', 'FEATURES_METRICS_ADMIN_ONLY', '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),
|
enabled: z.boolean().default(true),
|
||||||
num_threads: z.number().default(4),
|
num_threads: z.number().default(4),
|
||||||
format: z.enum(['jpg', 'png', 'webp']).default('jpg'),
|
format: z.enum(['jpg', 'png', 'webp']).default('jpg'),
|
||||||
|
instantaneous: z.boolean().default(false),
|
||||||
}),
|
}),
|
||||||
metrics: z.object({
|
metrics: z.object({
|
||||||
enabled: z.boolean().default(true),
|
enabled: z.boolean().default(true),
|
||||||
|
|||||||
@@ -169,4 +169,8 @@ export class Tasks {
|
|||||||
|
|
||||||
return this.tasks[len - 1] as WorkerTask<Data>;
|
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 '..';
|
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__) {
|
export default function thumbnails(prisma: typeof globalThis.__db__) {
|
||||||
return async function (this: IntervalTask, rerun = false) {
|
return async function (this: IntervalTask, rerun = false) {
|
||||||
const thumbnailWorkers = this.tasks.tasks.filter(
|
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`);
|
this.logger.debug(`found ${thumbnailNeeded.length} files that need thumbnails`);
|
||||||
|
|
||||||
const thumbToWorker: { id: string; worker: number }[] = [];
|
runThumbnailWorkers(
|
||||||
|
thumbnailWorkers,
|
||||||
let workerIndex = 0;
|
thumbnailNeeded.map((x) => x.id),
|
||||||
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],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ const zMs = zStringTrimmed.refine(
|
|||||||
);
|
);
|
||||||
const zBytes = zStringTrimmed.refine((value) => bytes(value) > 0, 'Value must be greater than 0');
|
const zBytes = zStringTrimmed.refine((value) => bytes(value) > 0, 'Value must be greater than 0');
|
||||||
|
|
||||||
const zIntervalMs = zMs.refine(
|
const zIntervalMs = zStringTrimmed
|
||||||
(value) => ms(value as StringValue) <= MAX_SAFE_TIMEOUT_MS,
|
.refine((value) => ms((value ?? '0') as StringValue) >= 0, 'Value must be greater than or equal to 0')
|
||||||
`Value must be less than or equal to ${MAX_SAFE_TIMEOUT_MS}ms`,
|
.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
|
const discordEmbed = z
|
||||||
.union([
|
.union([
|
||||||
@@ -227,6 +229,7 @@ export default typedPlugin(
|
|||||||
'Number of threads must be less than or equal to the number of CPUs: ' + cpus().length,
|
'Number of threads must be less than or equal to the number of CPUs: ' + cpus().length,
|
||||||
),
|
),
|
||||||
featuresThumbnailsFormat: z.enum(['jpg', 'png', 'webp']),
|
featuresThumbnailsFormat: z.enum(['jpg', 'png', 'webp']),
|
||||||
|
featuresThumbnailsInstantaneous: z.boolean(),
|
||||||
|
|
||||||
featuresMetricsEnabled: z.boolean(),
|
featuresMetricsEnabled: z.boolean(),
|
||||||
featuresMetricsAdminOnly: z.boolean(),
|
featuresMetricsAdminOnly: z.boolean(),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { fileSelect } from '@/lib/db/models/file';
|
|||||||
import { sanitizeFilename } from '@/lib/fs';
|
import { sanitizeFilename } from '@/lib/fs';
|
||||||
import { removeGps } from '@/lib/gps';
|
import { removeGps } from '@/lib/gps';
|
||||||
import { log } from '@/lib/logger';
|
import { log } from '@/lib/logger';
|
||||||
|
import { runThumbnailWorkers } from '@/lib/tasks/run/thumbnails';
|
||||||
import { parseHeaders, UploadHeaders } from '@/lib/uploader/parseHeaders';
|
import { parseHeaders, UploadHeaders } from '@/lib/uploader/parseHeaders';
|
||||||
import { onUpload } from '@/lib/webhooks';
|
import { onUpload } from '@/lib/webhooks';
|
||||||
import { Prisma } from '@/prisma/client';
|
import { Prisma } from '@/prisma/client';
|
||||||
@@ -251,6 +252,19 @@ export default typedPlugin(
|
|||||||
.type('text/plain')
|
.type('text/plain')
|
||||||
.send(response.files.map((x) => x.url).join(','));
|
.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);
|
return res.send(response);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user