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")
|
filesRoute String @default("/u")
|
||||||
filesLength Int @default(6)
|
filesLength Int @default(6)
|
||||||
filesDefaultFormat String @default("random")
|
filesDefaultFormat String @default("random")
|
||||||
|
filesDisabledTypes String[] @default([])
|
||||||
|
filesDisabledTypesDefault String?
|
||||||
filesDisabledExtensions String[]
|
filesDisabledExtensions String[]
|
||||||
filesMaxFileSize String @default("100mb")
|
filesMaxFileSize String @default("100mb")
|
||||||
filesDefaultExpiration String?
|
filesDefaultExpiration String?
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Button, LoadingOverlay, NumberInput, Select, Stack, Switch, TextInput }
|
|||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { settingsOnSubmit } from '../settingsOnSubmit';
|
import { checkCommaArray, settingsOnSubmit } from '../settingsOnSubmit';
|
||||||
import useServerSettings from '../useServerSettings';
|
import useServerSettings from '../useServerSettings';
|
||||||
|
|
||||||
export default function Files() {
|
export default function Files() {
|
||||||
@@ -25,6 +25,8 @@ function Form({ data, isLoading }: { data: Response['/api/server/settings']; isL
|
|||||||
filesRoute: data.settings.filesRoute,
|
filesRoute: data.settings.filesRoute,
|
||||||
filesLength: data.settings.filesLength,
|
filesLength: data.settings.filesLength,
|
||||||
filesDefaultFormat: data.settings.filesDefaultFormat,
|
filesDefaultFormat: data.settings.filesDefaultFormat,
|
||||||
|
filesDisabledTypes: data.settings.filesDisabledTypes.join(', '),
|
||||||
|
filesDisabledTypesDefault: data.settings.filesDisabledTypesDefault,
|
||||||
filesDisabledExtensions: data.settings.filesDisabledExtensions.join(', '),
|
filesDisabledExtensions: data.settings.filesDisabledExtensions.join(', '),
|
||||||
filesMaxFileSize: data.settings.filesMaxFileSize,
|
filesMaxFileSize: data.settings.filesMaxFileSize,
|
||||||
filesDefaultExpiration: data.settings.filesDefaultExpiration,
|
filesDefaultExpiration: data.settings.filesDefaultExpiration,
|
||||||
@@ -55,25 +57,17 @@ function Form({ data, isLoading }: { data: Response['/api/server/settings']; isL
|
|||||||
values.filesMaxExpiration = values.filesMaxExpiration.trim();
|
values.filesMaxExpiration = values.filesMaxExpiration.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.filesDisabledExtensions) {
|
if (values.filesDisabledTypesDefault?.trim() === '' || !values.filesDisabledTypesDefault) {
|
||||||
// @ts-ignore
|
values.filesDisabledTypesDefault = null;
|
||||||
values.filesDisabledExtensions = [];
|
|
||||||
} else if (
|
|
||||||
values.filesDisabledExtensions &&
|
|
||||||
typeof values.filesDisabledExtensions === 'string' &&
|
|
||||||
values.filesDisabledExtensions.trim() === ''
|
|
||||||
) {
|
|
||||||
// @ts-ignore
|
|
||||||
values.filesDisabledExtensions = [];
|
|
||||||
} else {
|
} else {
|
||||||
if (!Array.isArray(values.filesDisabledExtensions))
|
values.filesDisabledTypesDefault = values.filesDisabledTypesDefault.trim();
|
||||||
// @ts-ignore
|
|
||||||
values.filesDisabledExtensions = values.filesDisabledExtensions
|
|
||||||
.split(',')
|
|
||||||
.map((ext) => ext.trim())
|
|
||||||
.filter((ext) => ext !== '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
values.filesDisabledExtensions = checkCommaArray(values.filesDisabledExtensions);
|
||||||
|
// @ts-ignore
|
||||||
|
values.filesDisabledTypes = checkCommaArray(values.filesDisabledTypes);
|
||||||
|
|
||||||
return settingsOnSubmit(navigate, form)(values);
|
return settingsOnSubmit(navigate, form)(values);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -86,6 +80,20 @@ function Form({ data, isLoading }: { data: Response['/api/server/settings']; isL
|
|||||||
{...form.getInputProps('filesAssumeMimetypes', { type: 'checkbox' })}
|
{...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
|
<Switch
|
||||||
label='Remove GPS Metadata'
|
label='Remove GPS Metadata'
|
||||||
description='Remove GPS metadata from files.'
|
description='Remove GPS metadata from files.'
|
||||||
|
|||||||
@@ -6,6 +6,22 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
|
|||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { NavigateFunction } from 'react-router-dom';
|
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>>) {
|
export function settingsOnSubmit(navigate: NavigateFunction, form: ReturnType<typeof useForm<any>>) {
|
||||||
return async (values: unknown) => {
|
return async (values: unknown) => {
|
||||||
const { data, error } = await fetchApi<Response['/api/server/settings']>(
|
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',
|
1062: 'No files in multipart/form-data request',
|
||||||
1063: 'Already linked to this OAuth provider',
|
1063: 'Already linked to this OAuth provider',
|
||||||
1064: 'Invalid OAuth state parameter',
|
1064: 'Invalid OAuth state parameter',
|
||||||
|
1065: 'Invalid MIME type',
|
||||||
|
|
||||||
// 2xxx, session errors
|
// 2xxx, session errors
|
||||||
2000: 'Invalid login session',
|
2000: 'Invalid login session',
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export const DATABASE_TO_PROP = {
|
|||||||
filesRoute: 'files.route',
|
filesRoute: 'files.route',
|
||||||
filesLength: 'files.length',
|
filesLength: 'files.length',
|
||||||
filesDefaultFormat: 'files.defaultFormat',
|
filesDefaultFormat: 'files.defaultFormat',
|
||||||
|
filesDisabledTypes: 'files.disabledTypes',
|
||||||
|
filesDisabledTypesDefault: 'files.disabledTypesDefault',
|
||||||
filesDisabledExtensions: 'files.disabledExtensions',
|
filesDisabledExtensions: 'files.disabledExtensions',
|
||||||
filesMaxFileSize: 'files.maxFileSize',
|
filesMaxFileSize: 'files.maxFileSize',
|
||||||
filesDefaultExpiration: 'files.defaultExpiration',
|
filesDefaultExpiration: 'files.defaultExpiration',
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export const ENVS = [
|
|||||||
env('files.route', 'FILES_ROUTE', 'string', true),
|
env('files.route', 'FILES_ROUTE', 'string', true),
|
||||||
env('files.length', 'FILES_LENGTH', 'number', true),
|
env('files.length', 'FILES_LENGTH', 'number', true),
|
||||||
env('files.defaultFormat', 'FILES_DEFAULT_FORMAT', 'string', 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.disabledExtensions', 'FILES_DISABLED_EXTENSIONS', 'string[]', true),
|
||||||
env('files.maxFileSize', 'FILES_MAX_FILE_SIZE', 'string', true),
|
env('files.maxFileSize', 'FILES_MAX_FILE_SIZE', 'string', true),
|
||||||
env('files.defaultExpiration', 'FILES_DEFAULT_EXPIRATION', '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 const MAX_SAFE_TIMEOUT_MS = 2147483647;
|
||||||
|
|
||||||
export function validateInterval(value: string): boolean {
|
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'),
|
route: z.string().startsWith('/').min(1).trim().toLowerCase().default('/u'),
|
||||||
length: z.number().default(6),
|
length: z.number().default(6),
|
||||||
defaultFormat: z.enum(['random', 'date', 'uuid', 'name', 'gfycat', 'random-words']).default('random'),
|
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([]),
|
disabledExtensions: z.array(z.string()).default([]),
|
||||||
maxFileSize: z.string().default('100mb'),
|
maxFileSize: z.string().default('100mb'),
|
||||||
defaultExpiration: z.string().nullable().default(null),
|
defaultExpiration: z.string().nullable().default(null),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { checkOutput, COMPRESS_TYPES } from '@/lib/compress';
|
|||||||
import { reloadSettings } from '@/lib/config';
|
import { reloadSettings } from '@/lib/config';
|
||||||
import type { readDatabaseSettings } from '@/lib/config/read/db';
|
import type { readDatabaseSettings } from '@/lib/config/read/db';
|
||||||
import { safeConfig } from '@/lib/config/safe';
|
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 { prisma } from '@/lib/db';
|
||||||
import { log } from '@/lib/logger';
|
import { log } from '@/lib/logger';
|
||||||
import { secondlyRatelimit } from '@/lib/ratelimits';
|
import { secondlyRatelimit } from '@/lib/ratelimits';
|
||||||
@@ -181,6 +181,8 @@ export default typedPlugin(
|
|||||||
'Provided route is reserved',
|
'Provided route is reserved',
|
||||||
),
|
),
|
||||||
filesLength: z.number().min(1).max(64),
|
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']),
|
filesDefaultFormat: z.enum(['random', 'date', 'uuid', 'name', 'gfycat']),
|
||||||
filesDisabledExtensions: z
|
filesDisabledExtensions: z
|
||||||
.union([
|
.union([
|
||||||
@@ -191,7 +193,6 @@ export default typedPlugin(
|
|||||||
typeof value === 'string' ? value.split(',').map((ext) => ext.trim()) : value,
|
typeof value === 'string' ? value.split(',').map((ext) => ext.trim()) : value,
|
||||||
),
|
),
|
||||||
filesMaxFileSize: zBytes,
|
filesMaxFileSize: zBytes,
|
||||||
|
|
||||||
filesDefaultExpiration: zMs.nullable(),
|
filesDefaultExpiration: zMs.nullable(),
|
||||||
filesMaxExpiration: zMs.nullable(),
|
filesMaxExpiration: zMs.nullable(),
|
||||||
filesAssumeMimetypes: z.boolean(),
|
filesAssumeMimetypes: z.boolean(),
|
||||||
|
|||||||
@@ -155,7 +155,8 @@ export default typedPlugin(
|
|||||||
const { fileName } = nameResult;
|
const { fileName } = nameResult;
|
||||||
|
|
||||||
// determine mimetype
|
// 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) {
|
if (config.files.assumeMimetypes) {
|
||||||
response.assumedMimetypes![i] = assumed;
|
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
|
// compress the image if requested
|
||||||
let compressed;
|
let compressed;
|
||||||
if (mimetype.startsWith('image/') && options.imageCompression) {
|
if (mimetype.startsWith('image/') && options.imageCompression) {
|
||||||
|
|||||||
Reference in New Issue
Block a user