mirror of
https://github.com/diced/zipline.git
synced 2026-06-12 10:51:17 -07:00
feat: mimetype overhauls
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" ADD COLUMN "filesDisabledTypes" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
ADD COLUMN "filesDisabledTypesDefault" TEXT;
|
||||
@@ -36,6 +36,8 @@ model Zipline {
|
||||
filesRoute String @default("/u")
|
||||
filesLength Int @default(6)
|
||||
filesDefaultFormat String @default("random")
|
||||
filesDisabledTypes String[] @default([])
|
||||
filesDisabledTypesDefault String?
|
||||
filesDisabledExtensions String[]
|
||||
filesMaxFileSize String @default("100mb")
|
||||
filesDefaultExpiration String?
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button, LoadingOverlay, NumberInput, Select, Stack, Switch, TextInput }
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import { checkCommaArray, settingsOnSubmit } from '../settingsOnSubmit';
|
||||
import useServerSettings from '../useServerSettings';
|
||||
|
||||
export default function Files() {
|
||||
@@ -25,6 +25,8 @@ function Form({ data, isLoading }: { data: Response['/api/server/settings']; isL
|
||||
filesRoute: data.settings.filesRoute,
|
||||
filesLength: data.settings.filesLength,
|
||||
filesDefaultFormat: data.settings.filesDefaultFormat,
|
||||
filesDisabledTypes: data.settings.filesDisabledTypes.join(', '),
|
||||
filesDisabledTypesDefault: data.settings.filesDisabledTypesDefault,
|
||||
filesDisabledExtensions: data.settings.filesDisabledExtensions.join(', '),
|
||||
filesMaxFileSize: data.settings.filesMaxFileSize,
|
||||
filesDefaultExpiration: data.settings.filesDefaultExpiration,
|
||||
@@ -55,25 +57,17 @@ function Form({ data, isLoading }: { data: Response['/api/server/settings']; isL
|
||||
values.filesMaxExpiration = values.filesMaxExpiration.trim();
|
||||
}
|
||||
|
||||
if (!values.filesDisabledExtensions) {
|
||||
// @ts-ignore
|
||||
values.filesDisabledExtensions = [];
|
||||
} else if (
|
||||
values.filesDisabledExtensions &&
|
||||
typeof values.filesDisabledExtensions === 'string' &&
|
||||
values.filesDisabledExtensions.trim() === ''
|
||||
) {
|
||||
// @ts-ignore
|
||||
values.filesDisabledExtensions = [];
|
||||
if (values.filesDisabledTypesDefault?.trim() === '' || !values.filesDisabledTypesDefault) {
|
||||
values.filesDisabledTypesDefault = null;
|
||||
} else {
|
||||
if (!Array.isArray(values.filesDisabledExtensions))
|
||||
// @ts-ignore
|
||||
values.filesDisabledExtensions = values.filesDisabledExtensions
|
||||
.split(',')
|
||||
.map((ext) => ext.trim())
|
||||
.filter((ext) => ext !== '');
|
||||
values.filesDisabledTypesDefault = values.filesDisabledTypesDefault.trim();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
values.filesDisabledExtensions = checkCommaArray(values.filesDisabledExtensions);
|
||||
// @ts-ignore
|
||||
values.filesDisabledTypes = checkCommaArray(values.filesDisabledTypes);
|
||||
|
||||
return settingsOnSubmit(navigate, form)(values);
|
||||
};
|
||||
|
||||
@@ -86,6 +80,20 @@ function Form({ data, isLoading }: { data: Response['/api/server/settings']; isL
|
||||
{...form.getInputProps('filesAssumeMimetypes', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Disabled Types'
|
||||
description='Mimetypes to disable, separated by commas. It is recommended to have the Assume Mimetypes setting enabled if you are disabling mimetypes, as this will also block files with the corresponding extensions.'
|
||||
placeholder='text/html, application/javascript'
|
||||
{...form.getInputProps('filesDisabledTypes')}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label='Default MIME for Disabled Types'
|
||||
description='The default MIME type to use for disabled types. Leave blank to completely block disabled types.'
|
||||
placeholder='application/octet-stream'
|
||||
{...form.getInputProps('filesDisabledTypesDefault')}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Remove GPS Metadata'
|
||||
description='Remove GPS metadata from files.'
|
||||
|
||||
@@ -6,6 +6,22 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { NavigateFunction } from 'react-router-dom';
|
||||
|
||||
export function checkCommaArray(value: unknown): string[] {
|
||||
if (!value) return [];
|
||||
|
||||
if (value && typeof value === 'string' && value.trim() === '') return [];
|
||||
|
||||
if (!Array.isArray(value) && typeof value === 'string')
|
||||
return value
|
||||
.split(',')
|
||||
.map((x) => x.trim())
|
||||
.filter((x) => x !== '');
|
||||
|
||||
if (Array.isArray(value)) return value.map((x) => String(x).trim()).filter((x) => x !== '');
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function settingsOnSubmit(navigate: NavigateFunction, form: ReturnType<typeof useForm<any>>) {
|
||||
return async (values: unknown) => {
|
||||
const { data, error } = await fetchApi<Response['/api/server/settings']>(
|
||||
|
||||
@@ -63,6 +63,7 @@ export const API_ERRORS = {
|
||||
1062: 'No files in multipart/form-data request',
|
||||
1063: 'Already linked to this OAuth provider',
|
||||
1064: 'Invalid OAuth state parameter',
|
||||
1065: 'Invalid MIME type',
|
||||
|
||||
// 2xxx, session errors
|
||||
2000: 'Invalid login session',
|
||||
|
||||
@@ -22,6 +22,8 @@ export const DATABASE_TO_PROP = {
|
||||
filesRoute: 'files.route',
|
||||
filesLength: 'files.length',
|
||||
filesDefaultFormat: 'files.defaultFormat',
|
||||
filesDisabledTypes: 'files.disabledTypes',
|
||||
filesDisabledTypesDefault: 'files.disabledTypesDefault',
|
||||
filesDisabledExtensions: 'files.disabledExtensions',
|
||||
filesMaxFileSize: 'files.maxFileSize',
|
||||
filesDefaultExpiration: 'files.defaultExpiration',
|
||||
|
||||
@@ -56,6 +56,8 @@ export const ENVS = [
|
||||
env('files.route', 'FILES_ROUTE', 'string', true),
|
||||
env('files.length', 'FILES_LENGTH', 'number', true),
|
||||
env('files.defaultFormat', 'FILES_DEFAULT_FORMAT', 'string', true),
|
||||
env('files.disabledTypes', 'FILES_DISABLED_TYPES', 'string[]', true),
|
||||
env('files.disabledTypesDefault', 'FILES_DISABLED_TYPES_DEFAULT', 'string', true),
|
||||
env('files.disabledExtensions', 'FILES_DISABLED_EXTENSIONS', 'string[]', true),
|
||||
env('files.maxFileSize', 'FILES_MAX_FILE_SIZE', 'string', true),
|
||||
env('files.defaultExpiration', 'FILES_DEFAULT_EXPIRATION', 'string', true),
|
||||
|
||||
@@ -18,6 +18,8 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export const MIME_REGEX = /^[a-zA-Z0-9!#$&^_\-\+.]+\/[a-zA-Z0-9!#$&^_\-\+.]+$/gi;
|
||||
|
||||
export const MAX_SAFE_TIMEOUT_MS = 2147483647;
|
||||
|
||||
export function validateInterval(value: string): boolean {
|
||||
@@ -131,6 +133,8 @@ export const schema = z.object({
|
||||
route: z.string().startsWith('/').min(1).trim().toLowerCase().default('/u'),
|
||||
length: z.number().default(6),
|
||||
defaultFormat: z.enum(['random', 'date', 'uuid', 'name', 'gfycat', 'random-words']).default('random'),
|
||||
disabledTypes: z.array(z.string().regex(MIME_REGEX, 'Invalid MIME type format')).default([]),
|
||||
disabledTypesDefault: z.string().nullable().default(null),
|
||||
disabledExtensions: z.array(z.string()).default([]),
|
||||
maxFileSize: z.string().default('100mb'),
|
||||
defaultExpiration: z.string().nullable().default(null),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { checkOutput, COMPRESS_TYPES } from '@/lib/compress';
|
||||
import { reloadSettings } from '@/lib/config';
|
||||
import type { readDatabaseSettings } from '@/lib/config/read/db';
|
||||
import { safeConfig } from '@/lib/config/safe';
|
||||
import { MAX_SAFE_TIMEOUT_MS } from '@/lib/config/validate';
|
||||
import { MAX_SAFE_TIMEOUT_MS, MIME_REGEX } from '@/lib/config/validate';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { log } from '@/lib/logger';
|
||||
import { secondlyRatelimit } from '@/lib/ratelimits';
|
||||
@@ -181,6 +181,8 @@ export default typedPlugin(
|
||||
'Provided route is reserved',
|
||||
),
|
||||
filesLength: z.number().min(1).max(64),
|
||||
filesDisabledTypes: z.array(z.string().regex(MIME_REGEX, 'Invalid MIME type')),
|
||||
filesDisabledTypesDefault: z.string().regex(MIME_REGEX, 'Invalid MIME type').nullable(),
|
||||
filesDefaultFormat: z.enum(['random', 'date', 'uuid', 'name', 'gfycat']),
|
||||
filesDisabledExtensions: z
|
||||
.union([
|
||||
@@ -191,7 +193,6 @@ export default typedPlugin(
|
||||
typeof value === 'string' ? value.split(',').map((ext) => ext.trim()) : value,
|
||||
),
|
||||
filesMaxFileSize: zBytes,
|
||||
|
||||
filesDefaultExpiration: zMs.nullable(),
|
||||
filesMaxExpiration: zMs.nullable(),
|
||||
filesAssumeMimetypes: z.boolean(),
|
||||
|
||||
@@ -155,7 +155,8 @@ export default typedPlugin(
|
||||
const { fileName } = nameResult;
|
||||
|
||||
// determine mimetype
|
||||
const { mimetype, assumed } = await getMimetype(file.mimetype, extension);
|
||||
const { assumed, ...mimeRes } = await getMimetype(file.mimetype, extension);
|
||||
let mimetype = mimeRes.mimetype;
|
||||
|
||||
if (config.files.assumeMimetypes) {
|
||||
response.assumedMimetypes![i] = assumed;
|
||||
@@ -170,6 +171,12 @@ export default typedPlugin(
|
||||
}
|
||||
}
|
||||
|
||||
if (config.files.disabledTypes.includes(mimetype.trim().toLowerCase())) {
|
||||
console.log(mimetype, config.files.disabledTypesDefault);
|
||||
if (config.files.disabledTypesDefault) mimetype = config.files.disabledTypesDefault;
|
||||
else throw new ApiError(1065, `file[${i}]: File type ${mimetype} is not allowed`);
|
||||
}
|
||||
|
||||
// compress the image if requested
|
||||
let compressed;
|
||||
if (mimetype.startsWith('image/') && options.imageCompression) {
|
||||
|
||||
Reference in New Issue
Block a user