mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
ee85d948e2
GitOrigin-RevId: ef2ef77e26df59d1af3df6dc664e284d3c70102d
134 lines
3.7 KiB
JavaScript
134 lines
3.7 KiB
JavaScript
'use strict'
|
|
|
|
const BPromise = require('bluebird')
|
|
const config = require('config')
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
const OError = require('@overleaf/o-error')
|
|
const objectPersistor = require('@overleaf/object-persistor')
|
|
|
|
const assert = require('./assert')
|
|
const { BlobStore } = require('./blob_store')
|
|
const persistor = require('./persistor')
|
|
const ProjectArchive = require('./project_archive')
|
|
const projectKey = require('./project_key')
|
|
const temp = require('./temp')
|
|
|
|
const BUCKET = config.get('zipStore.bucket')
|
|
|
|
function getZipKey(projectId, version) {
|
|
return path.join(
|
|
projectKey.format(projectId),
|
|
version.toString(),
|
|
'project.zip'
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Store a zip of a given version of a project in bucket.
|
|
*
|
|
* @class
|
|
*/
|
|
class ZipStore {
|
|
/**
|
|
* Generate signed link to access the zip file.
|
|
*
|
|
* @param {number} projectId
|
|
* @param {number} version
|
|
* @return {string}
|
|
*/
|
|
async getSignedUrl(projectId, version) {
|
|
assert.projectId(projectId, 'bad projectId')
|
|
assert.integer(version, 'bad version')
|
|
|
|
const key = getZipKey(projectId, version)
|
|
return await persistor.getRedirectUrl(BUCKET, key)
|
|
}
|
|
|
|
/**
|
|
* Generate a zip of the given snapshot.
|
|
*
|
|
* @param {number} projectId
|
|
* @param {number} version
|
|
* @param {Snapshot} snapshot
|
|
*/
|
|
async storeZip(projectId, version, snapshot) {
|
|
assert.projectId(projectId, 'bad projectId')
|
|
assert.integer(version, 'bad version')
|
|
assert.object(snapshot, 'bad snapshot')
|
|
|
|
const zipKey = getZipKey(projectId, version)
|
|
|
|
if (await isZipPresent()) return
|
|
|
|
await BPromise.using(temp.open('zip'), async tempFileInfo => {
|
|
await zipSnapshot(tempFileInfo.path, snapshot)
|
|
await uploadZip(tempFileInfo.path)
|
|
})
|
|
|
|
// If the file is already there, we don't need to build the zip again. If we
|
|
// just HEAD the file, there's a race condition, because the zip files
|
|
// automatically expire. So, we try to copy the file from itself to itself,
|
|
// and if it fails, we know the file didn't exist. If it succeeds, this has
|
|
// the effect of re-extending its lifetime.
|
|
async function isZipPresent() {
|
|
try {
|
|
await persistor.copyObject(BUCKET, zipKey, zipKey)
|
|
return true
|
|
} catch (error) {
|
|
if (!(error instanceof objectPersistor.Errors.NotFoundError)) {
|
|
console.error(
|
|
'storeZip: isZipPresent: unexpected error (except in dev): %s',
|
|
error
|
|
)
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function zipSnapshot(tempPathname, snapshot) {
|
|
const blobStore = new BlobStore(projectId)
|
|
const zipTimeoutMs = parseInt(config.get('zipStore.zipTimeoutMs'), 10)
|
|
const archive = new ProjectArchive(snapshot, zipTimeoutMs)
|
|
try {
|
|
await archive.writeZip(blobStore, tempPathname)
|
|
} catch (err) {
|
|
throw new ZipStore.CreationError(projectId, version).withCause(err)
|
|
}
|
|
}
|
|
|
|
async function uploadZip(tempPathname, snapshot) {
|
|
const stream = fs.createReadStream(tempPathname)
|
|
try {
|
|
await persistor.sendStream(BUCKET, zipKey, stream, {
|
|
contentType: 'application/zip',
|
|
})
|
|
} catch (err) {
|
|
throw new ZipStore.UploadError(projectId, version).withCause(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class CreationError extends OError {
|
|
constructor(projectId, version) {
|
|
super(`Zip creation failed for ${projectId} version ${version}`, {
|
|
projectId,
|
|
version,
|
|
})
|
|
}
|
|
}
|
|
ZipStore.CreationError = CreationError
|
|
|
|
class UploadError extends OError {
|
|
constructor(projectId, version) {
|
|
super(`Zip upload failed for ${projectId} version ${version}`, {
|
|
projectId,
|
|
version,
|
|
})
|
|
}
|
|
}
|
|
ZipStore.UploadError = UploadError
|
|
|
|
module.exports = new ZipStore()
|