Merge pull request #30530 from overleaf/bg-fix-zip-upload-leak

clean up temporary folder on zip upload failure

GitOrigin-RevId: 83a934f41de7e4c737bb3e1123e6ac6b0cb15429
This commit is contained in:
Brian Gough
2026-01-12 09:06:26 +00:00
committed by Copybot
parent 6b4b733da1
commit b3c2146b6f
2 changed files with 142 additions and 51 deletions
@@ -30,38 +30,44 @@ export default {
async function createProjectFromZipArchive(ownerId, defaultName, zipPath) { async function createProjectFromZipArchive(ownerId, defaultName, zipPath) {
const contentsPath = await _extractZip(zipPath) const contentsPath = await _extractZip(zipPath)
const { path, content } =
await ProjectRootDocManager.promises.findRootDocFileFromDirectory(
contentsPath
)
const projectName =
DocumentHelper.getTitleFromTexContent(content || '') || defaultName
const uniqueName = await _generateUniqueName(ownerId, projectName)
const project = await ProjectCreationHandler.promises.createBlankProject(
ownerId,
uniqueName
)
try { try {
await _initializeProjectWithZipContents(ownerId, project, contentsPath) const { path, content } =
await ProjectRootDocManager.promises.findRootDocFileFromDirectory(
if (path) { contentsPath
await ProjectRootDocManager.promises.setRootDocFromName(project._id, path)
}
} catch (err) {
// no need to wait for the cleanup here
ProjectDeleter.promises
.deleteProject(project._id)
.catch(err =>
logger.error(
{ err, projectId: project._id },
'there was an error cleaning up project after importing a zip failed'
)
) )
throw err
const projectName =
DocumentHelper.getTitleFromTexContent(content || '') || defaultName
const uniqueName = await _generateUniqueName(ownerId, projectName)
const project = await ProjectCreationHandler.promises.createBlankProject(
ownerId,
uniqueName
)
try {
await _initializeProjectWithZipContents(ownerId, project, contentsPath)
if (path) {
await ProjectRootDocManager.promises.setRootDocFromName(
project._id,
path
)
}
} catch (err) {
// no need to wait for the cleanup here
ProjectDeleter.promises
.deleteProject(project._id)
.catch(err =>
logger.error(
{ err, projectId: project._id },
'there was an error cleaning up project after importing a zip failed'
)
)
throw err
}
return project
} finally {
await fs.promises.rm(contentsPath, { recursive: true, force: true })
} }
await fs.promises.rm(contentsPath, { recursive: true, force: true })
return project
} }
async function createProjectFromZipArchiveWithName( async function createProjectFromZipArchiveWithName(
@@ -71,30 +77,33 @@ async function createProjectFromZipArchiveWithName(
attributes = {} attributes = {}
) { ) {
const contentsPath = await _extractZip(zipPath) const contentsPath = await _extractZip(zipPath)
const uniqueName = await _generateUniqueName(ownerId, proposedName)
const project = await ProjectCreationHandler.promises.createBlankProject(
ownerId,
uniqueName,
attributes
)
try { try {
await _initializeProjectWithZipContents(ownerId, project, contentsPath) const uniqueName = await _generateUniqueName(ownerId, proposedName)
await ProjectRootDocManager.promises.setRootDocAutomatically(project._id) const project = await ProjectCreationHandler.promises.createBlankProject(
} catch (err) { ownerId,
// no need to wait for the cleanup here uniqueName,
ProjectDeleter.promises attributes
.deleteProject(project._id) )
.catch(err =>
logger.error( try {
{ err, projectId: project._id }, await _initializeProjectWithZipContents(ownerId, project, contentsPath)
'there was an error cleaning up project after importing a zip failed' await ProjectRootDocManager.promises.setRootDocAutomatically(project._id)
} catch (err) {
// no need to wait for the cleanup here
ProjectDeleter.promises
.deleteProject(project._id)
.catch(err =>
logger.error(
{ err, projectId: project._id },
'there was an error cleaning up project after importing a zip failed'
)
) )
) throw err
throw err }
return project
} finally {
await fs.promises.rm(contentsPath, { recursive: true, force: true })
} }
await fs.promises.rm(contentsPath, { recursive: true, force: true })
return project
} }
async function _extractZip(zipPath) { async function _extractZip(zipPath) {
@@ -102,7 +111,13 @@ async function _extractZip(zipPath) {
Path.dirname(zipPath), Path.dirname(zipPath),
`${Path.basename(zipPath, '.zip')}-${Date.now()}` `${Path.basename(zipPath, '.zip')}-${Date.now()}`
) )
await ArchiveManager.promises.extractZipArchive(zipPath, destination) try {
await ArchiveManager.promises.extractZipArchive(zipPath, destination)
} catch (error) {
logger.debug({ zipPath, error }, 'error extracting from zip archive')
await fs.promises.rm(destination, { recursive: true, force: true })
throw error
}
return destination return destination
} }
@@ -323,6 +323,48 @@ describe('ProjectUploadManager', function () {
.been.called .been.called
}) })
}) })
describe('when extraction fails', function () {
beforeEach(async function (ctx) {
ctx.ArchiveManager.promises.extractZipArchive.rejects(new Error('oops'))
await expect(
ctx.ProjectUploadManager.promises.createProjectFromZipArchive(
ctx.ownerId,
ctx.projectName,
ctx.zipPath
)
).to.be.rejectedWith('oops')
})
it('should remove the destination directory', function (ctx) {
ctx.fs.promises.rm.should.have.been.calledWith(ctx.extractedZipPath, {
recursive: true,
force: true,
})
})
})
describe('when project creation fails', function () {
beforeEach(async function (ctx) {
ctx.ProjectCreationHandler.promises.createBlankProject.rejects(
new Error('oops')
)
await expect(
ctx.ProjectUploadManager.promises.createProjectFromZipArchive(
ctx.ownerId,
ctx.projectName,
ctx.zipPath
)
).to.be.rejectedWith('oops')
})
it('should remove the destination directory', function (ctx) {
ctx.fs.promises.rm.should.have.been.calledWith(ctx.extractedZipPath, {
recursive: true,
force: true,
})
})
})
}) })
describe('createProjectFromZipArchiveWithName', function () { describe('createProjectFromZipArchiveWithName', function () {
@@ -406,6 +448,13 @@ describe('ProjectUploadManager', function () {
ctx.project._id ctx.project._id
) )
}) })
it('should remove the destination directory', function (ctx) {
ctx.fs.promises.rm.should.have.been.calledWith(ctx.extractedZipPath, {
recursive: true,
force: true,
})
})
}) })
describe('when setting automatically the root doc fails', function () { describe('when setting automatically the root doc fails', function () {
@@ -425,6 +474,33 @@ describe('ProjectUploadManager', function () {
ctx.project._id ctx.project._id
) )
}) })
it('should remove the destination directory', function (ctx) {
ctx.fs.promises.rm.should.have.been.calledWith(ctx.extractedZipPath, {
recursive: true,
force: true,
})
})
})
describe('when extraction fails', function () {
beforeEach(async function (ctx) {
ctx.ArchiveManager.promises.extractZipArchive.rejects(new Error('oops'))
await expect(
ctx.ProjectUploadManager.promises.createProjectFromZipArchiveWithName(
ctx.ownerId,
ctx.projectName,
ctx.zipPath
)
).to.be.rejectedWith('oops')
})
it('should remove the destination directory', function (ctx) {
ctx.fs.promises.rm.should.have.been.calledWith(ctx.extractedZipPath, {
recursive: true,
force: true,
})
})
}) })
}) })
}) })