diff --git a/prisma/migrations/20260520163136_files_disabled_types/migration.sql b/prisma/migrations/20260520163136_files_disabled_types/migration.sql
new file mode 100644
index 00000000..b4d5568b
--- /dev/null
+++ b/prisma/migrations/20260520163136_files_disabled_types/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "public"."Zipline" ADD COLUMN "filesDisabledTypes" TEXT[] DEFAULT ARRAY[]::TEXT[],
+ADD COLUMN "filesDisabledTypesDefault" TEXT;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 198a60da..fd155655 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -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?
diff --git a/src/components/pages/serverSettings/parts/Files.tsx b/src/components/pages/serverSettings/parts/Files.tsx
index c1cbd6ee..3e1e34db 100644
--- a/src/components/pages/serverSettings/parts/Files.tsx
+++ b/src/components/pages/serverSettings/parts/Files.tsx
@@ -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' })}
/>
+
+
+
+
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>) {
return async (values: unknown) => {
const { data, error } = await fetchApi(
diff --git a/src/lib/api/errors.ts b/src/lib/api/errors.ts
index e7445b9c..fd53fe97 100644
--- a/src/lib/api/errors.ts
+++ b/src/lib/api/errors.ts
@@ -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',
diff --git a/src/lib/config/read/db.ts b/src/lib/config/read/db.ts
index 62333fcf..e63e25bd 100644
--- a/src/lib/config/read/db.ts
+++ b/src/lib/config/read/db.ts
@@ -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',
diff --git a/src/lib/config/read/env.ts b/src/lib/config/read/env.ts
index 4bbc362a..041a8531 100644
--- a/src/lib/config/read/env.ts
+++ b/src/lib/config/read/env.ts
@@ -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),
diff --git a/src/lib/config/validate.ts b/src/lib/config/validate.ts
index 1605a1de..544e55a8 100644
--- a/src/lib/config/validate.ts
+++ b/src/lib/config/validate.ts
@@ -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),
diff --git a/src/server/routes/api/server/settings/index.ts b/src/server/routes/api/server/settings/index.ts
index e61c9e45..fa921ab7 100644
--- a/src/server/routes/api/server/settings/index.ts
+++ b/src/server/routes/api/server/settings/index.ts
@@ -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(),
diff --git a/src/server/routes/api/upload/index.ts b/src/server/routes/api/upload/index.ts
index 3dc973c7..6e774729 100644
--- a/src/server/routes/api/upload/index.ts
+++ b/src/server/routes/api/upload/index.ts
@@ -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) {