MeowPanel: 字体库 tar.gz 导入/导出 + 文件管理自然排序修复 + multipart 二进制边界修复
- 字体库导入/导出(用户 2026-08-31 定案):放弃 zip,与 .mpk 同构走 busybox 'gzip -dc | tar' 管道 (设备必带 tar/gzip,零新增依赖)。导入 POST /api/fonts/import(multipart file=<.tar.gz>,上限32MB, ?force=1 覆盖):tar -t 成员预校验(sanitize_member 拒绝对/../shell 危险字符,恰为三件套平铺)→ 解压到临时目录 → opendir/lstat 锁(拒软链/目录/设备/多出条目/解压后超大)→ font.json 解析 family/weight (safe_dir_segment 消毒)→ 覆盖冲突 409 → sha256_png/idx 仅 WARN → rename 到 MeowOS/fonts/<family>/<weight>/。 导出 GET /api/fonts/export?family=&weight=:tar -cf - | gzip -c 打包 <family>-<weight>.font.tar.gz 流式下载。 列表 GET /api/fonts:扫描三件套齐。前端设置页加「字体库」卡(导入 zip/tar.gz + 409 confirm 后 force=1 重试 + 导出按钮)。 - 文件管理排序修复:/api/files 排序改为目录优先 + 名称自然排序(数字感知 track2<track10,中文按无符号 字节序),替换原 strcasecmp 词法排序(用户报「很离谱」)。 - multipart 二进制边界修复(latent bug):strstr 遇数据内 NUL 提前停止 → memmem 语义 mem_find 字节安全查找, 修复字体 tar.gz/.mpk/二进制上传可能误吞后段的问题。 - 文档同步:docs/MEOWOS_SPEC_REF.md §5.2/§5.7.6、patch_v2/meowpanel/README.md 改 tar.gz 定案; FONT_SYSTEM.md 无 zip 表述无需改。meow_font_import_zip/export_zip 语义注为 tar.gz(_tgz 可更名)。 - 宿主编译 0 warning(-Werror 亦过);宿主端到端:导入→导出→逐字节比对、损坏/缺件/坏json/穿越/软链/非gzip全拒、 自然排序样本全过;真机已部署(meowpanel md5 51ffc183,服务 running)并 curl 验证 font 端点 + 排序。
This commit is contained in:
1 parent
4e3f41a5a3
commit
d8b95c0aab
7 files changed
+580
-23
No files matched your search
@@ -608,8 +608,49 @@
|
||||
if (!g[1].length) return;
|
||||
h += '<div class="card"><h3 class="card-title">' + g[0] + '</h3>' + g[1].map(settingControl).join('') + '</div>';
|
||||
});
|
||||
// 字体库(.zip 导入 / 导出)
|
||||
h += '<div class="card" id="fonts-card"><h3 class="card-title">字体库(.tar.gz 导入 / 导出)</h3>' +
|
||||
'<div class="field-row"><button class="btn" id="font-import">导入字体库 .tar.gz</button>' +
|
||||
'<input type="file" id="font-file" accept=".tar.gz,.tgz,application/gzip" hidden></div>' +
|
||||
'<div id="font-list"><p class="muted">加载字体库列表…</p></div>' +
|
||||
'<p class="muted" style="margin-top:8px">导入 tar.gz(内含 font.png + font.idx + font.json)→ 校验后解压到 MeowOS/fonts/<family>/<weight>/;导出把该三件套打包为 tar.gz。导入后进设备「设置 → 字体」页列表自动重扫生效。</p></div>';
|
||||
h += '</div>';
|
||||
$('#content').innerHTML = h;
|
||||
loadFonts();
|
||||
}
|
||||
async function loadFonts() {
|
||||
const box = $('#font-list'); if (!box) return;
|
||||
let d;
|
||||
try { d = await api('GET', '/api/fonts'); }
|
||||
catch (e) { box.innerHTML = '<p class="muted">字体库列表加载失败:' + esc(errText(e)) + '</p>'; return; }
|
||||
const fs = (d && d.fonts) || [];
|
||||
if (!fs.length) { box.innerHTML = '<p class="muted">暂无字库(TF 卡 MeowOS/fonts 下无完整三件套)。</p>'; return; }
|
||||
box.innerHTML = fs.map(f => {
|
||||
const nm = esc(f.name || f.family || '');
|
||||
const meta = esc((f.family || '') + '/' + (f.weight || '') + (f.tier ? ' · ' + f.tier : '') + (f.glyph_count ? ' · ' + f.glyph_count + ' 字' : ''));
|
||||
return '<div class="field-row"><div style="flex:1;overflow:hidden;text-overflow:ellipsis">' +
|
||||
'<div>' + nm + '</div><div class="muted">' + meta + '</div></div>' +
|
||||
'<button class="btn" data-fexport="' + esc(f.family || '') + '" data-fweight="' + esc(f.weight || '') + '">导出</button></div>';
|
||||
}).join('');
|
||||
}
|
||||
async function importFont(files) {
|
||||
if (!files || !files.length) return;
|
||||
const f = files[0];
|
||||
const fd = new FormData(); fd.append('file', f);
|
||||
toast('正在导入 ' + f.name + '…');
|
||||
let res;
|
||||
try { res = await api('POST', '/api/fonts/import', fd); }
|
||||
catch (err) {
|
||||
if (err.code === 'CONFLICT') {
|
||||
const ok = await confirmDlg('覆盖字库', '目标字库已存在,覆盖导入吗?', true);
|
||||
if (!ok) return;
|
||||
try { res = await api('POST', '/api/fonts/import?force=1', fd); }
|
||||
catch (e2) { toast('导入失败:' + errText(e2)); return; }
|
||||
} else { toast('导入失败:' + errText(err)); return; }
|
||||
}
|
||||
toast('已导入 ' + ((res && res.family) || '') + '/' + ((res && res.weight) || ''));
|
||||
if (res && res.hint) toast(res.hint);
|
||||
loadFonts();
|
||||
}
|
||||
function bindSettings() {
|
||||
const c = $('#content');
|
||||
@@ -619,6 +660,7 @@
|
||||
updateSetting(r.dataset.skey, +r.value, 'int');
|
||||
});
|
||||
c.addEventListener('change', function (e) {
|
||||
if (e.target.id === 'font-file') { importFont(e.target.files); return; }
|
||||
const t = e.target;
|
||||
const sk = t.dataset.skey; if (!sk) return;
|
||||
if (t.dataset.stype === 'bool') updateSetting(sk, t.checked, 'bool');
|
||||
@@ -626,6 +668,13 @@
|
||||
else if (t.dataset.stype === 'str') updateSetting(sk, t.value, 'str');
|
||||
});
|
||||
c.addEventListener('click', async function (e) {
|
||||
if (e.target.closest('#font-import')) { const inp = $('#font-file'); if (inp) { inp.value = ''; inp.click(); } return; }
|
||||
const fe = e.target.closest('[data-fexport]');
|
||||
if (fe) {
|
||||
const f = fe.dataset.fexport, w = fe.dataset.fweight;
|
||||
doDownload('/api/fonts/export?family=' + encodeURIComponent(f) + '&weight=' + encodeURIComponent(w), (f || 'font') + '-' + (w || 'regular') + '.font.tar.gz');
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('[data-stype="pass"]')) { setPanelPassword(); return; }
|
||||
if (e.target.closest('#set-restore')) {
|
||||
const ok = await confirmDlg('恢复全部默认', '将把全部设置恢复为出厂默认(含主题/亮度/音量)。继续?', true);
|
||||
|
||||
Reference in new issue
Block a user