2024-05-01 09:52:12 -04:00
|
|
|
const archiver = require('archiver')
|
|
|
|
const OutputCacheManager = require('./OutputCacheManager')
|
|
|
|
const OutputFileFinder = require('./OutputFileFinder')
|
|
|
|
const Settings = require('@overleaf/settings')
|
2024-06-12 04:27:13 -04:00
|
|
|
const { open, realpath } = require('node:fs/promises')
|
2024-06-06 04:51:16 -04:00
|
|
|
const path = require('path')
|
2024-05-01 09:52:12 -04:00
|
|
|
const { NotFoundError } = require('./Errors')
|
|
|
|
|
2024-06-12 04:28:34 -04:00
|
|
|
// NOTE: Updating this list requires a corresponding change in
|
|
|
|
// * services/web/frontend/js/features/pdf-preview/util/file-list.js
|
|
|
|
const ignoreFiles = ['output.fls', 'output.fdb_latexmk']
|
|
|
|
|
2024-05-01 09:52:12 -04:00
|
|
|
function getContentDir(projectId, userId) {
|
|
|
|
let subDir
|
|
|
|
if (userId != null) {
|
|
|
|
subDir = `${projectId}-${userId}`
|
|
|
|
} else {
|
|
|
|
subDir = projectId
|
|
|
|
}
|
|
|
|
return `${Settings.path.outputDir}/${subDir}/`
|
|
|
|
}
|
|
|
|
|
2024-06-12 04:27:13 -04:00
|
|
|
/**
|
|
|
|
* Is the provided path a symlink?
|
|
|
|
* @param {string} path
|
|
|
|
* @return {Promise<boolean>}
|
|
|
|
*/
|
|
|
|
async function isSymlink(path) {
|
|
|
|
try {
|
|
|
|
const realPath = await realpath(path)
|
|
|
|
return realPath !== path
|
|
|
|
} catch (error) {
|
|
|
|
if (error.code === 'ELOOP') {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
throw error
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
async archiveFilesForBuild(projectId, userId, build) {
|
|
|
|
const validFiles = await this._getAllOutputFiles(projectId, userId, build)
|
2024-05-01 09:52:12 -04:00
|
|
|
|
|
|
|
const archive = archiver('zip')
|
|
|
|
|
2024-06-12 04:27:13 -04:00
|
|
|
const missingFiles = []
|
2024-05-01 09:52:12 -04:00
|
|
|
|
|
|
|
for (const file of validFiles) {
|
|
|
|
try {
|
2024-06-12 04:27:13 -04:00
|
|
|
if (!(await isSymlink(file))) {
|
|
|
|
const fileHandle = await open(file, 'r')
|
|
|
|
const fileStream = fileHandle.createReadStream()
|
|
|
|
archive.append(fileStream, { name: path.basename(file) })
|
|
|
|
}
|
2024-05-01 09:52:12 -04:00
|
|
|
} catch (error) {
|
|
|
|
missingFiles.push(file)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (missingFiles.length > 0) {
|
|
|
|
archive.append(missingFiles.join('\n'), {
|
|
|
|
name: 'missing_files.txt',
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
await archive.finalize()
|
|
|
|
|
|
|
|
return archive
|
|
|
|
},
|
|
|
|
|
|
|
|
async _getAllOutputFiles(projectId, userId, build) {
|
|
|
|
const contentDir = getContentDir(projectId, userId)
|
|
|
|
|
|
|
|
try {
|
|
|
|
const { outputFiles } = await OutputFileFinder.promises.findOutputFiles(
|
|
|
|
[],
|
|
|
|
`${contentDir}${OutputCacheManager.path(build, '.')}`
|
|
|
|
)
|
|
|
|
|
2024-06-12 04:27:13 -04:00
|
|
|
return outputFiles
|
2024-06-12 04:28:34 -04:00
|
|
|
.filter(
|
|
|
|
// Ignore the pdf and also ignore the files ignored by the frontend.
|
|
|
|
({ path }) => path !== 'output.pdf' && !ignoreFiles.includes(path)
|
|
|
|
)
|
2024-06-12 04:27:13 -04:00
|
|
|
.map(
|
|
|
|
({ path }) => `${contentDir}${OutputCacheManager.path(build, path)}`
|
|
|
|
)
|
2024-05-01 09:52:12 -04:00
|
|
|
} catch (error) {
|
|
|
|
if (
|
|
|
|
error.code === 'ENOENT' ||
|
|
|
|
error.code === 'ENOTDIR' ||
|
|
|
|
error.code === 'EACCES'
|
|
|
|
) {
|
|
|
|
throw new NotFoundError('Output files not found')
|
|
|
|
}
|
|
|
|
throw error
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|