mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #18538 from overleaf/ar-add-download-all-link-in-web
[web] add download all link for output files GitOrigin-RevId: 3d574d75d53e577cb0f8fd3caa4f757d9e1b7889
This commit is contained in:
parent
a70a3fc77e
commit
d1a58e6b77
10 changed files with 141 additions and 17 deletions
|
@ -193,6 +193,7 @@ async function _sendBuiltRequest(projectId, userId, req, options, callback) {
|
||||||
status: compile.status,
|
status: compile.status,
|
||||||
outputFiles,
|
outputFiles,
|
||||||
clsiServerId,
|
clsiServerId,
|
||||||
|
buildId: compile.buildId,
|
||||||
stats: compile.stats,
|
stats: compile.stats,
|
||||||
timings: compile.timings,
|
timings: compile.timings,
|
||||||
outputUrlPrefix: compile.outputUrlPrefix,
|
outputUrlPrefix: compile.outputUrlPrefix,
|
||||||
|
@ -817,6 +818,7 @@ module.exports = {
|
||||||
'stats',
|
'stats',
|
||||||
'timings',
|
'timings',
|
||||||
'outputUrlPrefix',
|
'outputUrlPrefix',
|
||||||
|
'buildId',
|
||||||
]),
|
]),
|
||||||
sendExternalRequest: callbackifyMultiResult(sendExternalRequest, [
|
sendExternalRequest: callbackifyMultiResult(sendExternalRequest, [
|
||||||
'status',
|
'status',
|
||||||
|
|
|
@ -30,6 +30,15 @@ const pdfDownloadRateLimiter = new RateLimiter('full-pdf-download', {
|
||||||
duration: 60 * 60,
|
duration: 60 * 60,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function getOutputFilesArchiveSpecification(projectId, userId, buildId) {
|
||||||
|
const fileName = 'output.zip'
|
||||||
|
return {
|
||||||
|
path: fileName,
|
||||||
|
url: CompileController._getFileUrl(projectId, userId, buildId, fileName),
|
||||||
|
type: 'zip',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getImageNameForProject(projectId, callback) {
|
function getImageNameForProject(projectId, callback) {
|
||||||
ProjectGetter.getProject(projectId, { imageName: 1 }, (err, project) => {
|
ProjectGetter.getProject(projectId, { imageName: 1 }, (err, project) => {
|
||||||
if (err) return callback(err)
|
if (err) return callback(err)
|
||||||
|
@ -149,7 +158,8 @@ module.exports = CompileController = {
|
||||||
validationProblems,
|
validationProblems,
|
||||||
stats,
|
stats,
|
||||||
timings,
|
timings,
|
||||||
outputUrlPrefix
|
outputUrlPrefix,
|
||||||
|
buildId
|
||||||
) => {
|
) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
Metrics.inc('compile-error')
|
Metrics.inc('compile-error')
|
||||||
|
@ -187,14 +197,21 @@ module.exports = CompileController = {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const outputFilesArchive = buildId
|
||||||
|
? getOutputFilesArchiveSpecification(projectId, userId, buildId)
|
||||||
|
: null
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
status,
|
status,
|
||||||
outputFiles,
|
outputFiles,
|
||||||
|
outputFilesArchive,
|
||||||
compileGroup: limits?.compileGroup,
|
compileGroup: limits?.compileGroup,
|
||||||
clsiServerId,
|
clsiServerId,
|
||||||
validationProblems,
|
validationProblems,
|
||||||
stats,
|
stats,
|
||||||
timings,
|
timings,
|
||||||
|
outputUrlPrefix,
|
||||||
pdfDownloadDomain,
|
pdfDownloadDomain,
|
||||||
pdfCachingMinChunkSize,
|
pdfCachingMinChunkSize,
|
||||||
})
|
})
|
||||||
|
@ -398,6 +415,22 @@ module.exports = CompileController = {
|
||||||
if (error) {
|
if (error) {
|
||||||
return next(error)
|
return next(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const qs = {}
|
||||||
|
|
||||||
|
if (req.params.file === 'output.zip' && req.query?.files) {
|
||||||
|
/**
|
||||||
|
* The output.zip file is generated on the fly and allows either:
|
||||||
|
*
|
||||||
|
* 1. All files to be downloaded (no files query parameter)
|
||||||
|
* 2. A specific set of files to be downloaded (files query parameter)
|
||||||
|
*
|
||||||
|
* As the frontend separates the PDF download and ignores several other output
|
||||||
|
* files we will generally need to tell CLSI specifically what is required.
|
||||||
|
*/
|
||||||
|
qs.files = req.query.files
|
||||||
|
}
|
||||||
|
|
||||||
const url = CompileController._getFileUrl(
|
const url = CompileController._getFileUrl(
|
||||||
projectId,
|
projectId,
|
||||||
userId,
|
userId,
|
||||||
|
@ -408,7 +441,7 @@ module.exports = CompileController = {
|
||||||
projectId,
|
projectId,
|
||||||
'output-file',
|
'output-file',
|
||||||
url,
|
url,
|
||||||
{},
|
qs,
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
next
|
next
|
||||||
|
@ -573,10 +606,20 @@ module.exports = CompileController = {
|
||||||
return next(err)
|
return next(err)
|
||||||
}
|
}
|
||||||
url = new URL(`${Settings.apis.clsi.url}${url}`)
|
url = new URL(`${Settings.apis.clsi.url}${url}`)
|
||||||
url.search = new URLSearchParams({
|
|
||||||
...persistenceOptions.qs,
|
const params = new URLSearchParams(persistenceOptions.qs)
|
||||||
...qs,
|
|
||||||
}).toString()
|
for (const [key, value] of Object.entries(qs)) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const v of value) {
|
||||||
|
params.append(key, v)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
params.append(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
url.search = params.toString()
|
||||||
const timer = new Metrics.Timer(
|
const timer = new Metrics.Timer(
|
||||||
'proxy_to_clsi',
|
'proxy_to_clsi',
|
||||||
1,
|
1,
|
||||||
|
@ -604,7 +647,9 @@ module.exports = CompileController = {
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const key of ['Content-Length', 'Content-Type']) {
|
for (const key of ['Content-Length', 'Content-Type']) {
|
||||||
res.setHeader(key, response.headers.get(key))
|
if (response.headers.has(key)) {
|
||||||
|
res.setHeader(key, response.headers.get(key))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
res.writeHead(response.status)
|
res.writeHead(response.status)
|
||||||
return pipeline(stream, res)
|
return pipeline(stream, res)
|
||||||
|
|
|
@ -77,6 +77,7 @@ async function compile(projectId, userId, options = {}) {
|
||||||
stats,
|
stats,
|
||||||
timings,
|
timings,
|
||||||
outputUrlPrefix,
|
outputUrlPrefix,
|
||||||
|
buildId,
|
||||||
} = await ClsiManager.promises.sendRequest(projectId, compileAsUser, options)
|
} = await ClsiManager.promises.sendRequest(projectId, compileAsUser, options)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -88,6 +89,7 @@ async function compile(projectId, userId, options = {}) {
|
||||||
stats,
|
stats,
|
||||||
timings,
|
timings,
|
||||||
outputUrlPrefix,
|
outputUrlPrefix,
|
||||||
|
buildId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,6 +175,7 @@ module.exports = CompileManager = {
|
||||||
'stats',
|
'stats',
|
||||||
'timings',
|
'timings',
|
||||||
'outputUrlPrefix',
|
'outputUrlPrefix',
|
||||||
|
'buildId',
|
||||||
]),
|
]),
|
||||||
|
|
||||||
stopCompile: callbackify(stopCompile),
|
stopCompile: callbackify(stopCompile),
|
||||||
|
|
|
@ -308,6 +308,7 @@
|
||||||
"doing_this_will_verify_affiliation_and_allow_log_in_2": "",
|
"doing_this_will_verify_affiliation_and_allow_log_in_2": "",
|
||||||
"done": "",
|
"done": "",
|
||||||
"download": "",
|
"download": "",
|
||||||
|
"download_all": "",
|
||||||
"download_metadata": "",
|
"download_metadata": "",
|
||||||
"download_pdf": "",
|
"download_pdf": "",
|
||||||
"download_zip_file": "",
|
"download_zip_file": "",
|
||||||
|
|
|
@ -33,6 +33,18 @@ function PdfFileList({ fileList }) {
|
||||||
<b>{file.path}</b>
|
<b>{file.path}</b>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{fileList.archive?.fileCount > 0 && (
|
||||||
|
<MenuItem
|
||||||
|
download={basename(fileList.archive)}
|
||||||
|
href={fileList.archive.url}
|
||||||
|
key={fileList.archive.path}
|
||||||
|
>
|
||||||
|
<b>
|
||||||
|
{t('download_all')} ({fileList.archive.fileCount})
|
||||||
|
</b>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -48,6 +60,11 @@ PdfFileList.propTypes = {
|
||||||
fileList: PropTypes.shape({
|
fileList: PropTypes.shape({
|
||||||
top: FilesArray,
|
top: FilesArray,
|
||||||
other: FilesArray,
|
other: FilesArray,
|
||||||
|
archive: PropTypes.shape({
|
||||||
|
path: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
fileCount: PropTypes.number.isRequired,
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
const topFileTypes = ['bbl', 'gls', 'ind']
|
const topFileTypes = ['bbl', 'gls', 'ind']
|
||||||
const ignoreFiles = ['output.fls', 'output.fdb_latexmk']
|
const ignoreFiles = ['output.fls', 'output.fdb_latexmk']
|
||||||
|
|
||||||
export const buildFileList = (outputFiles, clsiServerId, compileGroup) => {
|
export const buildFileList = (
|
||||||
|
outputFiles,
|
||||||
|
clsiServerId,
|
||||||
|
compileGroup,
|
||||||
|
outputFilesArchive
|
||||||
|
) => {
|
||||||
const files = { top: [], other: [] }
|
const files = { top: [], other: [] }
|
||||||
|
|
||||||
if (outputFiles) {
|
if (outputFiles) {
|
||||||
|
@ -52,6 +57,18 @@ export const buildFileList = (outputFiles, clsiServerId, compileGroup) => {
|
||||||
files.other.push(file)
|
files.other.push(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const archivableFiles = [...files.top, ...files.other]
|
||||||
|
|
||||||
|
if (outputFilesArchive && archivableFiles.length > 0) {
|
||||||
|
archivableFiles.forEach(file => params.append('files', file.path))
|
||||||
|
|
||||||
|
files.archive = {
|
||||||
|
...outputFilesArchive,
|
||||||
|
fileCount: archivableFiles.length,
|
||||||
|
url: `${outputFilesArchive.url}?${params.toString()}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return files
|
return files
|
||||||
|
|
|
@ -54,6 +54,7 @@ export type CompileContext = {
|
||||||
isProjectOwner: boolean
|
isProjectOwner: boolean
|
||||||
logEntries?: Record<string, any>
|
logEntries?: Record<string, any>
|
||||||
logEntryAnnotations?: Record<string, any>
|
logEntryAnnotations?: Record<string, any>
|
||||||
|
outputFilesArchive?: string
|
||||||
pdfDownloadUrl?: string
|
pdfDownloadUrl?: string
|
||||||
pdfFile?: PdfFile
|
pdfFile?: PdfFile
|
||||||
pdfUrl?: string
|
pdfUrl?: string
|
||||||
|
@ -368,7 +369,12 @@ export const LocalCompileProvider: FC = ({ children }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
setFileList(
|
setFileList(
|
||||||
buildFileList(outputFiles, data.clsiServerId, data.compileGroup)
|
buildFileList(
|
||||||
|
outputFiles,
|
||||||
|
data.clsiServerId,
|
||||||
|
data.compileGroup,
|
||||||
|
data.outputFilesArchive
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// handle log files
|
// handle log files
|
||||||
|
|
|
@ -435,6 +435,7 @@
|
||||||
"dont_have_account": "Don’t have an account?",
|
"dont_have_account": "Don’t have an account?",
|
||||||
"dont_have_account_without_question_mark": "Don’t have an account",
|
"dont_have_account_without_question_mark": "Don’t have an account",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
|
"download_all": "Download all",
|
||||||
"download_metadata": "Download Overleaf metadata",
|
"download_metadata": "Download Overleaf metadata",
|
||||||
"download_pdf": "Download PDF",
|
"download_pdf": "Download PDF",
|
||||||
"download_zip_file": "Download .zip file",
|
"download_zip_file": "Download .zip file",
|
||||||
|
|
|
@ -156,19 +156,21 @@ describe('ClsiManager', function () {
|
||||||
|
|
||||||
describe('sendRequest', function () {
|
describe('sendRequest', function () {
|
||||||
describe('with a successful compile', function () {
|
describe('with a successful compile', function () {
|
||||||
|
const buildId = '18fbe9e7564-30dcb2f71250c690'
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
this.outputFiles = [
|
this.outputFiles = [
|
||||||
{
|
{
|
||||||
url: `/project/${this.project_id}/user/${this.user_id}/build/1234/output/output.pdf`,
|
url: `/project/${this.project_id}/user/${this.user_id}/build/1234/output/output.pdf`,
|
||||||
path: 'output.pdf',
|
path: 'output.pdf',
|
||||||
type: 'pdf',
|
type: 'pdf',
|
||||||
build: 1234,
|
build: buildId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: `/project/${this.project_id}/user/${this.user_id}/build/1234/output/output.log`,
|
url: `/project/${this.project_id}/user/${this.user_id}/build/1234/output/output.log`,
|
||||||
path: 'output.log',
|
path: 'output.log',
|
||||||
type: 'log',
|
type: 'log',
|
||||||
build: 1234,
|
build: buildId,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.responseBody.compile.outputFiles = this.outputFiles.map(
|
this.responseBody.compile.outputFiles = this.outputFiles.map(
|
||||||
|
@ -177,6 +179,7 @@ describe('ClsiManager', function () {
|
||||||
url: `http://${CLSI_HOST}${outputFile.url}`,
|
url: `http://${CLSI_HOST}${outputFile.url}`,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
this.responseBody.compile.buildId = buildId
|
||||||
this.timeout = 100
|
this.timeout = 100
|
||||||
this.result = await this.ClsiManager.promises.sendRequest(
|
this.result = await this.ClsiManager.promises.sendRequest(
|
||||||
this.project._id,
|
this.project._id,
|
||||||
|
@ -264,6 +267,10 @@ describe('ClsiManager', function () {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should return the buildId', function () {
|
||||||
|
expect(this.result.buildId).to.equal(buildId)
|
||||||
|
})
|
||||||
|
|
||||||
it('should persist the cookie from the response', function () {
|
it('should persist the cookie from the response', function () {
|
||||||
expect(
|
expect(
|
||||||
this.ClsiCookieManager.promises.setServerId
|
this.ClsiCookieManager.promises.setServerId
|
||||||
|
|
|
@ -110,9 +110,11 @@ describe('CompileController', function () {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.projectId = 'project-id'
|
this.projectId = 'project-id'
|
||||||
|
this.build_id = '18fbe9e7564-30dcb2f71250c690'
|
||||||
this.next = sinon.stub()
|
this.next = sinon.stub()
|
||||||
this.req = new MockRequest()
|
this.req = new MockRequest()
|
||||||
this.res = new MockResponse()
|
this.res = new MockResponse()
|
||||||
|
this.res = new MockResponse()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('compile', function () {
|
describe('compile', function () {
|
||||||
|
@ -129,7 +131,14 @@ describe('CompileController', function () {
|
||||||
url: `/project/${this.projectId}/user/${this.user_id}/build/id/output.pdf`,
|
url: `/project/${this.projectId}/user/${this.user_id}/build/id/output.pdf`,
|
||||||
type: 'pdf',
|
type: 'pdf',
|
||||||
},
|
},
|
||||||
])
|
]),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
this.build_id
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -156,6 +165,11 @@ describe('CompileController', function () {
|
||||||
type: 'pdf',
|
type: 'pdf',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
outputFilesArchive: {
|
||||||
|
path: 'output.zip',
|
||||||
|
url: `/project/${this.projectId}/user/wat/build/${this.build_id}/output/output.zip`,
|
||||||
|
type: 'zip',
|
||||||
|
},
|
||||||
pdfDownloadDomain: 'https://compiles.overleaf.test',
|
pdfDownloadDomain: 'https://compiles.overleaf.test',
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -181,7 +195,8 @@ describe('CompileController', function () {
|
||||||
undefined, // validationProblems
|
undefined, // validationProblems
|
||||||
undefined, // stats
|
undefined, // stats
|
||||||
undefined, // timings
|
undefined, // timings
|
||||||
'/zone/b'
|
'/zone/b',
|
||||||
|
this.build_id
|
||||||
)
|
)
|
||||||
this.CompileController.compile(this.req, this.res, this.next)
|
this.CompileController.compile(this.req, this.res, this.next)
|
||||||
})
|
})
|
||||||
|
@ -198,6 +213,12 @@ describe('CompileController', function () {
|
||||||
type: 'pdf',
|
type: 'pdf',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
outputFilesArchive: {
|
||||||
|
path: 'output.zip',
|
||||||
|
url: `/project/${this.projectId}/user/wat/build/${this.build_id}/output/output.zip`,
|
||||||
|
type: 'zip',
|
||||||
|
},
|
||||||
|
outputUrlPrefix: '/zone/b',
|
||||||
pdfDownloadDomain: 'https://compiles.overleaf.test/zone/b',
|
pdfDownloadDomain: 'https://compiles.overleaf.test/zone/b',
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -240,6 +261,11 @@ describe('CompileController', function () {
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
status: this.status,
|
status: this.status,
|
||||||
outputFiles: this.outputFiles,
|
outputFiles: this.outputFiles,
|
||||||
|
outputFilesArchive: {
|
||||||
|
path: 'output.zip',
|
||||||
|
url: `/project/${this.projectId}/user/wat/build/${this.build_id}/output/output.zip`,
|
||||||
|
type: 'zip',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -429,9 +455,9 @@ describe('CompileController', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when the a build-id is provided', function () {
|
describe('when a build-id is provided', function () {
|
||||||
beforeEach(function (done) {
|
beforeEach(function (done) {
|
||||||
this.req.params.build_id = this.buildId = '1234-5678'
|
this.req.params.build_id = this.build_id
|
||||||
this.CompileController.proxyToClsi = sinon
|
this.CompileController.proxyToClsi = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.callsFake(() => done())
|
.callsFake(() => done())
|
||||||
|
@ -443,7 +469,7 @@ describe('CompileController', function () {
|
||||||
.calledWith(
|
.calledWith(
|
||||||
this.projectId,
|
this.projectId,
|
||||||
'output-file',
|
'output-file',
|
||||||
`/project/${this.projectId}/user/${this.user_id}/build/${this.buildId}/output/output.pdf`,
|
`/project/${this.projectId}/user/${this.user_id}/build/${this.build_id}/output/output.pdf`,
|
||||||
{},
|
{},
|
||||||
this.req,
|
this.req,
|
||||||
this.res,
|
this.res,
|
||||||
|
@ -457,7 +483,6 @@ describe('CompileController', function () {
|
||||||
describe('getFileFromClsiWithoutUser', function () {
|
describe('getFileFromClsiWithoutUser', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.submission_id = 'sub-1234'
|
this.submission_id = 'sub-1234'
|
||||||
this.build_id = 123456
|
|
||||||
this.file = 'project.pdf'
|
this.file = 'project.pdf'
|
||||||
this.req.params = {
|
this.req.params = {
|
||||||
submission_id: this.submission_id,
|
submission_id: this.submission_id,
|
||||||
|
|
Loading…
Reference in a new issue