texlite 0.7.3 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -9
- package/README.zh-CN.md +9 -9
- package/dist/client/assets/{GitDialog-BmRzMv-C.js → GitDialog-W3hLOWrF.js} +2 -2
- package/dist/client/assets/HistoryDialog-7ADYJcGi.js +5 -0
- package/dist/client/assets/LatexEditor-ChHQwJXZ.js +59 -0
- package/dist/client/assets/PdfPreview-CuvWphzY.js +1 -0
- package/dist/client/assets/{ProjectNavigationDialogs-BVQb4qdD.js → ProjectNavigationDialogs-CzFhIw6B.js} +1 -1
- package/dist/client/assets/SystemMetricsDialog-C0m9JUDy.js +1 -0
- package/dist/client/assets/index-DRShM2ob.js +64 -0
- package/dist/client/assets/index-DzcH7hBu.css +1 -0
- package/dist/client/assets/latexFormatterWorker-DYkYtcFy.js +6 -0
- package/dist/client/assets/minus-BfqMIeUg.js +1 -0
- package/dist/client/assets/pdf-BB1XvBIF.js +55 -0
- package/dist/client/assets/{react-CsO_xvbl.js → react-BoEjKzFZ.js} +1 -1
- package/dist/client/assets/spellCheck-BBnCnxmQ.js +4 -0
- package/dist/client/assets/tex_fmt_bg-VEAUIuFz.wasm +0 -0
- package/dist/client/index.html +3 -3
- package/dist/server/anchors.js +22 -1
- package/dist/server/app.js +103 -2375
- package/dist/server/auth.js +3 -2
- package/dist/server/cli.js +8 -2
- package/dist/server/collaboration.js +250 -2
- package/dist/server/compileArtifacts.js +10 -7
- package/dist/server/compiler.js +9 -4
- package/dist/server/config.js +25 -1
- package/dist/server/environment.js +5 -1
- package/dist/server/files.js +7 -6
- package/dist/server/git.js +28 -39
- package/dist/server/harper.js +144 -0
- package/dist/server/harperRuntime.js +38 -0
- package/dist/server/harperWorker.js +15 -0
- package/dist/server/history.js +11 -9
- package/dist/server/http.js +33 -13
- package/dist/server/i18n.js +276 -0
- package/dist/server/latexRoot.js +78 -0
- package/dist/server/limits.js +9 -0
- package/dist/server/projectSearch.js +3 -4
- package/dist/server/routes/auth.js +77 -0
- package/dist/server/routes/citations.js +331 -0
- package/dist/server/routes/collaboration.js +17 -0
- package/dist/server/routes/comments.js +176 -0
- package/dist/server/routes/compile.js +71 -50
- package/dist/server/routes/projectFiles.js +572 -0
- package/dist/server/routes/projectGit.js +145 -0
- package/dist/server/routes/projectHistory.js +150 -0
- package/dist/server/routes/projectMembers.js +112 -0
- package/dist/server/routes/projectShared.js +238 -0
- package/dist/server/routes/projects.js +473 -0
- package/dist/server/routes/system.js +37 -0
- package/dist/server/routes/users.js +163 -0
- package/dist/server/security.js +2 -4
- package/dist/server/synctex.js +7 -4
- package/dist/server/zip.js +29 -13
- package/package.json +4 -2
- package/texlite.config.example.json +5 -1
- package/dist/client/assets/BinaryModule-Aj1vLnwf-_qIOaefD.js +0 -4
- package/dist/client/assets/HistoryDialog-V0IkSDtJ.js +0 -1
- package/dist/client/assets/LatexEditor-CokkLLxi.js +0 -59
- package/dist/client/assets/PdfPreview-Cs1WYPsC.js +0 -1
- package/dist/client/assets/SystemMetricsDialog-DBrendnp.js +0 -1
- package/dist/client/assets/__vite-browser-external-BIHI7g3E.js +0 -1
- package/dist/client/assets/binary-BQKEW09b.js +0 -1
- package/dist/client/assets/harper_wasm_bg-LWXDb7Ll.wasm +0 -0
- package/dist/client/assets/index-BS1IMOaB.js +0 -62
- package/dist/client/assets/index-Lw53KTaT.css +0 -1
- package/dist/client/assets/index-QyVe7I1s.js +0 -3456
- package/dist/client/assets/index-rBWMBTI-.js +0 -5
- package/dist/client/assets/minus-yEV4SLvG.js +0 -1
- package/dist/client/assets/pdf-DPnwNQHb.js +0 -55
- package/dist/client/assets/spellCheck-OZGa6ljx.js +0 -5
- package/dist/server/latexFormatter.js +0 -118
|
@@ -8,19 +8,34 @@ import { resolveSourcePath, safeRelativePath, texFileStem } from "../files.js";
|
|
|
8
8
|
import { captureCompileSnapshot, cleanCompileArtifacts, cleanCompileCache, compileProject, discardCompileSnapshot, hasCompileCache, publishCompileArtifacts, publishedCompileArtifacts } from "../compiler.js";
|
|
9
9
|
import { parseCompileDiagnostics } from "../compileDiagnostics.js";
|
|
10
10
|
import { pdfToSource, sourceToPdf } from "../synctex.js";
|
|
11
|
-
import { availablePdf, compileRunPdf, compileMainFile, compilePdfUrl, isTextCompileArtifact, listCompileArtifacts, syncArtifacts } from "../compileArtifacts.js";
|
|
12
|
-
import {
|
|
11
|
+
import { availablePdf, compileRunPdf, compileMainFile, compilePdfUrl, isTextCompileArtifact, listCompileArtifacts, pdfLoadingMode, syncArtifacts } from "../compileArtifacts.js";
|
|
12
|
+
import { MAX_TEXT_PREVIEW_BYTES } from "../limits.js";
|
|
13
|
+
import { apiError, contentDisposition, httpError } from "../http.js";
|
|
13
14
|
const now = () => new Date().toISOString();
|
|
14
15
|
const timingDuration = (milliseconds) => Math.round(milliseconds * 10) / 10;
|
|
15
16
|
const MAX_SNAPSHOT_ATTEMPTS = 3;
|
|
16
17
|
const SNAPSHOT_RETRY_AFTER_SECONDS = 1;
|
|
18
|
+
function pdfResponseMetadata(config, pdfPath) {
|
|
19
|
+
if (!pdfPath)
|
|
20
|
+
return { pdfSizeBytes: null, pdfLoadingMode: null };
|
|
21
|
+
const stat = regularFileStat(pdfPath);
|
|
22
|
+
if (!stat)
|
|
23
|
+
return {
|
|
24
|
+
pdfSizeBytes: null,
|
|
25
|
+
pdfLoadingMode: config.pdfLoadingStrategy === "range" ? "range" : "full"
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
pdfSizeBytes: stat.size,
|
|
29
|
+
pdfLoadingMode: pdfLoadingMode(config, stat.size)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
17
32
|
function mainDocumentChangedError() {
|
|
18
|
-
return
|
|
33
|
+
return httpError(409, "MAIN_DOCUMENT_CHANGED");
|
|
19
34
|
}
|
|
20
35
|
class CompileSnapshotBusyError extends Error {
|
|
21
36
|
code = "COMPILE_SNAPSHOT_BUSY";
|
|
22
37
|
constructor() {
|
|
23
|
-
super("
|
|
38
|
+
super("COMPILE_SNAPSHOT_BUSY");
|
|
24
39
|
this.name = "CompileSnapshotBusyError";
|
|
25
40
|
}
|
|
26
41
|
}
|
|
@@ -33,11 +48,11 @@ export function registerCompileRoutes(app, context) {
|
|
|
33
48
|
const { id } = request.params;
|
|
34
49
|
const project = accessibleProject(db, id, user);
|
|
35
50
|
if (!project)
|
|
36
|
-
return apiError(reply, 404, "PROJECT_NOT_FOUND"
|
|
51
|
+
return apiError(reply, 404, "PROJECT_NOT_FOUND");
|
|
37
52
|
const query = request.query;
|
|
38
53
|
const mainFile = compileMainFile(config, id, project.main_file, query.mainFile);
|
|
39
54
|
if (!mainFile)
|
|
40
|
-
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID"
|
|
55
|
+
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID");
|
|
41
56
|
const latest = db.prepare(`SELECT run.id, run.status, run.log, run.created_at, run.finished_at,
|
|
42
57
|
run.requested_by, user.username AS requested_by_username, user.display_name AS requested_by_name
|
|
43
58
|
FROM compile_runs run LEFT JOIN users user ON user.id = run.requested_by
|
|
@@ -52,6 +67,7 @@ export function registerCompileRoutes(app, context) {
|
|
|
52
67
|
// A retained legacy PDF has no run bundle, so its file version (mtime) is
|
|
53
68
|
// the only stable token that can be resolved back to those bytes.
|
|
54
69
|
const pdfVersion = published?.runId ?? pdf?.version;
|
|
70
|
+
const pdfMetadata = pdfResponseMetadata(config, pdf?.path ?? null);
|
|
55
71
|
return {
|
|
56
72
|
mainFile,
|
|
57
73
|
latestRun: latest ? {
|
|
@@ -66,7 +82,8 @@ export function registerCompileRoutes(app, context) {
|
|
|
66
82
|
} : null,
|
|
67
83
|
hasPdf: Boolean(pdf),
|
|
68
84
|
pdfUrl: pdf ? compilePdfUrl(id, mainFile, pdfVersion ?? pdf.version) : null,
|
|
69
|
-
pdfCompiledAt: publishedRun?.finished_at ?? latestSuccess?.finished_at ?? null
|
|
85
|
+
pdfCompiledAt: publishedRun?.finished_at ?? latestSuccess?.finished_at ?? null,
|
|
86
|
+
...pdfMetadata
|
|
70
87
|
};
|
|
71
88
|
});
|
|
72
89
|
app.get("/api/projects/:id/compile/artifacts", async (request, reply) => {
|
|
@@ -76,11 +93,11 @@ export function registerCompileRoutes(app, context) {
|
|
|
76
93
|
const { id } = request.params;
|
|
77
94
|
const project = accessibleProject(db, id, user);
|
|
78
95
|
if (!project)
|
|
79
|
-
return apiError(reply, 404, "PROJECT_NOT_FOUND"
|
|
96
|
+
return apiError(reply, 404, "PROJECT_NOT_FOUND");
|
|
80
97
|
const query = request.query;
|
|
81
98
|
const mainFile = compileMainFile(config, id, project.main_file, query.mainFile);
|
|
82
99
|
if (!mainFile)
|
|
83
|
-
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID"
|
|
100
|
+
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID");
|
|
84
101
|
const published = publishedCompileArtifacts(config, id, mainFile, mainFile === project.main_file);
|
|
85
102
|
if (!query.path) {
|
|
86
103
|
if (!published)
|
|
@@ -98,23 +115,23 @@ export function registerCompileRoutes(app, context) {
|
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
117
|
if (!published)
|
|
101
|
-
return apiError(reply, 404, "COMPILE_ARTIFACTS_NOT_FOUND"
|
|
118
|
+
return apiError(reply, 404, "COMPILE_ARTIFACTS_NOT_FOUND");
|
|
102
119
|
const relative = safeRelativePath(query.path);
|
|
103
120
|
const absolute = path.join(published.output, relative);
|
|
104
121
|
const outputDirectory = path.resolve(published.output);
|
|
105
122
|
const resolved = path.resolve(absolute);
|
|
106
123
|
if (!resolved.startsWith(`${outputDirectory}${path.sep}`)) {
|
|
107
|
-
return apiError(reply, 404, "COMPILE_ARTIFACT_NOT_FOUND"
|
|
124
|
+
return apiError(reply, 404, "COMPILE_ARTIFACT_NOT_FOUND");
|
|
108
125
|
}
|
|
109
126
|
let stat;
|
|
110
127
|
try {
|
|
111
128
|
stat = fs.statSync(resolved);
|
|
112
129
|
if (!stat.isFile())
|
|
113
|
-
return apiError(reply, 404, "COMPILE_ARTIFACT_NOT_FOUND"
|
|
130
|
+
return apiError(reply, 404, "COMPILE_ARTIFACT_NOT_FOUND");
|
|
114
131
|
}
|
|
115
132
|
catch (error) {
|
|
116
133
|
if (isMissingFileError(error))
|
|
117
|
-
return apiError(reply, 404, "COMPILE_ARTIFACT_NOT_FOUND"
|
|
134
|
+
return apiError(reply, 404, "COMPILE_ARTIFACT_NOT_FOUND");
|
|
118
135
|
throw error;
|
|
119
136
|
}
|
|
120
137
|
if (query.download === "1") {
|
|
@@ -127,7 +144,7 @@ export function registerCompileRoutes(app, context) {
|
|
|
127
144
|
catch (error) {
|
|
128
145
|
await fs.promises.rm(temporaryFile, { force: true }).catch(() => undefined);
|
|
129
146
|
if (isMissingFileError(error))
|
|
130
|
-
return apiError(reply, 404, "COMPILE_ARTIFACT_NOT_FOUND"
|
|
147
|
+
return apiError(reply, 404, "COMPILE_ARTIFACT_NOT_FOUND");
|
|
131
148
|
throw error;
|
|
132
149
|
}
|
|
133
150
|
reply.header("Content-Type", "application/octet-stream");
|
|
@@ -139,15 +156,15 @@ export function registerCompileRoutes(app, context) {
|
|
|
139
156
|
stream.once("error", cleanup);
|
|
140
157
|
return reply.send(stream);
|
|
141
158
|
}
|
|
142
|
-
if (stat.size >
|
|
143
|
-
return apiError(reply, 415, "ARTIFACT_PREVIEW_UNSUPPORTED"
|
|
159
|
+
if (stat.size > MAX_TEXT_PREVIEW_BYTES || !isTextCompileArtifact(relative)) {
|
|
160
|
+
return apiError(reply, 415, "ARTIFACT_PREVIEW_UNSUPPORTED");
|
|
144
161
|
}
|
|
145
162
|
try {
|
|
146
163
|
return { path: relative, content: fs.readFileSync(resolved, "utf8") };
|
|
147
164
|
}
|
|
148
165
|
catch (error) {
|
|
149
166
|
if (isMissingFileError(error))
|
|
150
|
-
return apiError(reply, 404, "COMPILE_ARTIFACT_NOT_FOUND"
|
|
167
|
+
return apiError(reply, 404, "COMPILE_ARTIFACT_NOT_FOUND");
|
|
151
168
|
throw error;
|
|
152
169
|
}
|
|
153
170
|
});
|
|
@@ -158,19 +175,19 @@ export function registerCompileRoutes(app, context) {
|
|
|
158
175
|
const { id } = request.params;
|
|
159
176
|
const project = accessibleProject(db, id, user);
|
|
160
177
|
if (!project || !canEdit(project))
|
|
161
|
-
return apiError(reply, 403, "COMPILE_FORBIDDEN"
|
|
178
|
+
return apiError(reply, 403, "COMPILE_FORBIDDEN");
|
|
162
179
|
const body = (request.body ?? {});
|
|
163
180
|
const mainFile = compileMainFile(config, id, project.main_file, body.mainFile);
|
|
164
181
|
if (!mainFile)
|
|
165
|
-
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID"
|
|
182
|
+
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID");
|
|
166
183
|
if (body.mode !== "cache" && body.mode !== "artifacts") {
|
|
167
|
-
return apiError(reply, 400, "REQUEST_INVALID"
|
|
184
|
+
return apiError(reply, 400, "REQUEST_INVALID");
|
|
168
185
|
}
|
|
169
186
|
return await projectMutations.runCompileExclusive(id, () => {
|
|
170
187
|
const activeRun = db.prepare(`SELECT 1 AS active FROM compile_runs
|
|
171
188
|
WHERE project_id = ? AND main_file = ? AND status IN ('queued', 'running') LIMIT 1`).get(id, mainFile);
|
|
172
189
|
if (activeRun)
|
|
173
|
-
return apiError(reply, 409, "COMPILE_CLEAN_BUSY"
|
|
190
|
+
return apiError(reply, 409, "COMPILE_CLEAN_BUSY");
|
|
174
191
|
const requestedBy = { id: user.id, username: user.username, name: user.display_name };
|
|
175
192
|
if (body.mode === "cache") {
|
|
176
193
|
cleanCompileCache(config, id, mainFile);
|
|
@@ -186,7 +203,7 @@ export function registerCompileRoutes(app, context) {
|
|
|
186
203
|
}
|
|
187
204
|
const currentProject = accessibleProject(db, id, user);
|
|
188
205
|
if (!currentProject || !canEdit(currentProject))
|
|
189
|
-
return apiError(reply, 403, "COMPILE_FORBIDDEN"
|
|
206
|
+
return apiError(reply, 403, "COMPILE_FORBIDDEN");
|
|
190
207
|
const runs = db.prepare(`SELECT id FROM compile_runs
|
|
191
208
|
WHERE project_id = ? AND main_file = ? AND status NOT IN ('queued', 'running')`).all(id, mainFile);
|
|
192
209
|
cleanCompileArtifacts(config, id, mainFile, currentProject.main_file, runs.map((run) => run.id));
|
|
@@ -204,10 +221,10 @@ export function registerCompileRoutes(app, context) {
|
|
|
204
221
|
}, { preflight: () => {
|
|
205
222
|
const current = accessibleProject(db, id, user);
|
|
206
223
|
if (!current || !canEdit(current))
|
|
207
|
-
throw
|
|
224
|
+
throw httpError(403, "COMPILE_FORBIDDEN");
|
|
208
225
|
const selected = compileMainFile(config, id, current.main_file, body.mainFile);
|
|
209
226
|
if (!selected || selected !== mainFile)
|
|
210
|
-
throw
|
|
227
|
+
throw httpError(400, "MAIN_DOCUMENT_INVALID");
|
|
211
228
|
} });
|
|
212
229
|
});
|
|
213
230
|
app.get("/api/projects/:id/sync/pdf", async (request, reply) => {
|
|
@@ -217,28 +234,28 @@ export function registerCompileRoutes(app, context) {
|
|
|
217
234
|
const { id } = request.params;
|
|
218
235
|
const project = accessibleProject(db, id, user);
|
|
219
236
|
if (!project)
|
|
220
|
-
return apiError(reply, 404, "PROJECT_NOT_FOUND"
|
|
237
|
+
return apiError(reply, 404, "PROJECT_NOT_FOUND");
|
|
221
238
|
const query = request.query;
|
|
222
239
|
const mainFile = compileMainFile(config, id, project.main_file, query.mainFile);
|
|
223
240
|
if (!mainFile)
|
|
224
|
-
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID"
|
|
241
|
+
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID");
|
|
225
242
|
const sourcePath = safeRelativePath(query.path ?? "");
|
|
226
243
|
const line = Number(query.line);
|
|
227
244
|
const column = Number(query.column ?? 1);
|
|
228
245
|
if (!Number.isInteger(line) || line < 1 || !Number.isInteger(column) || column < 1) {
|
|
229
|
-
return apiError(reply, 400, "SYNC_SOURCE_INVALID"
|
|
246
|
+
return apiError(reply, 400, "SYNC_SOURCE_INVALID");
|
|
230
247
|
}
|
|
231
248
|
return await projectMutations.runConsistentRead(id, async () => {
|
|
232
249
|
if (!fs.existsSync(resolveSourcePath(config, id, sourcePath))) {
|
|
233
|
-
return apiError(reply, 404, "SOURCE_FILE_NOT_FOUND"
|
|
250
|
+
return apiError(reply, 404, "SOURCE_FILE_NOT_FOUND");
|
|
234
251
|
}
|
|
235
252
|
const artifacts = syncArtifacts(config, id, mainFile, project.main_file);
|
|
236
253
|
if (!artifacts)
|
|
237
|
-
return apiError(reply, 409, "SYNCTEX_NOT_AVAILABLE"
|
|
254
|
+
return apiError(reply, 409, "SYNCTEX_NOT_AVAILABLE");
|
|
238
255
|
return await sourceToPdf(artifacts.source, artifacts.pdf, sourcePath, line, column);
|
|
239
256
|
}, { preflight: () => {
|
|
240
257
|
if (!accessibleProject(db, id, user))
|
|
241
|
-
throw
|
|
258
|
+
throw httpError(404, "PROJECT_NOT_FOUND");
|
|
242
259
|
} });
|
|
243
260
|
});
|
|
244
261
|
app.get("/api/projects/:id/sync/source", async (request, reply) => {
|
|
@@ -248,31 +265,31 @@ export function registerCompileRoutes(app, context) {
|
|
|
248
265
|
const { id } = request.params;
|
|
249
266
|
const project = accessibleProject(db, id, user);
|
|
250
267
|
if (!project)
|
|
251
|
-
return apiError(reply, 404, "PROJECT_NOT_FOUND"
|
|
268
|
+
return apiError(reply, 404, "PROJECT_NOT_FOUND");
|
|
252
269
|
const query = request.query;
|
|
253
270
|
const mainFile = compileMainFile(config, id, project.main_file, query.mainFile);
|
|
254
271
|
if (!mainFile)
|
|
255
|
-
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID"
|
|
272
|
+
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID");
|
|
256
273
|
const page = Number(query.page);
|
|
257
274
|
const x = Number(query.x);
|
|
258
275
|
const y = Number(query.y);
|
|
259
276
|
if (!Number.isInteger(page) || page < 1 || !Number.isFinite(x) || x < 0 || !Number.isFinite(y) || y < 0) {
|
|
260
|
-
return apiError(reply, 400, "SYNC_PDF_INVALID"
|
|
277
|
+
return apiError(reply, 400, "SYNC_PDF_INVALID");
|
|
261
278
|
}
|
|
262
279
|
return await projectMutations.runConsistentRead(id, async () => {
|
|
263
280
|
const artifacts = syncArtifacts(config, id, mainFile, project.main_file);
|
|
264
281
|
if (!artifacts)
|
|
265
|
-
return apiError(reply, 409, "SYNCTEX_NOT_AVAILABLE"
|
|
282
|
+
return apiError(reply, 409, "SYNCTEX_NOT_AVAILABLE");
|
|
266
283
|
const location = await pdfToSource(artifacts.source, artifacts.pdf, page, x, y);
|
|
267
284
|
const sourceDirectory = path.resolve(artifacts.source);
|
|
268
285
|
const relative = path.relative(sourceDirectory, path.resolve(location.input));
|
|
269
286
|
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
270
|
-
return apiError(reply, 400, "SYNCTEX_EXTERNAL_PATH"
|
|
287
|
+
return apiError(reply, 400, "SYNCTEX_EXTERNAL_PATH");
|
|
271
288
|
}
|
|
272
289
|
return { path: safeRelativePath(relative.replaceAll(path.sep, "/")), line: location.line, column: location.column };
|
|
273
290
|
}, { preflight: () => {
|
|
274
291
|
if (!accessibleProject(db, id, user))
|
|
275
|
-
throw
|
|
292
|
+
throw httpError(404, "PROJECT_NOT_FOUND");
|
|
276
293
|
} });
|
|
277
294
|
});
|
|
278
295
|
app.post("/api/projects/:id/compile", async (request, reply) => {
|
|
@@ -283,12 +300,12 @@ export function registerCompileRoutes(app, context) {
|
|
|
283
300
|
const { id } = request.params;
|
|
284
301
|
const initialProject = accessibleProject(db, id, user);
|
|
285
302
|
if (!initialProject || !canEdit(initialProject))
|
|
286
|
-
return apiError(reply, 403, "COMPILE_FORBIDDEN"
|
|
303
|
+
return apiError(reply, 403, "COMPILE_FORBIDDEN");
|
|
287
304
|
const body = (request.body ?? {});
|
|
288
305
|
const requestedMainFile = typeof body.mainFile === "string" && body.mainFile ? body.mainFile : null;
|
|
289
306
|
const initialMainFile = compileMainFile(config, id, initialProject.main_file, body.mainFile);
|
|
290
307
|
if (!initialMainFile)
|
|
291
|
-
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID"
|
|
308
|
+
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID");
|
|
292
309
|
const runId = randomUUID();
|
|
293
310
|
// Admission must stay cheap. In particular, do not call
|
|
294
311
|
// captureCompileSnapshot here: several collaborators can reach this
|
|
@@ -537,8 +554,9 @@ export function registerCompileRoutes(app, context) {
|
|
|
537
554
|
// archive/read or another output mutation. The long latexmk
|
|
538
555
|
// process above remains outside that queue.
|
|
539
556
|
await projectMutations.runSerialized(id, () => {
|
|
540
|
-
if (!db.prepare("SELECT 1 FROM projects WHERE id = ?").get(id))
|
|
541
|
-
throw new Error("
|
|
557
|
+
if (!db.prepare("SELECT 1 FROM projects WHERE id = ?").get(id)) {
|
|
558
|
+
throw new Error("Project was deleted before compile artifacts could be published.");
|
|
559
|
+
}
|
|
542
560
|
publishCompileArtifacts(config, id, snapshot, compiled);
|
|
543
561
|
}, { flush: false });
|
|
544
562
|
refreshSnapshotStale();
|
|
@@ -620,7 +638,7 @@ export function registerCompileRoutes(app, context) {
|
|
|
620
638
|
`snapshot;dur=${timingDuration(result.snapshotMs ?? 0)}`,
|
|
621
639
|
`total;dur=${timingDuration(requestMs)}`
|
|
622
640
|
].join(", "));
|
|
623
|
-
return apiError(reply, 409, result.errorCode ?? "COMPILE_SNAPSHOT_BUSY",
|
|
641
|
+
return apiError(reply, 409, result.errorCode ?? "COMPILE_SNAPSHOT_BUSY", { retryable: true, retryAfterSeconds: SNAPSHOT_RETRY_AFTER_SECONDS });
|
|
624
642
|
}
|
|
625
643
|
if (result.timings) {
|
|
626
644
|
metrics.record("compile.cacheSync", result.timings.cacheSyncMs);
|
|
@@ -631,6 +649,8 @@ export function registerCompileRoutes(app, context) {
|
|
|
631
649
|
const timings = result.timings
|
|
632
650
|
? { snapshotMs: result.snapshotMs ?? 0, ...result.timings, requestMs }
|
|
633
651
|
: { snapshotMs: result.snapshotMs ?? 0, requestMs };
|
|
652
|
+
const responsePdf = result.ok ? availablePdf(config, id, mainFile, project.main_file) : null;
|
|
653
|
+
const pdfMetadata = pdfResponseMetadata(config, responsePdf?.path ?? null);
|
|
634
654
|
reply.header("Server-Timing", [
|
|
635
655
|
`snapshot;dur=${timingDuration(result.snapshotMs ?? 0)}`,
|
|
636
656
|
result.timings ? `cache;dur=${timingDuration(result.timings.cacheSyncMs)}` : "",
|
|
@@ -643,6 +663,7 @@ export function registerCompileRoutes(app, context) {
|
|
|
643
663
|
mainFile, runId: result.runId, ok: result.ok, skipped: result.skipped === true, log: result.log, diagnostics: result.diagnostics,
|
|
644
664
|
pdfUrl: result.ok ? compilePdfUrl(id, mainFile, result.runId) : null,
|
|
645
665
|
pdfCompiledAt: result.ok ? completed?.finished_at ?? null : null,
|
|
666
|
+
...pdfMetadata,
|
|
646
667
|
timings
|
|
647
668
|
};
|
|
648
669
|
});
|
|
@@ -658,36 +679,36 @@ export function registerCompileRoutes(app, context) {
|
|
|
658
679
|
const { id } = request.params;
|
|
659
680
|
const project = accessibleProject(db, id, user);
|
|
660
681
|
if (!project)
|
|
661
|
-
return apiError(reply, 404, "PROJECT_NOT_FOUND"
|
|
682
|
+
return apiError(reply, 404, "PROJECT_NOT_FOUND");
|
|
662
683
|
const query = request.query;
|
|
663
684
|
const mainFile = compileMainFile(config, id, project.main_file, query.mainFile);
|
|
664
685
|
if (!mainFile)
|
|
665
|
-
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID"
|
|
686
|
+
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID");
|
|
666
687
|
const downloading = query.download === "1";
|
|
667
688
|
let artifact = availablePdf(config, id, mainFile, project.main_file);
|
|
668
689
|
if (!artifact)
|
|
669
|
-
return apiError(reply, 404, "PDF_NOT_FOUND"
|
|
690
|
+
return apiError(reply, 404, "PDF_NOT_FOUND");
|
|
670
691
|
let versioned = false;
|
|
671
692
|
if (query.run) {
|
|
672
693
|
if (/^[a-f0-9-]{36}$/i.test(query.run)) {
|
|
673
694
|
const run = db.prepare(`SELECT main_file, status FROM compile_runs
|
|
674
695
|
WHERE id = ? AND project_id = ?`).get(query.run, id);
|
|
675
696
|
if (!run || run.status !== "succeeded" || run.main_file !== mainFile) {
|
|
676
|
-
return apiError(reply, 404, "PDF_NOT_FOUND"
|
|
697
|
+
return apiError(reply, 404, "PDF_NOT_FOUND");
|
|
677
698
|
}
|
|
678
699
|
artifact = compileRunPdf(config, id, mainFile, query.run);
|
|
679
700
|
if (!artifact)
|
|
680
|
-
return apiError(reply, 404, "PDF_NOT_FOUND"
|
|
701
|
+
return apiError(reply, 404, "PDF_NOT_FOUND");
|
|
681
702
|
versioned = true;
|
|
682
703
|
}
|
|
683
704
|
else if (artifact.version !== query.run) {
|
|
684
|
-
return apiError(reply, 404, "PDF_NOT_FOUND"
|
|
705
|
+
return apiError(reply, 404, "PDF_NOT_FOUND");
|
|
685
706
|
}
|
|
686
707
|
}
|
|
687
708
|
const pdf = artifact.path;
|
|
688
709
|
const stat = regularFileStat(pdf);
|
|
689
710
|
if (!stat)
|
|
690
|
-
return apiError(reply, 404, "PDF_NOT_FOUND"
|
|
711
|
+
return apiError(reply, 404, "PDF_NOT_FOUND");
|
|
691
712
|
const etag = `"${stat.size.toString(16)}-${Math.trunc(stat.mtimeMs).toString(16)}"`;
|
|
692
713
|
reply.header("Content-Type", "application/pdf");
|
|
693
714
|
const targetSuffix = mainFile === project.main_file ? "" : `-${texFileStem(mainFile)}`;
|
|
@@ -721,7 +742,7 @@ export function registerCompileRoutes(app, context) {
|
|
|
721
742
|
}
|
|
722
743
|
catch (error) {
|
|
723
744
|
if (isMissingFileError(error))
|
|
724
|
-
return apiError(reply, 404, "PDF_NOT_FOUND"
|
|
745
|
+
return apiError(reply, 404, "PDF_NOT_FOUND");
|
|
725
746
|
throw error;
|
|
726
747
|
}
|
|
727
748
|
return reply.code(206).send(stream);
|
|
@@ -733,7 +754,7 @@ export function registerCompileRoutes(app, context) {
|
|
|
733
754
|
}
|
|
734
755
|
catch (error) {
|
|
735
756
|
if (isMissingFileError(error))
|
|
736
|
-
return apiError(reply, 404, "PDF_NOT_FOUND"
|
|
757
|
+
return apiError(reply, 404, "PDF_NOT_FOUND");
|
|
737
758
|
throw error;
|
|
738
759
|
}
|
|
739
760
|
return reply.code(200).send(stream);
|