|
|
@@ -1,16 +1,24 @@
|
|
|
+// @ts-check
|
|
|
'use strict'
|
|
|
|
|
|
+/** @typedef {import('overleaf-editor-core/types').Snapshot} Snapshot */
|
|
|
+
|
|
|
const Archive = require('archiver')
|
|
|
const BPromise = require('bluebird')
|
|
|
const fs = require('fs')
|
|
|
const { pipeline } = require('stream')
|
|
|
|
|
|
const core = require('overleaf-editor-core')
|
|
|
+
|
|
|
const Snapshot = core.Snapshot
|
|
|
const OError = require('@overleaf/o-error')
|
|
|
|
|
|
const assert = require('./assert')
|
|
|
|
|
|
+/**
|
|
|
+ * @typedef {import('../../storage/lib/blob_store/index').BlobStore} BlobStore
|
|
|
+ */
|
|
|
+
|
|
|
// The maximum safe concurrency appears to be 1.
|
|
|
// https://github.com/overleaf/issues/issues/1909
|
|
|
const FETCH_CONCURRENCY = 1 // number of files to fetch at once
|
|
|
@@ -21,101 +29,113 @@ class DownloadError extends OError {
|
|
|
super(`ProjectArchive: blob download failed: ${hash}`, { hash })
|
|
|
}
|
|
|
}
|
|
|
-ProjectArchive.DownloadError = DownloadError
|
|
|
|
|
|
class ArchiveTimeout extends OError {
|
|
|
constructor() {
|
|
|
super('ProjectArchive timed out')
|
|
|
}
|
|
|
}
|
|
|
-ProjectArchive.ArchiveTimeout = ArchiveTimeout
|
|
|
|
|
|
-/**
|
|
|
- * @constructor
|
|
|
- * @param {Snapshot} snapshot
|
|
|
- * @param {?number} timeout in ms
|
|
|
- * @classdesc
|
|
|
- * Writes the project snapshot to a zip file.
|
|
|
- */
|
|
|
-function ProjectArchive(snapshot, timeout) {
|
|
|
- assert.instance(snapshot, Snapshot)
|
|
|
- this.snapshot = snapshot
|
|
|
- this.timeout = timeout || DEFAULT_ZIP_TIMEOUT
|
|
|
+class MissingfileError extends OError {
|
|
|
+ constructor() {
|
|
|
+ super('ProjectArchive: attempting to look up a file that does not exist')
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * Write zip archive to the given file path.
|
|
|
- *
|
|
|
- * @param {BlobStore} blobStore
|
|
|
- * @param {string} zipFilePath
|
|
|
- */
|
|
|
-ProjectArchive.prototype.writeZip = function projectArchiveToZip(
|
|
|
- blobStore,
|
|
|
- zipFilePath
|
|
|
-) {
|
|
|
- const snapshot = this.snapshot
|
|
|
- const timeout = this.timeout
|
|
|
-
|
|
|
- const startTime = process.hrtime()
|
|
|
- const archive = new Archive('zip')
|
|
|
-
|
|
|
- // Convert elapsed seconds and nanoseconds to milliseconds.
|
|
|
- function findElapsedMilliseconds() {
|
|
|
- const elapsed = process.hrtime(startTime)
|
|
|
- return elapsed[0] * 1e3 + elapsed[1] * 1e-6
|
|
|
+class ProjectArchive {
|
|
|
+ static ArchiveTimeout = ArchiveTimeout
|
|
|
+ static MissingfileError = MissingfileError
|
|
|
+ static DownloadError = DownloadError
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @constructor
|
|
|
+ * @param {Snapshot} snapshot
|
|
|
+ * @param {?number} timeout in ms
|
|
|
+ * @classdesc
|
|
|
+ * Writes the project snapshot to a zip file.
|
|
|
+ */
|
|
|
+ constructor(snapshot, timeout) {
|
|
|
+ assert.instance(snapshot, Snapshot)
|
|
|
+ this.snapshot = snapshot
|
|
|
+ this.timeout = timeout || DEFAULT_ZIP_TIMEOUT
|
|
|
}
|
|
|
|
|
|
- function addFileToArchive(pathname) {
|
|
|
- if (findElapsedMilliseconds() > timeout) {
|
|
|
- throw new ProjectArchive.ArchiveTimeout()
|
|
|
+ /**
|
|
|
+ * Write zip archive to the given file path.
|
|
|
+ *
|
|
|
+ * @param {BlobStore} blobStore
|
|
|
+ * @param {string} zipFilePath
|
|
|
+ */
|
|
|
+ writeZip(blobStore, zipFilePath) {
|
|
|
+ const snapshot = this.snapshot
|
|
|
+ const timeout = this.timeout
|
|
|
+
|
|
|
+ const startTime = process.hrtime()
|
|
|
+ const archive = new Archive('zip')
|
|
|
+
|
|
|
+ // Convert elapsed seconds and nanoseconds to milliseconds.
|
|
|
+ function findElapsedMilliseconds() {
|
|
|
+ const elapsed = process.hrtime(startTime)
|
|
|
+ return elapsed[0] * 1e3 + elapsed[1] * 1e-6
|
|
|
}
|
|
|
|
|
|
- const file = snapshot.getFile(pathname)
|
|
|
- return file.load('eager', blobStore).then(function () {
|
|
|
- const content = file.getContent()
|
|
|
- if (content === null) {
|
|
|
- return streamFileToArchive(pathname, file).catch(function (err) {
|
|
|
- throw new ProjectArchive.DownloadError(file.getHash()).withCause(err)
|
|
|
- })
|
|
|
- } else {
|
|
|
- archive.append(content, { name: pathname })
|
|
|
+ function addFileToArchive(pathname) {
|
|
|
+ if (findElapsedMilliseconds() > timeout) {
|
|
|
+ throw new ProjectArchive.ArchiveTimeout()
|
|
|
}
|
|
|
- })
|
|
|
- }
|
|
|
|
|
|
- function streamFileToArchive(pathname, file) {
|
|
|
- return new BPromise(function (resolve, reject) {
|
|
|
- blobStore
|
|
|
- .getStream(file.getHash())
|
|
|
- .then(stream => {
|
|
|
- stream.on('error', reject)
|
|
|
- stream.on('end', resolve)
|
|
|
- archive.append(stream, { name: pathname })
|
|
|
- })
|
|
|
- .catch(reject)
|
|
|
+ const file = snapshot.getFile(pathname)
|
|
|
+ if (!file) {
|
|
|
+ throw new ProjectArchive.MissingfileError()
|
|
|
+ }
|
|
|
+ return file.load('eager', blobStore).then(function () {
|
|
|
+ const content = file.getContent({ filterTrackedDeletes: true })
|
|
|
+ if (content === null) {
|
|
|
+ return streamFileToArchive(pathname, file).catch(function (err) {
|
|
|
+ throw new ProjectArchive.DownloadError(file.getHash()).withCause(
|
|
|
+ err
|
|
|
+ )
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ archive.append(content, { name: pathname })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ function streamFileToArchive(pathname, file) {
|
|
|
+ return new BPromise(function (resolve, reject) {
|
|
|
+ blobStore
|
|
|
+ .getStream(file.getHash())
|
|
|
+ .then(stream => {
|
|
|
+ stream.on('error', reject)
|
|
|
+ stream.on('end', resolve)
|
|
|
+ archive.append(stream, { name: pathname })
|
|
|
+ })
|
|
|
+ .catch(reject)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const addFilesToArchiveAndFinalize = BPromise.map(
|
|
|
+ snapshot.getFilePathnames(),
|
|
|
+ addFileToArchive,
|
|
|
+ { concurrency: FETCH_CONCURRENCY }
|
|
|
+ ).then(function () {
|
|
|
+ archive.finalize()
|
|
|
})
|
|
|
- }
|
|
|
|
|
|
- const addFilesToArchiveAndFinalize = BPromise.map(
|
|
|
- snapshot.getFilePathnames(),
|
|
|
- addFileToArchive,
|
|
|
- { concurrency: FETCH_CONCURRENCY }
|
|
|
- ).then(function () {
|
|
|
- archive.finalize()
|
|
|
- })
|
|
|
-
|
|
|
- const streamArchiveToFile = new BPromise(function (resolve, reject) {
|
|
|
- const stream = fs.createWriteStream(zipFilePath)
|
|
|
- pipeline(archive, stream, function (err) {
|
|
|
- if (err) {
|
|
|
- reject(err)
|
|
|
- } else {
|
|
|
- resolve()
|
|
|
- }
|
|
|
+ const streamArchiveToFile = new BPromise(function (resolve, reject) {
|
|
|
+ const stream = fs.createWriteStream(zipFilePath)
|
|
|
+ pipeline(archive, stream, function (err) {
|
|
|
+ if (err) {
|
|
|
+ reject(err)
|
|
|
+ } else {
|
|
|
+ resolve()
|
|
|
+ }
|
|
|
+ })
|
|
|
})
|
|
|
- })
|
|
|
|
|
|
- return BPromise.join(streamArchiveToFile, addFilesToArchiveAndFinalize)
|
|
|
+ return BPromise.join(streamArchiveToFile, addFilesToArchiveAndFinalize)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
module.exports = ProjectArchive
|