fix: better error handling for uploading

This commit is contained in:
diced
2026-04-09 14:51:43 -07:00
parent 1a1bc46667
commit d6b0ba3b16
4 changed files with 46 additions and 21 deletions

View File

@@ -59,6 +59,8 @@ export const API_ERRORS = {
1058: 'From date must be before to date',
1059: 'From date must be in the past',
1060: 'Passkey has legacy registration data and cannot be used',
1061: 'Invalid multipart/form-data request',
1062: 'No files in multipart/form-data request',
// 2xxx, session errors
2000: 'Invalid login session',

View File

@@ -7,6 +7,9 @@ import { Config } from '../config/validate';
import { sanitizeFilename } from '../fs';
import { formatFileName } from '../uploader/formatFileName';
import { guess } from '../mimes';
import { log } from '../logger';
const logger = log('upload');
const commonDoubleExts = [
'.tar.gz',
@@ -79,25 +82,34 @@ export async function getFilename(
extension: string,
override?: string,
): Promise<{ error: string } | { fileName: string }> {
let fileName = override ? sanitizeFilename(override) : formatFileName(format, originalName);
if (!fileName) return { error: 'invalid file name' };
try {
let fileName = override ? sanitizeFilename(override) : formatFileName(format, originalName);
let fullFileName = `${fileName}${extension}`;
let existing = await prisma.file.findFirst({ where: { name: fullFileName } });
if (existing && (override || format === 'name')) {
return { error: 'file with the same name already exists' };
}
while (existing && format === 'random') {
fileName = formatFileName(format, originalName);
if (!fileName) return { error: 'invalid file name' };
fullFileName = `${fileName}${extension}`;
existing = await prisma.file.findFirst({ where: { name: fullFileName } });
}
let fullFileName = `${fileName}${extension}`;
let existing = await prisma.file.findFirst({ where: { name: fullFileName } });
return { fileName };
if (existing && (override || format === 'name')) {
return { error: 'file with the same name already exists' };
}
while (existing && format === 'random') {
fileName = formatFileName(format, originalName);
if (!fileName) return { error: 'invalid file name' };
fullFileName = `${fileName}${extension}`;
existing = await prisma.file.findFirst({ where: { name: fullFileName } });
}
return { fileName };
} catch (e) {
logger.warn(`error generating file name: ${e}`);
return {
error: e instanceof URIError ? 'invalid file name: make sure it is URL encoded' : 'invalid file name',
};
}
}
export async function getMimetype(

View File

@@ -1,8 +1,8 @@
import ms from 'ms';
import { Config } from '../config/validate';
import { checkOutput, COMPRESS_TYPES, CompressType } from '../compress';
import { config } from '../config';
import { sanitizeExtension, sanitizeFilename } from '../fs';
import { Config } from '../config/validate';
import { sanitizeExtension } from '../fs';
// from ms@3.0.0-canary.1
type Unit =
@@ -257,10 +257,9 @@ export function parseHeaders(headers: UploadHeaders, fileConfig: Config['files']
const filename = headers['x-zipline-filename'];
if (filename) {
const fn = sanitizeFilename(filename);
if (!fn) return headerError('x-zipline-filename', 'Invalid filename');
// checks aren't needed here as they are sanitized later in getFilename
response.overrides.filename = fn;
response.overrides.filename = filename;
}
const extension = headers['x-zipline-file-extension'];

View File

@@ -16,6 +16,7 @@ import { onUpload } from '@/lib/webhooks';
import { Prisma } from '@/prisma/client';
import { userMiddleware } from '@/server/middleware/user';
import typedPlugin from '@/server/typedPlugin';
import { SavedMultipartFile } from '@fastify/multipart';
import { stat } from 'fs/promises';
import { z } from 'zod';
@@ -99,7 +100,18 @@ export default typedPlugin(
if (!req.user && !folder.allowUploads) throw new ApiError(3002);
}
const files = await req.saveRequestFiles({ tmpdir: config.core.tempDirectory });
let files: SavedMultipartFile[] = [];
try {
files = await req.saveRequestFiles({ tmpdir: config.core.tempDirectory });
} catch (e) {
logger.warn('error parsing multipart/form-data request', {
error: e instanceof Error ? e.message : e,
});
if (e instanceof Error && e.message.startsWith('Multipart:')) throw new ApiError(1061);
}
if (!files.length) throw new ApiError(1062);
const totalFileSize = files.reduce((acc, x) => acc + x.file.bytesRead, 0);
const quotaCheck = await checkQuota(req.user, totalFileSize, files.length);