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:
@@ -74,6 +74,18 @@ def ensure_cert(cert_dir: Path, extra_ips: list[str] | None = None) -> tuple[Pat
|
|||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent
|
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 ──
|
# ── HTTP Handler ──
|
||||||
|
|
||||||
@@ -149,7 +161,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
files = []
|
files = []
|
||||||
for p in sorted(self.server.upload_dir.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True):
|
for p in sorted(self.server.upload_dir.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True):
|
||||||
if p.is_file():
|
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)
|
self._json(files)
|
||||||
|
|
||||||
def _route_download(self):
|
def _route_download(self):
|
||||||
|
|||||||
+6
-14
@@ -259,10 +259,7 @@ $('lang-sw').addEventListener('click', e => {
|
|||||||
if (e.target.dataset.l) setLang(e.target.dataset.l);
|
if (e.target.dataset.l) setLang(e.target.dataset.l);
|
||||||
});
|
});
|
||||||
|
|
||||||
const TEXT_EXT = new Set(
|
/* File type is determined server-side via /files API ("text" | "image" | "other") */
|
||||||
'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(','));
|
|
||||||
|
|
||||||
/* ── Helpers ── */
|
/* ── Helpers ── */
|
||||||
|
|
||||||
@@ -287,10 +284,6 @@ function esc(s) {
|
|||||||
.replace(/"/g,'"').replace(/'/g,''');
|
.replace(/"/g,'"').replace(/'/g,''');
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileExt(name) {
|
|
||||||
return (name.split('.').pop() || '').toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Clipboard ── */
|
/* ── Clipboard ── */
|
||||||
|
|
||||||
function fallbackCopy(text) {
|
function fallbackCopy(text) {
|
||||||
@@ -332,15 +325,14 @@ function toPngBlob(blob) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyFile(name) {
|
function copyFile(name, type) {
|
||||||
const url = '/dl/' + encodeURIComponent(name);
|
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))
|
fetch(url).then(r => r.text()).then(text => copyText(text))
|
||||||
.catch(() => toast(t().copyFail));
|
.catch(() => toast(t().copyFail));
|
||||||
|
|
||||||
} else if (IMG_EXT.has(ext)) {
|
} else if (type === 'image') {
|
||||||
const link = location.origin + url;
|
const link = location.origin + url;
|
||||||
if (!navigator.clipboard?.write) {
|
if (!navigator.clipboard?.write) {
|
||||||
copyText(link, t().linkCopied);
|
copyText(link, t().linkCopied);
|
||||||
@@ -457,7 +449,7 @@ function renderFiles() {
|
|||||||
|
|
||||||
flist.innerHTML = slice.map(f => `
|
flist.innerHTML = slice.map(f => `
|
||||||
<div class="file">
|
<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">
|
<div class="meta">
|
||||||
<span class="fsize">${fmtSize(f.size)}</span>
|
<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>
|
<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;
|
if (!el) return;
|
||||||
const action = el.dataset.action;
|
const action = el.dataset.action;
|
||||||
const name = el.dataset.name;
|
const name = el.dataset.name;
|
||||||
if (action === 'copy') copyFile(name);
|
if (action === 'copy') copyFile(name, el.dataset.type);
|
||||||
else if (action === 'del') {
|
else if (action === 'del') {
|
||||||
fetch('/del/' + encodeURIComponent(name), { method: 'DELETE' })
|
fetch('/del/' + encodeURIComponent(name), { method: 'DELETE' })
|
||||||
.then(() => { toast(t().deleted); loadFiles() });
|
.then(() => { toast(t().deleted); loadFiles() });
|
||||||
|
|||||||
Reference in New Issue
Block a user