| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- // @ts-check
- 'use strict'
- /**
- * @import { Snapshot } from 'overleaf-editor-core'
- * @import { BlobStore } from '../../storage/lib/blob_store/index'
- */
- const Archive = require('archiver')
- const BPromise = require('bluebird')
- const fs = require('node:fs')
- const { pipeline } = require('node:stream')
- const core = require('overleaf-editor-core')
- const Snapshot = core.Snapshot
- const OError = require('@overleaf/o-error')
- const assert = require('./assert')
- // 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
- class DownloadError extends OError {
- constructor(hash) {
- super(`ProjectArchive: blob download failed: ${hash}`, { hash })
- }
- }
- class ArchiveTimeout extends OError {
- constructor() {
- super('ProjectArchive timed out')
- }
- }
- class MissingfileError extends OError {
- constructor() {
- super('ProjectArchive: attempting to look up a file that does not exist')
- }
- }
- 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
- }
- /**
- * 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
- }
- function addFileToArchive(pathname) {
- if (findElapsedMilliseconds() > timeout) {
- throw new ProjectArchive.ArchiveTimeout()
- }
- 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 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)
- }
- }
- module.exports = ProjectArchive
|