mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 02:52:33 -05:00
One unified filename subsystem, used everywhere a name is shown, uploaded,
served, or migrated. Design: docs/Features/Filename/Filename.md.
Display (imports/lib/fileNameDisplay.js, general cleanFileName + global
{{cleanFilename}}/{{downloadFilename}} helpers): URL-decode, normalize to
generally-used characters (NFKC + confusable-homoglyph folding so typosquatting
"payp<Cyrillic-a>l.exe" shows as "paypal.exe", only in a Latin-majority base so
genuine non-Latin names are preserved), remove invisible/control/bidi chars,
strip HTML/JS/XML/template markup. Removed the earlier invisible-char filter
button, warning triangle, inline descriptions, legend and server invisibleOnly
filter.
Upload (attachments + avatars, all backends): reject exploit-looking filenames
and EICAR virus test files; strip invisible chars; correct the extension to the
real detected type (double-click opens the right app); type-based name when
empty; cap length to a portable max (30, Amiga OS FFS) preserving the extension;
sanitize SVG JS / XML-loop content at the staging location before promoting to
final storage and delete the temp file after; enough-disk-space check with
small-RAM streaming and stop-and-remove-partial on any write error.
Existing files: general storage-agnostic detector streams a small header to
WRITABLE_PATH/files/temp, runs `file`, deletes the temp file; admin-only bounded
batch extension corrector; serve-time on-the-fly exploit sanitizer wired into
httpStreamOutput (sniffs the start, rewrites only dangerous markup, streams the
rest unchanged).
Migrations (moveToStorage/copyFile): sanitize content, finalize the name with
the same general function (detected type, corrected extension, folded
homoglyphs, capped length), save it at the destination, and number
same-name/different-content collisions (document.pdf, document-1.pdf, ...).
Tests: rewrote fileNameDisplay.test.cjs; added uploadFileName, diskSpace,
fileTypeCorrection and fileHardeningGuards unit tests (positive + negative);
updated Playwright spec 41 to the clean-name model. CHANGELOG + design doc.
42 lines
1.9 KiB
JavaScript
42 lines
1.9 KiB
JavaScript
'use strict';
|
|
|
|
// Best-effort free-disk-space probe, used before writing an uploaded file to the
|
|
// default (filesystem) storage so a large upload cannot fill the disk. When the
|
|
// platform / Node build does not expose free-space info (e.g. running inside a
|
|
// restricted sandbox, or the storage backend is remote cloud with no local disk),
|
|
// the probe returns null and the caller falls back to safe small-chunk streaming
|
|
// with immediate cleanup on any write error.
|
|
|
|
const fs = require('fs');
|
|
|
|
// Return the number of free bytes available on the filesystem that holds dirPath,
|
|
// or null when it cannot be determined. Uses fs.statfsSync when available
|
|
// (Node >= 18.15); otherwise returns null so the caller degrades gracefully.
|
|
function getFreeDiskBytes(dirPath) {
|
|
try {
|
|
if (typeof fs.statfsSync !== 'function') return null;
|
|
const stats = fs.statfsSync(dirPath);
|
|
if (!stats || typeof stats.bavail !== 'number' || typeof stats.bsize !== 'number') {
|
|
return null;
|
|
}
|
|
const free = stats.bavail * stats.bsize;
|
|
return Number.isFinite(free) && free >= 0 ? free : null;
|
|
} catch (e) {
|
|
// Path missing, unsupported platform, or statfs failed — unknown, not "full".
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// True when there is enough free space for neededBytes plus a safety margin, or
|
|
// when free space is UNKNOWN (null) — in the unknown case the caller still writes,
|
|
// but must use small-chunk streaming and clean up partial output on any error.
|
|
// A false result means we positively know the disk is (nearly) full.
|
|
function hasEnoughDiskSpace(dirPath, neededBytes, marginBytes = 16 * 1024 * 1024) {
|
|
const free = getFreeDiskBytes(dirPath);
|
|
if (free === null) return true; // unknown -> proceed with safe streaming
|
|
const need = Math.max(0, Number(neededBytes) || 0) + marginBytes;
|
|
return free >= need;
|
|
}
|
|
|
|
module.exports = { getFreeDiskBytes, hasEnoughDiskSpace };
|