mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-03 16:41:27 +01:00
The file pane decided what to show by MIME type, and uploads keep a
type only when it cannot execute, so a .md, a .json or a .pem arrived
as application/octet-stream and got "no preview". It now goes by name
first (preview.ts), and a file with a name it does not know has its
first 8 KiB looked at: no NUL bytes and valid UTF-8 means text.
- text and code: the first 256 KiB with line numbers, as before
- markdown: rendered through the same sanitiser as documents, with a
source toggle
- CSV/TSV: a table of the first 500 rows, quotes respected
- PDF: the browser's own viewer in a frame. Only a PDF is served
without the sandbox CSP (Chrome's viewer will not start under it),
with frame-ancestors 'self' and X-Frame-Options SAMEORIGIN; the app
CSP allows same-origin frames.
- certificates, keys, keystores: POST /api/nodes/{id}/inspect reads
PEM (chains, keys, requests, public keys), DER and PKCS#12 on the
server, in pure Rust, including the RC2/3DES keystores that
openssl -legacy and older keychains write. It answers with subject,
issuer, validity, key, names, usage and fingerprints, and pairs each
private key with its certificate by public key. A password is asked
for only when needed, used for that one answer and never stored;
no key material is ever sent back. Sealed files are read through the
vault, and only while it is open.
The tests read throwaway fixtures under src/routes/testdata, made with
the openssl commands listed beside them; .gitignore lets their .pem
files past a global *.pem rule.
85 lines
3.0 KiB
TypeScript
85 lines
3.0 KiB
TypeScript
/** What the file pane shows a file as. */
|
|
export type PreviewKind = 'image' | 'video' | 'audio' | 'pdf' | 'markdown' | 'table' | 'text' | 'cert' | 'none'
|
|
|
|
/*
|
|
* Uploads keep a MIME type only when it cannot execute, so a `.json` or a
|
|
* `.pem` is stored as `application/octet-stream`. The name says more.
|
|
*/
|
|
const TEXT = new Set(
|
|
(
|
|
'txt text log out err diff patch json jsonl ndjson yaml yml toml ini cfg conf env properties ' +
|
|
'xml html htm css scss js mjs cjs ts tsx jsx svelte vue rs go py rb php java kt swift c h cc cpp hpp cs ' +
|
|
'sh bash zsh fish ps1 bat sql graphql proto lua pl r tex bib srt vtt gitignore dockerfile makefile pub'
|
|
).split(' '),
|
|
)
|
|
const CERT = new Set(['pem', 'crt', 'cer', 'der', 'csr', 'key', 'p8', 'p12', 'pfx'])
|
|
|
|
/** `notes.md` → `md`; `Makefile` → `makefile`; a dotfile is its own name. */
|
|
function extension(name: string): string {
|
|
const lower = name.toLowerCase()
|
|
const dot = lower.lastIndexOf('.')
|
|
return dot === -1 ? lower : lower.slice(dot + 1)
|
|
}
|
|
|
|
/**
|
|
* By name first: `.ts` is TypeScript here, whatever the MIME tables say
|
|
* about MPEG transport streams. `none` may still turn out to be text — see
|
|
* `looksLikeText`.
|
|
*/
|
|
export function previewKind(name: string, mime: string): PreviewKind {
|
|
const ext = extension(name)
|
|
if (CERT.has(ext)) return 'cert'
|
|
if (ext === 'md' || ext === 'markdown') return 'markdown'
|
|
if (ext === 'csv' || ext === 'tsv') return 'table'
|
|
if (TEXT.has(ext)) return 'text'
|
|
const [type] = mime.split('/')
|
|
if (type === 'image' || type === 'video' || type === 'audio') return type
|
|
if (mime === 'application/pdf') return 'pdf'
|
|
if (mime === 'text/plain') return 'text'
|
|
return 'none'
|
|
}
|
|
|
|
/** No NUL bytes and valid UTF-8, a character cut off at the end aside. */
|
|
export function looksLikeText(bytes: Uint8Array): boolean {
|
|
if (bytes.includes(0)) return false
|
|
try {
|
|
new TextDecoder('utf-8', { fatal: true }).decode(bytes, { stream: true })
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/** CSV or TSV into rows, quotes respected, at most `limit` of them. */
|
|
export function parseTable(text: string, separator: string, limit: number): string[][] {
|
|
const rows: string[][] = []
|
|
let row: string[] = []
|
|
let cell = ''
|
|
let quoted = false
|
|
for (let i = 0; i < text.length && rows.length < limit; i++) {
|
|
const c = text[i]
|
|
if (quoted) {
|
|
if (c === '"' && text[i + 1] === '"') {
|
|
cell += '"'
|
|
i++
|
|
} else if (c === '"') quoted = false
|
|
else cell += c
|
|
} else if (c === '"' && cell === '') quoted = true
|
|
else if (c === separator) {
|
|
row.push(cell)
|
|
cell = ''
|
|
} else if (c === '\n' || c === '\r') {
|
|
if (c === '\r' && text[i + 1] === '\n') i++
|
|
row.push(cell)
|
|
rows.push(row)
|
|
row = []
|
|
cell = ''
|
|
} else cell += c
|
|
}
|
|
if ((cell !== '' || row.length > 0) && rows.length < limit) rows.push([...row, cell])
|
|
return rows
|
|
}
|
|
|
|
/** The separator of a table file, by its name. */
|
|
export const separatorOf = (name: string) => (extension(name) === 'tsv' ? '\t' : ',')
|