Detect text files via MIME + binary heuristic instead of hardcoded extension list

Server-side _file_type() now uses mimetypes for images/known text,
and falls back to null-byte check on first 8KB for everything else.
Client uses the "type" field from /files API instead of local extension sets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 20:35:16 +04:00
co-authored by Claude Opus 4.6
parent eba5ece147
commit 266308a2eb
2 changed files with 19 additions and 15 deletions
+13 -1
View File
@@ -74,6 +74,18 @@ def ensure_cert(cert_dir: Path, extra_ips: list[str] | None = None) -> tuple[Pat
BASE_DIR = Path(__file__).resolve().parent
def _file_type(path: Path) -> str:
ct = mimetypes.guess_type(path.name)[0] or ''
if ct.startswith('image/'):
return 'image'
if ct.startswith('text/'):
return 'text'
try:
with open(path, 'rb') as f:
return 'text' if b'\x00' not in f.read(8192) else 'other'
except OSError:
return 'other'
# ── HTTP Handler ──
@@ -149,7 +161,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
files = []
for p in sorted(self.server.upload_dir.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True):
if p.is_file():
files.append({"name": p.name, "size": p.stat().st_size})
files.append({"name": p.name, "size": p.stat().st_size, "type": _file_type(p)})
self._json(files)
def _route_download(self):
+6 -14
View File
@@ -259,10 +259,7 @@ $('lang-sw').addEventListener('click', e => {
if (e.target.dataset.l) setLang(e.target.dataset.l);
});
const TEXT_EXT = new Set(
'txt,log,json,xml,csv,html,css,js,ts,py,md,sh,yml,yaml,toml,ini,cfg,conf,env,sql,rs,go,java,c,h,cpp,rb,php'.split(',')
);
const IMG_EXT = new Set('png,jpg,jpeg,gif,webp,bmp,ico,svg'.split(','));
/* File type is determined server-side via /files API ("text" | "image" | "other") */
/* ── Helpers ── */
@@ -287,10 +284,6 @@ function esc(s) {
.replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
function fileExt(name) {
return (name.split('.').pop() || '').toLowerCase();
}
/* ── Clipboard ── */
function fallbackCopy(text) {
@@ -332,15 +325,14 @@ function toPngBlob(blob) {
});
}
function copyFile(name) {
function copyFile(name, type) {
const url = '/dl/' + encodeURIComponent(name);
const ext = fileExt(name);
if (TEXT_EXT.has(ext)) {
if (type === 'text') {
fetch(url).then(r => r.text()).then(text => copyText(text))
.catch(() => toast(t().copyFail));
} else if (IMG_EXT.has(ext)) {
} else if (type === 'image') {
const link = location.origin + url;
if (!navigator.clipboard?.write) {
copyText(link, t().linkCopied);
@@ -457,7 +449,7 @@ function renderFiles() {
flist.innerHTML = slice.map(f => `
<div class="file">
<span class="fname" data-action="copy" data-name="${esc(f.name)}" title="${t().copy}">${esc(f.name)}</span>
<span class="fname" data-action="copy" data-name="${esc(f.name)}" data-type="${f.type}" title="${t().copy}">${esc(f.name)}</span>
<div class="meta">
<span class="fsize">${fmtSize(f.size)}</span>
<a class="btn dl" href="/dl/${encodeURIComponent(f.name)}" title="${t().download}"><svg width="14" height="14"><use href="#icon-dl"/></svg></a>
@@ -482,7 +474,7 @@ flist.addEventListener('click', e => {
if (!el) return;
const action = el.dataset.action;
const name = el.dataset.name;
if (action === 'copy') copyFile(name);
if (action === 'copy') copyFile(name, el.dataset.type);
else if (action === 'del') {
fetch('/del/' + encodeURIComponent(name), { method: 'DELETE' })
.then(() => { toast(t().deleted); loadFiles() });