feat: SQLite preview read straight from the file, and sorting of the file table by column, kept per folder

This commit is contained in:
redtea committed 2026-10-14 18:46:32 +03:00
1 parent 1a90af7800
commit c911143eb2
2 files changed
+191 -10

No files matched your search

+143 -6
View File
@@ -143,13 +143,13 @@
// ---------- tables: one grid per sheet ----------
const MAX_ROWS = 2000, MAX_COLS = 60;
function colName(i) { let s = ''; for (i++; i > 0; i = Math.floor((i - 1) / 26)) s = String.fromCharCode(65 + (i - 1) % 26) + s; return s; }
function grid(rows) {
let cols = 0;
function grid(rows, names) {
let cols = names ? names.length : 0;
for (const r of rows) cols = Math.max(cols, r.length);
cols = Math.min(cols, MAX_COLS);
const cut = rows.length > MAX_ROWS || rows.some(r => r.length > MAX_COLS);
let html = '<div class="dv-scroll dv-grid"><table class="dv-sheet"><thead><tr><th></th>';
for (let c = 0; c < cols; c++) html += '<th>' + colName(c) + '</th>';
for (let c = 0; c < cols; c++) html += '<th>' + esc(names && names[c] != null ? names[c] : colName(c)) + '</th>';
html += '</tr></thead><tbody>';
rows.slice(0, MAX_ROWS).forEach((r, i) => {
html += '<tr><th>' + (i + 1) + '</th>';
@@ -170,8 +170,8 @@
html += '<div class="dv-tabs">' + list.map((s, i) => '<button type="button" data-sheet="' + i + '"' + (i ? '' : ' class="on"') + '>' + esc(s.name) + '</button>').join('') + '</div>';
}
list.forEach((s, i) => {
const g = grid(s.rows);
html += '<div class="dv-pane" data-pane="' + i + '"' + (i ? ' hidden' : '') + '>' + g.html +
const g = grid(s.rows, s.cols);
html += '<div class="dv-pane" data-pane="' + i + '"' + (i ? ' hidden' : '') + '>' + (s.note ? '<p class="dv-note">' + esc(s.note) + '</p>' : '') + g.html +
(g.cut ? '<p class="dv-note">показаны первые ' + MAX_ROWS + ' строк и ' + MAX_COLS + ' столбцов</p>' : '') + '</div>';
});
return html;
@@ -387,5 +387,142 @@
text.split(/\r\n|\r|\n|\x07/).filter(l => l.trim()).map(l => '<p>' + esc(l) + '</p>').join('') + '</div>';
}
window.DocView = {docx, xlsx, ods, odt, csv, doc, bindTabs};
// ---------- sqlite ----------
// Reads the database file itself: the b-tree of sqlite_master for the
// tables, then each table's b-tree for its first rows. No SQL engine.
function sqlite(buf) {
const b = new Uint8Array(buf);
if (new TextDecoder().decode(b.subarray(0, 15)) !== 'SQLite format 3') throw new Error('не база SQLite');
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
let ps = dv.getUint16(16);
if (ps === 1) ps = 65536;
const usable = ps - b[20];
const enc = dv.getUint32(56);
const dec = new TextDecoder(enc === 2 ? 'utf-16le' : enc === 3 ? 'utf-16be' : 'utf-8');
const page = n => (n - 1) * ps;
function varint(o) {
let v = 0;
for (let i = 0; i < 9; i++) {
const c = b[o + i];
if (i === 8) return [v * 256 + c, o + 9];
v = v * 128 + (c & 0x7f);
if (!(c & 0x80)) return [v, o + i + 1];
}
}
// A cell's payload, gathered from its overflow pages when it spills.
function payload(o, size) {
const max = usable - 35;
if (size <= max) return b.subarray(o, o + size);
const min = Math.floor((usable - 12) * 32 / 255) - 23;
let local = min + (size - min) % (usable - 4);
if (local > max) local = min;
const out = new Uint8Array(size);
out.set(b.subarray(o, o + local));
let at = local, next = dv.getUint32(o + local);
for (let guard = 0; next && at < size && guard < 1e6; guard++) {
const po = page(next), len = Math.min(usable - 4, size - at);
out.set(b.subarray(po + 4, po + 4 + len), at);
at += len;
next = dv.getUint32(po);
}
return out;
}
function record(p) {
const pv = new DataView(p.buffer, p.byteOffset, p.byteLength);
let [hlen, o] = vi(p, 0);
const types = [];
while (o < hlen) { const r = vi(p, o); types.push(r[0]); o = r[1]; }
let d = hlen;
return types.map(t => {
let v = null;
if (t >= 1 && t <= 6) {
const n = [0, 1, 2, 3, 4, 6, 8][t];
let x = 0;
for (let i = 0; i < n; i++) x = x * 256 + p[d + i];
if (p[d] & 0x80) x -= Math.pow(2, 8 * n);
v = n === 8 && Math.abs(x) > Number.MAX_SAFE_INTEGER ? pv.getBigInt64(d).toString() : x;
d += n;
} else if (t === 7) { v = pv.getFloat64(d); d += 8; }
else if (t === 8) v = 0;
else if (t === 9) v = 1;
else if (t >= 12 && t % 2 === 0) { const n = (t - 12) / 2; v = '[blob ' + n + ' B]'; d += n; }
else if (t >= 13) { const n = (t - 13) / 2; v = dec.decode(p.subarray(d, d + n)); d += n; }
return v;
});
}
// varint over a payload buffer rather than the file
function vi(p, o) {
let v = 0;
for (let i = 0; i < 9; i++) {
const c = p[o + i];
if (i === 8) return [v * 256 + c, o + 9];
v = v * 128 + (c & 0x7f);
if (!(c & 0x80)) return [v, o + i + 1];
}
}
// Walks a table b-tree in rowid order, up to limit rows; also counts all.
function table(root, limit) {
const rows = [];
let count = 0;
const walk = (n, depth) => {
if (depth > 40) return;
const h = page(n) + (n === 1 ? 100 : 0);
const type = b[h], cells = dv.getUint16(h + 3);
const ptrs = h + (type === 0x05 ? 12 : 8);
if (type === 0x0d) {
count += cells;
for (let i = 0; i < cells && rows.length < limit; i++) {
let o = page(n) + dv.getUint16(ptrs + i * 2), size, rowid;
[size, o] = varint(o);
[rowid, o] = varint(o);
rows.push({rowid, values: record(payload(o, size))});
}
} else if (type === 0x05) {
for (let i = 0; i < cells; i++) walk(dv.getUint32(page(n) + dv.getUint16(ptrs + i * 2)), depth + 1);
walk(dv.getUint32(h + 8), depth + 1);
}
};
walk(root, 0);
return {rows, count};
}
// Column names out of CREATE TABLE; an INTEGER PRIMARY KEY is the rowid.
function columns(sql) {
const m = /\(([\s\S]*)\)/.exec(sql || '');
if (!m) return {names: [], rowidCol: -1};
const parts = [];
let depth = 0, cur = '';
for (const c of m[1]) {
if (c === '(') depth++;
if (c === ')') depth--;
if (c === ',' && !depth) { parts.push(cur); cur = ''; } else cur += c;
}
parts.push(cur);
const names = [];
let rowidCol = -1;
for (const p of parts) {
const t = p.trim();
if (!t || /^(constraint|primary\s+key|unique|check|foreign\s+key)\b/i.test(t)) continue;
const nm = /^("(?:[^"]|"")+"|`[^`]+`|\[[^\]]+\]|\S+)/.exec(t)[1].replace(/^["`\[]|["`\]]$/g, '');
if (/^\S+\s+integer\s+primary\s+key\b/i.test(t.replace(/^("(?:[^"]|"")+"|`[^`]+`|\[[^\]]+\])/, 'x'))) rowidCol = names.length;
names.push(nm);
}
return {names, rowidCol};
}
const master = table(1, 100000).rows.map(r => ({type: r.values[0], name: r.values[1], root: r.values[3], sql: r.values[4] || ''}));
const list = [];
for (const t of master.filter(x => x.type === 'table' && x.root).sort((a, c) => String(a.name).localeCompare(String(c.name)))) {
if (/without\s+rowid/i.test(t.sql)) { list.push({name: t.name, rows: [], note: 'таблица WITHOUT ROWID - её строки здесь не показываются'}); continue; }
const {names, rowidCol} = columns(t.sql);
let data;
try { data = table(t.root, MAX_ROWS); } catch (e) { list.push({name: t.name, rows: [], note: 'не удалось прочитать: ' + e.message}); continue; }
const rows = data.rows.map(r => { const v = r.values.slice(); if (rowidCol >= 0 && v[rowidCol] == null) v[rowidCol] = r.rowid; return v; });
list.push({name: t.name, cols: names, rows, note: data.count + ' ' + (data.count % 10 === 1 && data.count % 100 !== 11 ? 'строка' : data.count % 10 >= 2 && data.count % 10 <= 4 && (data.count % 100 < 12 || data.count % 100 > 14) ? 'строки' : 'строк') + (data.count > rows.length ? ', показаны первые ' + rows.length : '')});
}
if (!list.length) return '<p class="dv-note">в базе нет таблиц</p>';
return sheets(list);
}
window.DocView = {docx, xlsx, ods, odt, csv, doc, sqlite, bindTabs};
})();