Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions backend/src/config/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,35 @@ module.exports = {
// --- Archive extraction ---
MAX_EXTRACTED_ARCHIVE_SIZE: process.env.MAX_EXTRACTED_ARCHIVE_SIZE?.trim() || null,
MAX_ARCHIVE_ENTRIES: Number(process.env.MAX_ARCHIVE_ENTRIES) || 100000,
MAX_BROWSABLE_ARCHIVE_SIZE: process.env.MAX_BROWSABLE_ARCHIVE_SIZE?.trim() || null,
ARCHIVE_CACHE_MAX_SIZE: process.env.ARCHIVE_CACHE_MAX_SIZE?.trim() || null,
ARCHIVE_EXTENSIONS: process.env.ARCHIVE_EXTENSIONS || '',
// --- Folder size index ---
FOLDER_SIZE_MODE: process.env.FOLDER_SIZE_MODE?.trim().toLowerCase() || 'off',
FOLDER_SIZE_MODE_SET:
typeof process.env.FOLDER_SIZE_MODE === 'string' && process.env.FOLDER_SIZE_MODE.trim() !== '',
// Lightweight process and cgroup diagnostics, off by default. When enabled the
// sampler logs only anomalous intervals unless explicitly told otherwise.
PERFORMANCE_DIAGNOSTICS_ENABLED:
normalizeBoolean(process.env.PERFORMANCE_DIAGNOSTICS_ENABLED) ?? false,
PERFORMANCE_DIAGNOSTICS_INTERVAL_MS:
process.env.PERFORMANCE_DIAGNOSTICS_INTERVAL_MS != null
? Number(process.env.PERFORMANCE_DIAGNOSTICS_INTERVAL_MS)
: 15000,
PERFORMANCE_DIAGNOSTICS_LOG_EVERY_INTERVAL:
normalizeBoolean(process.env.PERFORMANCE_DIAGNOSTICS_LOG_EVERY_INTERVAL) ?? false,
PERFORMANCE_DIAGNOSTICS_CPU_THRESHOLD:
process.env.PERFORMANCE_DIAGNOSTICS_CPU_THRESHOLD != null
? Number(process.env.PERFORMANCE_DIAGNOSTICS_CPU_THRESHOLD)
: 75,
PERFORMANCE_DIAGNOSTICS_RSS_THRESHOLD_MB:
process.env.PERFORMANCE_DIAGNOSTICS_RSS_THRESHOLD_MB != null
? Number(process.env.PERFORMANCE_DIAGNOSTICS_RSS_THRESHOLD_MB)
: 768,
PERFORMANCE_DIAGNOSTICS_EVENT_LOOP_DELAY_MS:
process.env.PERFORMANCE_DIAGNOSTICS_EVENT_LOOP_DELAY_MS != null
? Number(process.env.PERFORMANCE_DIAGNOSTICS_EVENT_LOOP_DELAY_MS)
: 250,
FOLDER_SIZE_EXCLUDE_PATHS: process.env.FOLDER_SIZE_EXCLUDE_PATHS || '',
FOLDER_SIZE_CONCURRENCY: Number(process.env.FOLDER_SIZE_CONCURRENCY) || 6,
FOLDER_SIZE_NETWORK_CONCURRENCY: Number(process.env.FOLDER_SIZE_NETWORK_CONCURRENCY) || 2,
Expand All @@ -96,6 +120,7 @@ module.exports = {
SEARCH_INDEX_CPU_PERCENT: Number(process.env.SEARCH_INDEX_CPU_PERCENT) || null,
SEARCH_INDEX_MEMORY_MB: Number(process.env.SEARCH_INDEX_MEMORY_MB) || null,
SEARCH_INDEX_EXCLUDE: process.env.SEARCH_INDEX_EXCLUDE?.trim() || null,
PREVIEW_MAX_RENDER_SIZE: process.env.PREVIEW_MAX_RENDER_SIZE?.trim() || null,
SEARCH_INDEX_REBUILD: normalizeBoolean(process.env.SEARCH_INDEX_REBUILD) ?? false,
SEARCH_INDEX_RECONCILE_MS: Number(process.env.SEARCH_INDEX_RECONCILE_MS) || null,
SEARCH_TIMEOUT_MS: Number(process.env.SEARCH_TIMEOUT_MS) || null,
Expand Down
59 changes: 59 additions & 0 deletions backend/src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,23 @@ const archives = (() => {
return Number.isFinite(parsed) && parsed > 0 ? parsed : 32 * 1024 * 1024 * 1024;
})(),
maxEntries: env.MAX_ARCHIVE_ENTRIES,
// A compound archive — a .tar.gz and its family — is two archives, and the
// inner one has to be decompressed before anything inside it can be named.
// Above this it is not: browsing a backup by unpacking it first would
// betray the whole point, and extracting it is the operation that exists
// for that. The number is the inner archive's own declared size, so the
// refusal comes before anything is written.
browseMaxBytes: (() => {
const parsed = parseByteSize(env.MAX_BROWSABLE_ARCHIVE_SIZE);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 2 * 1024 * 1024 * 1024;
})(),
// What those decompressed copies may take up altogether. They are a
// convenience and are made again whenever they are missing, so the least
// recently opened goes first when this is passed.
cacheMaxBytes: (() => {
const parsed = parseByteSize(env.ARCHIVE_CACHE_MAX_SIZE);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 8 * 1024 * 1024 * 1024;
})(),
};
if (!raw) return { extensions: DEFAULT_ARCHIVE_EXTENSIONS, ...limits };
// 'zip,iso' replaces the default list; '+udf,squashfs' extends it.
Expand Down Expand Up @@ -661,7 +678,49 @@ const folderSize = {
rebuild: env.FOLDER_SIZE_REBUILD,
};

// --- Runtime diagnostics ---
// --- Runtime diagnostics ---
const atLeast = (value, minimum, fallback) =>
Number.isFinite(value) && value >= minimum ? value : fallback;

const performanceDiagnostics = {
enabled: env.PERFORMANCE_DIAGNOSTICS_ENABLED,
intervalMs: atLeast(env.PERFORMANCE_DIAGNOSTICS_INTERVAL_MS, 5000, 15000),
logEveryInterval: env.PERFORMANCE_DIAGNOSTICS_LOG_EVERY_INTERVAL,
cpuThreshold: atLeast(env.PERFORMANCE_DIAGNOSTICS_CPU_THRESHOLD, 1, 75),
rssThresholdMb: atLeast(env.PERFORMANCE_DIAGNOSTICS_RSS_THRESHOLD_MB, 1, 768),
eventLoopDelayThresholdMs: atLeast(env.PERFORMANCE_DIAGNOSTICS_EVENT_LOOP_DELAY_MS, 1, 250),
};

/**
* How much of a document the preview will render.
*
* Not the same question as what the editor will open, and the difference is
* why this is a setting of its own. The editor streams text into a code view;
* the preview parses the document, sanitises the HTML it produces and then
* hands the browser every node to lay out — all on the one thread the
* interface has. A six-megabyte markdown file opens in the editor and freezes
* the tab in the preview, on the same machine, from the same file.
*
* It was hard-coded before this, which meant someone who raised
* EDITOR_MAX_FILESIZE in good faith was refused at a number that appeared in
* no setting and no document.
*
* Generous by default because freezing is no longer the failure mode: the
* preview renders in slices of a frame and hands the browser back between
* them. What is left is the weight of the document in the tab, which is a
* reader's problem rather than an application's. And the preview reads through
* the editor's endpoint, so EDITOR_MAX_FILESIZE already caps what can reach
* it — this only bites when it is set lower than that.
*/
const previewMaxRenderBytes = (() => {
const parsed = parseByteSize(env.PREVIEW_MAX_RENDER_SIZE);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 16 * 1024 * 1024;
})();

module.exports = {
performanceDiagnostics,
preview: { maxRenderBytes: previewMaxRenderBytes },
folderSize,
webauthn,
activity,
Expand Down
10 changes: 10 additions & 0 deletions backend/src/openapi/paths/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ module.exports = {
},
}),
},
'/api/files/recent-destinations': {
get: op({
id: 'listRecentDestinations',
summary: 'Folders this account recently copied or moved into',
description: 'Only those it can still reach.',
tag: TAG,
access: 'account',
responses: { 200: json(obj({ items: arrayOf(str()) }, ['items'])), ...errors(401) },
}),
},
'/api/files/delete-impact': {
post: op({
id: 'describeDeletion',
Expand Down
Loading
Loading