[monorepo] switch all output file reads to clsi-nginx (#31691)
* [monorepo] switch all output file reads to clsi-nginx * [clsi-lb] allow gallery download requests * [terraform] clsi: use nginx.conf from clsi service * [clsi] fix flakey tests * [clsi] replace alias with rewrite and root in nginx config * [k8s] clsi-lb: expose download port on internal service * [web] add explicit endpoint for downloading all output files Serve the output.zip endpoint from clsi. * [clsi] fix regex for latexqc submission ids Previously, we only handled template submission ids. GitOrigin-RevId: 6c3b21b01ec41ae767530b14aac31fbe3d640dd5
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
CHAT_HOST=chat
|
||||
CLSI_HOST=clsi
|
||||
DOWNLOAD_HOST=clsi-nginx
|
||||
CONTACTS_HOST=contacts
|
||||
DOCSTORE_HOST=docstore
|
||||
DOCUMENT_UPDATER_HOST=document-updater
|
||||
|
||||
@@ -36,6 +36,17 @@ services:
|
||||
- ${DOCKER_SOCKET_PATH:-/var/run/docker.sock}:/var/run/docker.sock
|
||||
- clsi-cache:/overleaf/services/clsi/cache
|
||||
|
||||
clsi-nginx:
|
||||
image: nginx:1.28
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /var/cache/nginx
|
||||
- /run
|
||||
volumes:
|
||||
- ${PWD}/output:/output:ro
|
||||
- ../services/clsi/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro
|
||||
|
||||
contacts:
|
||||
build:
|
||||
context: ..
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# keep in sync with clsi-startup.sh files
|
||||
# keep in sync with clsi/nginx.conf
|
||||
# keep in sync with services/clsi/nginx.conf
|
||||
# Changes to the above:
|
||||
# - added debug header
|
||||
# - added Security-Headers
|
||||
# - remove CORS rules, Server-CE/Server-Pro runs behind a single origin
|
||||
# - change /output path to /var/lib/overleaf/data/output
|
||||
# - remove tiny.pdf endpoints
|
||||
|
||||
server {
|
||||
# Extra header for debugging.
|
||||
add_header 'X-Served-By' 'clsi-nginx' always;
|
||||
|
||||
# Security-Headers
|
||||
@@ -30,20 +29,14 @@ server {
|
||||
application/pdf pdf;
|
||||
}
|
||||
# handle output files for specific users
|
||||
location ~ ^/project/([0-9a-f]+)/user/([0-9a-f]+)/build/([0-9a-f-]+)/output/output\.([a-z.]+)$ {
|
||||
alias /var/lib/overleaf/data/output/$1-$2/generated-files/$3/output.$4;
|
||||
}
|
||||
# handle .blg files for specific users
|
||||
location ~ ^/project/([0-9a-f]+)/user/([0-9a-f]+)/build/([0-9a-f-]+)/output/(.+)\.blg$ {
|
||||
alias /var/lib/overleaf/data/output/$1-$2/generated-files/$3/$4.blg;
|
||||
location ~ ^/project/([0-9a-f]+)/user/([0-9a-f]+)/build/([0-9a-f-]+)/output/(.+)$ {
|
||||
rewrite ^/project/([0-9a-f]+)/user/([0-9a-f]+)/build/([0-9a-f-]+)/output/(.+)$ /$4 break;
|
||||
root /var/lib/overleaf/data/output/$1-$2/generated-files/$3/;
|
||||
}
|
||||
# handle output files for anonymous users
|
||||
location ~ ^/project/([0-9a-f]+)/build/([0-9a-f-]+)/output/output\.([a-z.]+)$ {
|
||||
alias /var/lib/overleaf/data/output/$1/generated-files/$2/output.$3;
|
||||
}
|
||||
# handle .blg files for anonymous users
|
||||
location ~ ^/project/([0-9a-f]+)/build/([0-9a-f-]+)/output/(.+)\.blg$ {
|
||||
alias /var/lib/overleaf/data/output/$1/generated-files/$2/$3.blg;
|
||||
location ~ ^/project/([0-9a-f]+)/build/([0-9a-f-]+)/output/(.+)$ {
|
||||
rewrite ^/project/([0-9a-f]+)/build/([0-9a-f-]+)/output/(.+)$ /$3 break;
|
||||
root /var/lib/overleaf/data/output/$1/generated-files/$2/;
|
||||
}
|
||||
|
||||
# PDF range for specific users
|
||||
@@ -58,4 +51,9 @@ server {
|
||||
expires 1d;
|
||||
alias /var/lib/overleaf/data/output/$1/content/$2;
|
||||
}
|
||||
|
||||
# Do not look up any non matching files in the default root.
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ function compile(req, res, next) {
|
||||
outputUrlPrefix: Settings.apis.clsi.outputUrlPrefix,
|
||||
outputFiles: outputFiles.map(file => ({
|
||||
url:
|
||||
`${Settings.apis.clsi.url}/project/${request.project_id}` +
|
||||
`${Settings.apis.clsi.downloadHost}/project/${request.project_id}` +
|
||||
(request.user_id != null
|
||||
? `/user/${request.user_id}`
|
||||
: '') +
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
clsi
|
||||
--data-dirs=cache,compiles,output
|
||||
--dependencies=
|
||||
--env-add=ALLOWED_COMPILE_GROUPS="clsi-perf simple-latex-file",ENABLE_PDF_CACHING="true",PDF_CACHING_ENABLE_WORKER_POOL="true",ALLOWED_IMAGES=quay.io/sharelatex/texlive-full:2025.1,TEXLIVE_IMAGE=quay.io/sharelatex/texlive-full:2025.1,TEX_LIVE_IMAGE_NAME_OVERRIDE=us-east1-docker.pkg.dev/overleaf-ops/ol-docker,TEXLIVE_IMAGE_USER="tex",SANDBOXED_COMPILES="true",SANDBOXED_COMPILES_HOST_DIR_COMPILES=$PWD/compiles,SANDBOXED_COMPILES_HOST_DIR_OUTPUT=$PWD/output
|
||||
--env-add=DOWNLOAD_HOST=http://clsi-nginx:8080,ALLOWED_COMPILE_GROUPS="clsi-perf simple-latex-file",ENABLE_PDF_CACHING="true",PDF_CACHING_ENABLE_WORKER_POOL="true",ALLOWED_IMAGES=quay.io/sharelatex/texlive-full:2025.1,TEXLIVE_IMAGE=quay.io/sharelatex/texlive-full:2025.1,TEX_LIVE_IMAGE_NAME_OVERRIDE=us-east1-docker.pkg.dev/overleaf-ops/ol-docker,TEXLIVE_IMAGE_USER="tex",SANDBOXED_COMPILES="true",SANDBOXED_COMPILES_HOST_DIR_COMPILES=$PWD/compiles,SANDBOXED_COMPILES_HOST_DIR_OUTPUT=$PWD/output
|
||||
--env-pass-through=
|
||||
--esmock-loader=False
|
||||
--node-version=24.13.0
|
||||
|
||||
@@ -49,7 +49,7 @@ module.exports = {
|
||||
outputUrlPrefix: `${process.env.ZONE ? `/zone/${process.env.ZONE}` : ''}`,
|
||||
clsiServerId: process.env.CLSI_SERVER_ID || CLSI_SERVER_ID,
|
||||
|
||||
downloadHost: process.env.DOWNLOAD_HOST || 'http://localhost:3013',
|
||||
downloadHost: process.env.DOWNLOAD_HOST || 'http://localhost:8080',
|
||||
},
|
||||
clsiPerf: {
|
||||
host: `${process.env.CLSI_PERF_HOST || '127.0.0.1'}:${
|
||||
|
||||
@@ -27,6 +27,7 @@ services:
|
||||
MOCHA_GREP: ${MOCHA_GREP}
|
||||
NODE_ENV: test
|
||||
NODE_OPTIONS: "--unhandled-rejections=strict"
|
||||
DOWNLOAD_HOST: http://clsi-nginx:8080
|
||||
ALLOWED_COMPILE_GROUPS: "clsi-perf simple-latex-file"
|
||||
ENABLE_PDF_CACHING: "true"
|
||||
PDF_CACHING_ENABLE_WORKER_POOL: "true"
|
||||
@@ -40,7 +41,11 @@ services:
|
||||
volumes:
|
||||
- ./reports:/overleaf/services/clsi/reports
|
||||
- ./compiles:/overleaf/services/clsi/compiles
|
||||
- ./output:/overleaf/services/clsi/output
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
depends_on:
|
||||
clsi-nginx:
|
||||
condition: service_started
|
||||
command: npm run test:acceptance
|
||||
|
||||
tar:
|
||||
@@ -50,3 +55,15 @@ services:
|
||||
- ./:/tmp/build/
|
||||
command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs .
|
||||
user: root
|
||||
|
||||
clsi-nginx:
|
||||
image: nginx:1.28
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /var/cache/nginx
|
||||
- /run
|
||||
volumes:
|
||||
- ./output:/output:ro
|
||||
- ./nginx.conf:/etc/nginx/conf.d/nginx.conf:ro
|
||||
- ./tiny.pdf:/var/clsi/tiny.pdf:ro
|
||||
|
||||
@@ -41,6 +41,7 @@ services:
|
||||
LOG_LEVEL: ${LOG_LEVEL:-}
|
||||
NODE_ENV: test
|
||||
NODE_OPTIONS: "--unhandled-rejections=strict"
|
||||
DOWNLOAD_HOST: http://clsi-nginx:8080
|
||||
ALLOWED_COMPILE_GROUPS: "clsi-perf simple-latex-file"
|
||||
ENABLE_PDF_CACHING: "true"
|
||||
PDF_CACHING_ENABLE_WORKER_POOL: "true"
|
||||
@@ -51,4 +52,19 @@ services:
|
||||
SANDBOXED_COMPILES: "true"
|
||||
SANDBOXED_COMPILES_HOST_DIR_COMPILES: $PWD/compiles
|
||||
SANDBOXED_COMPILES_HOST_DIR_OUTPUT: $PWD/output
|
||||
depends_on:
|
||||
clsi-nginx:
|
||||
condition: service_started
|
||||
command: npm run --silent test:acceptance
|
||||
|
||||
clsi-nginx:
|
||||
image: nginx:1.28
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
- /var/cache/nginx
|
||||
- /run
|
||||
volumes:
|
||||
- ./output:/output:ro
|
||||
- ./nginx.conf:/etc/nginx/conf.d/nginx.conf:ro
|
||||
- ./tiny.pdf:/var/clsi/tiny.pdf:ro
|
||||
|
||||
+16
-21
@@ -1,14 +1,10 @@
|
||||
# keep in sync with clsi-startup.sh files
|
||||
# keep in sync with server-ce/nginx/clsi-nginx.conf
|
||||
# Changes to the above:
|
||||
# - added debug header
|
||||
|
||||
server {
|
||||
# Extra header for dev-env.
|
||||
add_header 'X-Served-By' 'clsi-nginx' always;
|
||||
|
||||
listen 8080;
|
||||
server_name clsi-proxy;
|
||||
server_name clsi-nginx;
|
||||
server_tokens off;
|
||||
access_log off;
|
||||
# Ignore symlinks possibly created by users
|
||||
@@ -46,40 +42,34 @@ server {
|
||||
}
|
||||
|
||||
# handle output files for specific users
|
||||
location ~ ^/project/([0-9a-f]+)/user/([0-9a-f]+)/build/([0-9a-f-]+)/output/output\.([a-z.]+)$ {
|
||||
location ~ ^/project/([0-9a-f]+)/user/([0-9a-f]+)/build/([0-9a-f-]+)/output/(.+)$ {
|
||||
if ($request_method = 'OPTIONS') {
|
||||
# handle OPTIONS method for CORS requests
|
||||
add_header 'Allow' 'GET,HEAD';
|
||||
return 204;
|
||||
}
|
||||
alias /output/$1-$2/generated-files/$3/output.$4;
|
||||
}
|
||||
# handle .blg files for specific users
|
||||
location ~ ^/project/([0-9a-f]+)/user/([0-9a-f]+)/build/([0-9a-f-]+)/output/(.+)\.blg$ {
|
||||
if ($request_method = 'OPTIONS') {
|
||||
# handle OPTIONS method for CORS requests
|
||||
add_header 'Allow' 'GET,HEAD';
|
||||
return 204;
|
||||
}
|
||||
alias /output/$1-$2/generated-files/$3/$4.blg;
|
||||
rewrite ^/project/([0-9a-f]+)/user/([0-9a-f]+)/build/([0-9a-f-]+)/output/(.+)$ /$4 break;
|
||||
root /output/$1-$2/generated-files/$3/;
|
||||
}
|
||||
# handle output files for anonymous users
|
||||
location ~ ^/project/([0-9a-f]+)/build/([0-9a-f-]+)/output/output\.([a-z.]+)$ {
|
||||
location ~ ^/project/([0-9a-f]+)/build/([0-9a-f-]+)/output/(.+)$ {
|
||||
if ($request_method = 'OPTIONS') {
|
||||
# handle OPTIONS method for CORS requests
|
||||
add_header 'Allow' 'GET,HEAD';
|
||||
return 204;
|
||||
}
|
||||
alias /output/$1/generated-files/$2/output.$3;
|
||||
rewrite ^/project/([0-9a-f]+)/build/([0-9a-f-]+)/output/(.+)$ /$3 break;
|
||||
root /output/$1/generated-files/$2/;
|
||||
}
|
||||
# handle .blg files for anonymous users
|
||||
location ~ ^/project/([0-9a-f]+)/build/([0-9a-f-]+)/output/(.+)\.blg$ {
|
||||
# handle output files for submissions
|
||||
location ~ ^/project/([a-z0-9_-]+)/build/([0-9a-f-]+)/output/(.+)$ {
|
||||
if ($request_method = 'OPTIONS') {
|
||||
# handle OPTIONS method for CORS requests
|
||||
add_header 'Allow' 'GET,HEAD';
|
||||
return 204;
|
||||
}
|
||||
alias /output/$1/generated-files/$2/$3.blg;
|
||||
rewrite ^/project/([a-z0-9_-]+)/build/([0-9a-f-]+)/output/(.+)$ /$3 break;
|
||||
root /output/$1/generated-files/$2/;
|
||||
}
|
||||
|
||||
# PDF range for specific users
|
||||
@@ -114,4 +104,9 @@ server {
|
||||
location = /instance-state {
|
||||
alias /var/clsi/instance-state;
|
||||
}
|
||||
|
||||
# Do not look up any non matching files in the default root.
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ describe('Example Documents', function () {
|
||||
(exampleDir =>
|
||||
describe(exampleDir, function () {
|
||||
before(function () {
|
||||
this.project_id = Client.randomId() + '_' + exampleDir
|
||||
this.project_id = Client.randomId()
|
||||
this.outputFiles = []
|
||||
// Allow each test to provide a configuration file
|
||||
const checksJsonPath = fixturePath(
|
||||
|
||||
@@ -7,7 +7,8 @@ import Settings from '@overleaf/settings'
|
||||
const host = Settings.apis.clsi.url
|
||||
|
||||
function randomId() {
|
||||
return Math.random().toString(16).slice(2)
|
||||
// Avoid ids starting with 0, which get a dummy PDF served.
|
||||
return 'a' + Math.random().toString(16).slice(2)
|
||||
}
|
||||
|
||||
function compile(projectId, data) {
|
||||
|
||||
@@ -144,7 +144,7 @@ describe('CompileController', () => {
|
||||
buildId: ctx.buildId,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: ctx.output_files.map(file => ({
|
||||
url: `${ctx.Settings.apis.clsi.url}/project/${ctx.project_id}/build/${file.build}/output/${file.path}`,
|
||||
url: `${ctx.Settings.apis.clsi.downloadHost}/project/${ctx.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
clsiCacheShard: undefined,
|
||||
@@ -172,7 +172,7 @@ describe('CompileController', () => {
|
||||
buildId: ctx.buildId,
|
||||
outputUrlPrefix: '',
|
||||
outputFiles: ctx.output_files.map(file => ({
|
||||
url: `${ctx.Settings.apis.clsi.url}/project/${ctx.project_id}/build/${file.build}/output/${file.path}`,
|
||||
url: `${ctx.Settings.apis.clsi.downloadHost}/project/${ctx.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
clsiCacheShard: undefined,
|
||||
@@ -220,7 +220,7 @@ describe('CompileController', () => {
|
||||
outputUrlPrefix: '/zone/b',
|
||||
buildId: ctx.buildId,
|
||||
outputFiles: ctx.output_files.map(file => ({
|
||||
url: `${ctx.Settings.apis.clsi.url}/project/${ctx.project_id}/build/${file.build}/output/${file.path}`,
|
||||
url: `${ctx.Settings.apis.clsi.downloadHost}/project/${ctx.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
clsiCacheShard: undefined,
|
||||
@@ -268,7 +268,7 @@ describe('CompileController', () => {
|
||||
timings: ctx.timings,
|
||||
outputUrlPrefix: '/zone/b',
|
||||
outputFiles: ctx.output_files.map(file => ({
|
||||
url: `${ctx.Settings.apis.clsi.url}/project/${ctx.project_id}/build/${file.build}/output/${file.path}`,
|
||||
url: `${ctx.Settings.apis.clsi.downloadHost}/project/${ctx.project_id}/build/${file.build}/output/${file.path}`,
|
||||
...file,
|
||||
})),
|
||||
clsiCacheShard: undefined,
|
||||
|
||||
@@ -118,18 +118,17 @@ const ClsiCookieManagerFactory = function (backendGroup) {
|
||||
) {
|
||||
let status
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
const url = new URL(Settings.apis.clsi.url)
|
||||
url.pathname = '/instance-state'
|
||||
url.search = new URLSearchParams({
|
||||
clsiserverid,
|
||||
compileGroup,
|
||||
compileBackendClass,
|
||||
}).toString()
|
||||
const { response, body } = await fetchStringWithResponse(
|
||||
`${Settings.apis.clsi.url}/instance-state?${params}`,
|
||||
{
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
}
|
||||
)
|
||||
const { response, body } = await fetchStringWithResponse(url.href, {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
})
|
||||
status =
|
||||
response.status === 200 && body === `${clsiserverid},UP\n`
|
||||
? 'load-shedding'
|
||||
|
||||
@@ -557,7 +557,8 @@ function _getCompilerUrl(
|
||||
userId,
|
||||
action
|
||||
) {
|
||||
const u = new URL(`/project/${projectId}`, Settings.apis.clsi.url)
|
||||
const u = new URL(Settings.apis.clsi.url)
|
||||
u.pathname = `/project/${projectId}`
|
||||
if (userId != null) {
|
||||
u.pathname += `/user/${userId}`
|
||||
}
|
||||
@@ -729,9 +730,8 @@ async function getOutputFileStream(
|
||||
outputFilePath
|
||||
) {
|
||||
const { compileBackendClass, compileGroup } = options
|
||||
const url = new URL(
|
||||
`${Settings.apis.clsi.url}/project/${projectId}/user/${userId}/build/${buildId}/output/${outputFilePath}`
|
||||
)
|
||||
const url = new URL(Settings.apis.clsi.downloadHost)
|
||||
url.pathname = `/project/${projectId}/user/${userId}/build/${buildId}/output/${outputFilePath}`
|
||||
url.searchParams.set('compileBackendClass', compileBackendClass)
|
||||
url.searchParams.set('compileGroup', compileGroup)
|
||||
url.searchParams.set('clsiserverid', clsiServerId)
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from '@overleaf/fetch-utils'
|
||||
import Features from '../../infrastructure/Features.mjs'
|
||||
import ClsiCacheController from './ClsiCacheController.mjs'
|
||||
import { prepareZipAttachment } from '../../infrastructure/Response.mjs'
|
||||
|
||||
const { z, zz, parseReq } = Validation
|
||||
const ClsiCookieManager = ClsiCookieManagerFactory(
|
||||
@@ -31,6 +32,8 @@ const ClsiCookieManager = ClsiCookieManagerFactory(
|
||||
|
||||
const COMPILE_TIMEOUT_MS = 10 * 60 * 1000
|
||||
|
||||
const buildIdSchema = z.string().regex(/[a-z0-9-]/)
|
||||
|
||||
const pdfDownloadRateLimiter = new RateLimiter('full-pdf-download', {
|
||||
points: 1000,
|
||||
duration: 60 * 60,
|
||||
@@ -409,6 +412,33 @@ const _CompileController = {
|
||||
)
|
||||
},
|
||||
|
||||
async getOutputZipFromClsi(req, res) {
|
||||
const projectId = req.params.Project_id
|
||||
const userId = CompileController._getUserIdForCompile(req)
|
||||
|
||||
const project = await ProjectGetter.promises.getProject(projectId, {
|
||||
name: 1,
|
||||
})
|
||||
const filename = `${_CompileController._getSafeProjectName(project)}-output.zip`
|
||||
prepareZipAttachment(res, filename)
|
||||
|
||||
const qs = {}
|
||||
const url = _CompileController._getFileUrl(
|
||||
projectId,
|
||||
userId,
|
||||
req.params.build_id,
|
||||
'output.zip'
|
||||
)
|
||||
await CompileController._proxyToClsi(
|
||||
projectId,
|
||||
'output-zip-file',
|
||||
url,
|
||||
qs,
|
||||
req,
|
||||
res
|
||||
)
|
||||
},
|
||||
|
||||
async getFileFromClsi(req, res) {
|
||||
const projectId = req.params.Project_id
|
||||
const userId = CompileController._getUserIdForCompile(req)
|
||||
@@ -465,6 +495,7 @@ const _CompileController = {
|
||||
} else if (userId != null) {
|
||||
url = `/project/${projectId}/user/${userId}/output/${file}`
|
||||
} else if (buildId != null) {
|
||||
buildId = buildIdSchema.parse(buildId)
|
||||
url = `/project/${projectId}/build/${buildId}/output/${file}`
|
||||
} else {
|
||||
url = `/project/${projectId}/output/${file}`
|
||||
@@ -523,7 +554,15 @@ const _CompileController = {
|
||||
)
|
||||
},
|
||||
|
||||
async _proxyToClsiWithLimits(projectId, action, url, qs, limits, req, res) {
|
||||
async _proxyToClsiWithLimits(
|
||||
projectId,
|
||||
action,
|
||||
requestPath,
|
||||
qs,
|
||||
limits,
|
||||
req,
|
||||
res
|
||||
) {
|
||||
const persistenceOptions = await _getPersistenceOptions(
|
||||
req,
|
||||
projectId,
|
||||
@@ -534,7 +573,12 @@ const _CompileController = {
|
||||
throw err
|
||||
})
|
||||
|
||||
url = new URL(`${Settings.apis.clsi.url}${url}`)
|
||||
const url = new URL(
|
||||
action === 'output-zip-file'
|
||||
? Settings.apis.clsi.url
|
||||
: Settings.apis.clsi.downloadHost
|
||||
)
|
||||
url.pathname = requestPath
|
||||
|
||||
const searchParams = {
|
||||
...persistenceOptions.qs,
|
||||
@@ -718,6 +762,7 @@ const CompileController = {
|
||||
downloadPdf: expressify(_CompileController.downloadPdf), //
|
||||
compileAndDownloadPdf: expressify(_CompileController.compileAndDownloadPdf),
|
||||
deleteAuxFiles: expressify(_CompileController.deleteAuxFiles),
|
||||
getOutputZipFromClsi: expressify(_CompileController.getOutputZipFromClsi),
|
||||
getFileFromClsi: expressify(_CompileController.getFileFromClsi),
|
||||
getFileFromClsiWithoutUser: expressify(
|
||||
_CompileController.getFileFromClsiWithoutUser
|
||||
|
||||
@@ -609,6 +609,20 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
||||
{ params: ['Project_id'] }
|
||||
)
|
||||
|
||||
// Download all the output files of a specific build
|
||||
webRouter.get(
|
||||
'/project/:Project_id/build/:build_id/output/output.zip',
|
||||
rateLimiterMiddlewareOutputFiles,
|
||||
AuthorizationMiddleware.ensureUserCanReadProject,
|
||||
CompileController.getOutputZipFromClsi
|
||||
)
|
||||
webRouter.get(
|
||||
'/project/:Project_id/user/:user_id/build/:build_id/output/output.zip',
|
||||
rateLimiterMiddlewareOutputFiles,
|
||||
AuthorizationMiddleware.ensureUserCanReadProject,
|
||||
CompileController.getOutputZipFromClsi
|
||||
)
|
||||
|
||||
// direct url access to output files for a specific build
|
||||
webRouter.get(
|
||||
/^\/project\/([^/]*)\/build\/([0-9a-f-]+)\/output\/(.*)$/,
|
||||
|
||||
@@ -237,7 +237,9 @@ module.exports = {
|
||||
},
|
||||
clsi: {
|
||||
url: `http://${process.env.CLSI_HOST || '127.0.0.1'}:3013`,
|
||||
// url: "http://#{process.env['CLSI_LB_HOST']}:3014"
|
||||
downloadHost: process.env.CLSI_LB_IP
|
||||
? `http://${process.env.CLSI_LB_IP}:80`
|
||||
: `http://${process.env.DOWNLOAD_HOST || '127.0.0.1'}:8080`,
|
||||
backendGroupName: undefined,
|
||||
submissionBackendClass:
|
||||
process.env.CLSI_SUBMISSION_BACKEND_CLASS || 'c3d',
|
||||
|
||||
@@ -67,6 +67,7 @@ module.exports = {
|
||||
},
|
||||
clsi: {
|
||||
url: 'http://127.0.0.1:23013',
|
||||
downloadHost: 'http://127.0.0.1:23080',
|
||||
},
|
||||
realTime: {
|
||||
url: 'http://127.0.0.1:23026',
|
||||
|
||||
@@ -4,6 +4,7 @@ import Features from '../../../app/src/infrastructure/Features.mjs'
|
||||
import MockAnalyticsApi from './mocks/MockAnalyticsApi.mjs'
|
||||
import MockChatApi from './mocks/MockChatApi.mjs'
|
||||
import MockClsiApi from './mocks/MockClsiApi.mjs'
|
||||
import MockClsiNginxApi from './mocks/MockClsiNginxApi.mjs'
|
||||
import MockDocstoreApi from './mocks/MockDocstoreApi.mjs'
|
||||
import MockDocUpdaterApi from './mocks/MockDocUpdaterApi.mjs'
|
||||
import MockGitBridgeApi from './mocks/MockGitBridgeApi.mjs'
|
||||
@@ -22,6 +23,7 @@ const mockOpts = {
|
||||
|
||||
MockChatApi.initialize(23010, mockOpts)
|
||||
MockClsiApi.initialize(23013, mockOpts)
|
||||
MockClsiNginxApi.initialize(23080, mockOpts)
|
||||
MockDocstoreApi.initialize(23016, mockOpts)
|
||||
MockDocUpdaterApi.initialize(23003, mockOpts)
|
||||
MockNotificationsApi.initialize(23042, mockOpts)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import AbstractMockApi from './AbstractMockApi.mjs'
|
||||
import { plainTextResponse } from '../../../../app/src/infrastructure/Response.mjs'
|
||||
|
||||
class MockClsiApi extends AbstractMockApi {
|
||||
static compile(req, res) {
|
||||
@@ -9,13 +8,13 @@ class MockClsiApi extends AbstractMockApi {
|
||||
error: null,
|
||||
outputFiles: [
|
||||
{
|
||||
url: `http://clsi:3013/project/${req.params.project_id}/build/1234/output/output.pdf`,
|
||||
url: `http://clsi:8080/project/${req.params.project_id}/build/1234/output/output.pdf`,
|
||||
path: 'output.pdf',
|
||||
type: 'pdf',
|
||||
build: 1234,
|
||||
},
|
||||
{
|
||||
url: `http://clsi:3013/project/${req.params.project_id}/build/1234/output/output.log`,
|
||||
url: `http://clsi:8080/project/${req.params.project_id}/build/1234/output/output.log`,
|
||||
path: 'output.log',
|
||||
type: 'log',
|
||||
build: 1234,
|
||||
@@ -32,27 +31,6 @@ class MockClsiApi extends AbstractMockApi {
|
||||
MockClsiApi.compile
|
||||
)
|
||||
|
||||
this.app.get(
|
||||
'/project/:project_id/build/:build_id/output/*',
|
||||
(req, res) => {
|
||||
const filename = req.params[0]
|
||||
if (filename === 'output.pdf') {
|
||||
plainTextResponse(res, 'mock-pdf')
|
||||
} else if (filename === 'output.log') {
|
||||
plainTextResponse(res, 'mock-log')
|
||||
} else {
|
||||
res.sendStatus(404)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.app.get(
|
||||
'/project/:project_id/user/:user_id/build/:build_id/output/:output_path',
|
||||
(req, res) => {
|
||||
plainTextResponse(res, 'hello')
|
||||
}
|
||||
)
|
||||
|
||||
this.app.get('/project/:project_id/status', (req, res) => {
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import AbstractMockApi from './AbstractMockApi.mjs'
|
||||
import { plainTextResponse } from '../../../../app/src/infrastructure/Response.mjs'
|
||||
|
||||
class MockClsiNginxApi extends AbstractMockApi {
|
||||
applyRoutes() {
|
||||
this.app.get(
|
||||
'/project/:project_id/build/:build_id/output/*',
|
||||
(req, res) => {
|
||||
const filename = req.params[0]
|
||||
if (filename === 'output.pdf') {
|
||||
plainTextResponse(res, 'mock-pdf')
|
||||
} else if (filename === 'output.log') {
|
||||
plainTextResponse(res, 'mock-log')
|
||||
} else {
|
||||
res.sendStatus(404)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.app.get(
|
||||
'/project/:project_id/user/:user_id/build/:build_id/output/:output_path',
|
||||
(req, res) => {
|
||||
plainTextResponse(res, 'hello')
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MockClsiNginxApi
|
||||
|
||||
// type hint for the inherited `instance` method
|
||||
/**
|
||||
* @function instance
|
||||
* @memberOf MockClsiNginxApi
|
||||
* @static
|
||||
* @returns {MockClsiNginxApi}
|
||||
*/
|
||||
@@ -42,7 +42,8 @@ describe('CompileController', function () {
|
||||
ctx.settings = {
|
||||
apis: {
|
||||
clsi: {
|
||||
url: 'http://clsi.example.com',
|
||||
url: 'http://clsi.example.com:3013',
|
||||
downloadHost: 'http://clsi.example.com:8080',
|
||||
submissionBackendClass: 'c3d',
|
||||
},
|
||||
clsi_priority: {
|
||||
@@ -776,7 +777,7 @@ describe('CompileController', function () {
|
||||
|
||||
it('should open a request to the CLSI', function (ctx) {
|
||||
ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
||||
`${ctx.settings.apis.clsi.url}${ctx.url}?compileGroup=standard&compileBackendClass=c3d&query=foo`
|
||||
`${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=standard&compileBackendClass=c3d&query=foo`
|
||||
)
|
||||
})
|
||||
|
||||
@@ -806,7 +807,7 @@ describe('CompileController', function () {
|
||||
|
||||
it('should open a request to the CLSI', function (ctx) {
|
||||
ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
||||
`${ctx.settings.apis.clsi.url}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`
|
||||
`${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -826,7 +827,7 @@ describe('CompileController', function () {
|
||||
})
|
||||
ctx.fetchUtils.fetchStreamWithResponse.rejects(
|
||||
new RequestFailedError(
|
||||
`${ctx.settings.apis.clsi.url}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`,
|
||||
`${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`,
|
||||
{ method: 'GET' },
|
||||
{ status: 404 }
|
||||
)
|
||||
@@ -844,7 +845,7 @@ describe('CompileController', function () {
|
||||
|
||||
it('should open a request to the CLSI', function (ctx) {
|
||||
ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
||||
`${ctx.settings.apis.clsi.url}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`
|
||||
`${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`
|
||||
)
|
||||
})
|
||||
|
||||
@@ -873,7 +874,7 @@ describe('CompileController', function () {
|
||||
})
|
||||
ctx.fetchUtils.fetchStreamWithResponse.rejects(
|
||||
new RequestFailedError(
|
||||
`${ctx.settings.apis.clsi.url}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`,
|
||||
`${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`,
|
||||
{ method: 'GET' },
|
||||
{ status: 404 }
|
||||
)
|
||||
@@ -891,7 +892,7 @@ describe('CompileController', function () {
|
||||
|
||||
it('should open a request to the CLSI', function (ctx) {
|
||||
ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
||||
`${ctx.settings.apis.clsi.url}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`
|
||||
`${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=priority&compileBackendClass=c4d`
|
||||
)
|
||||
})
|
||||
|
||||
@@ -924,7 +925,7 @@ describe('CompileController', function () {
|
||||
|
||||
it('should open a request to the CLSI', function (ctx) {
|
||||
ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
||||
`${ctx.settings.apis.clsi.url}${ctx.url}?compileGroup=standard&compileBackendClass=c3d`
|
||||
`${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=standard&compileBackendClass=c3d`
|
||||
)
|
||||
})
|
||||
|
||||
@@ -955,7 +956,7 @@ describe('CompileController', function () {
|
||||
|
||||
it('should proxy to the standard url', function (ctx) {
|
||||
ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
||||
`${ctx.settings.apis.clsi.url}${ctx.url}?compileGroup=standard&compileBackendClass=c3d`
|
||||
`${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=standard&compileBackendClass=c3d`
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -982,7 +983,7 @@ describe('CompileController', function () {
|
||||
|
||||
it('should proxy to the standard url without the build parameter', function (ctx) {
|
||||
ctx.fetchUtils.fetchStreamWithResponse.should.have.been.calledWith(
|
||||
`${ctx.settings.apis.clsi.url}${ctx.url}?compileGroup=standard&compileBackendClass=c3d`
|
||||
`${ctx.settings.apis.clsi.downloadHost}${ctx.url}?compileGroup=standard&compileBackendClass=c3d`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user