OutputFileArchiveManager.js 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. const archiver = require('archiver')
  2. const OutputCacheManager = require('./OutputCacheManager')
  3. const OutputFileFinder = require('./OutputFileFinder')
  4. const Settings = require('@overleaf/settings')
  5. const { open, realpath } = require('node:fs/promises')
  6. const path = require('path')
  7. const { NotFoundError } = require('./Errors')
  8. // NOTE: Updating this list requires a corresponding change in
  9. // * services/web/frontend/js/features/pdf-preview/util/file-list.js
  10. const ignoreFiles = ['output.fls', 'output.fdb_latexmk']
  11. function getContentDir(projectId, userId) {
  12. let subDir
  13. if (userId != null) {
  14. subDir = `${projectId}-${userId}`
  15. } else {
  16. subDir = projectId
  17. }
  18. return `${Settings.path.outputDir}/${subDir}/`
  19. }
  20. /**
  21. * Is the provided path a symlink?
  22. * @param {string} path
  23. * @return {Promise<boolean>}
  24. */
  25. async function isSymlink(path) {
  26. try {
  27. const realPath = await realpath(path)
  28. return realPath !== path
  29. } catch (error) {
  30. if (error.code === 'ELOOP') {
  31. return true
  32. }
  33. throw error
  34. }
  35. }
  36. module.exports = {
  37. async archiveFilesForBuild(projectId, userId, build) {
  38. const validFiles = await this._getAllOutputFiles(projectId, userId, build)
  39. const archive = archiver('zip')
  40. const missingFiles = []
  41. for (const file of validFiles) {
  42. try {
  43. if (!(await isSymlink(file))) {
  44. const fileHandle = await open(file, 'r')
  45. const fileStream = fileHandle.createReadStream()
  46. archive.append(fileStream, { name: path.basename(file) })
  47. }
  48. } catch (error) {
  49. missingFiles.push(file)
  50. }
  51. }
  52. if (missingFiles.length > 0) {
  53. archive.append(missingFiles.join('\n'), {
  54. name: 'missing_files.txt',
  55. })
  56. }
  57. await archive.finalize()
  58. return archive
  59. },
  60. async _getAllOutputFiles(projectId, userId, build) {
  61. const contentDir = getContentDir(projectId, userId)
  62. try {
  63. const { outputFiles } = await OutputFileFinder.promises.findOutputFiles(
  64. [],
  65. `${contentDir}${OutputCacheManager.path(build, '.')}`
  66. )
  67. return outputFiles
  68. .filter(
  69. // Ignore the pdf and also ignore the files ignored by the frontend.
  70. ({ path }) => path !== 'output.pdf' && !ignoreFiles.includes(path)
  71. )
  72. .map(
  73. ({ path }) => `${contentDir}${OutputCacheManager.path(build, path)}`
  74. )
  75. } catch (error) {
  76. if (
  77. error.code === 'ENOENT' ||
  78. error.code === 'ENOTDIR' ||
  79. error.code === 'EACCES'
  80. ) {
  81. throw new NotFoundError('Output files not found')
  82. }
  83. throw error
  84. }
  85. },
  86. }