From f62b8525f2989381ebaa882e6cdcbb86e7c9878a Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Thu, 20 Aug 2026 13:23:59 +0200 Subject: [PATCH] [web] gracefully handle empty relativePath in uppy uploads (#36773) GitOrigin-RevId: 640094f69cf53ba9f33a5bdc3b6d532547431d01 --- .../Uploads/ProjectUploadController.mjs | 6 +- .../acceptance/src/ProjectStructureTests.mjs | 55 +++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/services/web/app/src/Features/Uploads/ProjectUploadController.mjs b/services/web/app/src/Features/Uploads/ProjectUploadController.mjs index 79c83f3383..0402ef97ac 100644 --- a/services/web/app/src/Features/Uploads/ProjectUploadController.mjs +++ b/services/web/app/src/Features/Uploads/ProjectUploadController.mjs @@ -29,7 +29,7 @@ const uploadProjectSchema = z.object({ body: z.strictObject({ name: z.string().nonempty(), type: uploadMetaTypeSchema, - relativePath: zz.filepath().optional(), + relativePath: zz.filepath().or(z.literal('')).optional(), }), file: zz.uploadedFile(), }) @@ -47,7 +47,7 @@ const uploadFileSchema = z.object({ // {success:false, error:'invalid_filename'} response instead of a // generic validation error name: z.string().optional(), - relativePath: zz.filepath().optional(), + relativePath: zz.filepath().or(z.literal('')).optional(), type: uploadMetaTypeSchema, targetFolderId: zz.objectId().optional(), }), @@ -63,7 +63,7 @@ const importDocumentSchema = z.object({ }), body: z.strictObject({ name: z.string().nonempty(), - relativePath: zz.filepath().optional(), + relativePath: zz.filepath().or(z.literal('')).optional(), type: uploadMetaTypeSchema, }), file: zz.uploadedFile(), diff --git a/services/web/test/acceptance/src/ProjectStructureTests.mjs b/services/web/test/acceptance/src/ProjectStructureTests.mjs index fe013fd81c..7d6df0a4d4 100644 --- a/services/web/test/acceptance/src/ProjectStructureTests.mjs +++ b/services/web/test/acceptance/src/ProjectStructureTests.mjs @@ -124,6 +124,29 @@ describe('ProjectStructureChanges', function () { }) }) + describe('uploading a project with relativePath set to the literal "" string', function () { + it('should create the project successfully', async function () { + const zipFilename = 'test_project_with_name.zip' + const zipFile = fs.createReadStream( + Path.resolve(Path.join(import.meta.dirname, '..', 'files', zipFilename)) + ) + + const { response, body } = await owner.doRequest('POST', { + uri: 'project/new/upload', + formData: { + name: zipFilename, + qqfile: zipFile, + relativePath: '', + }, + }) + + expect(response.statusCode).to.be.within(200, 299) + const parsedBody = JSON.parse(body) + expect(parsedBody.success).to.equal(true) + expect(parsedBody.project_id).to.be.a('string') + }) + }) + describe('uploading a project with a name', function () { let exampleProjectId const testProjectName = 'wombat' @@ -199,6 +222,38 @@ describe('ProjectStructureChanges', function () { error: 'invalid_upload_request', }) }) + + it('should create file with relativePath set to literal ""', async function () { + const testTex = 'test.tex' + const stream = fs.createReadStream( + Path.resolve(Path.join(import.meta.dirname, '..', 'files', testTex)) + ) + + const projectId = await owner.createProject('foo', { + template: 'blank', + }) + const projectBefore = await ProjectGetter.promises.getProject(projectId) + const { response, body } = await owner.doRequest('POST', { + uri: `project/${projectId}/upload`, + qs: { + folder_id: projectBefore.rootFolder[0]._id.toString(), + }, + formData: { + name: testTex, + qqfile: stream, + relativePath: '', + }, + }) + + expect(response.statusCode).to.be.within(200, 299) + const parsedBody = JSON.parse(body) + expect(parsedBody.success).to.equal(true) + const project = await ProjectGetter.promises.getProject(projectId) + expect(project.rootFolder[0].docs.map(d => d.name)).to.deep.equal([ + 'main.tex', + testTex, + ]) + }) }) })