Embedded Files and Documents - SmithSoft-Studios/JAZMIN GitHub Wiki
A JAZMIN file can carry files alongside its data: PDFs, images, audio, video, HTML, scripts, styles, fonts, or any other bytes. Typical uses:
- Attachments: the invoice PDF for each statement, the photos taken on a site visit.
- Branding: a logo per client.
- A document: a template (HTML, CSS, scripts) that renders the data as a page in the viewer, or as a PDF on a server. One file then holds the data and how to show it.
How they are stored:
- Once. Identical bytes are stored a single time, however many paths or groups refer to them.
- In 256 KiB blocks, so part of a large file (a video being sought, a page of a PDF) is read without loading all of it.
- As they are when already compressed (JPEG, PNG, MP4, PDF); compressed otherwise.
-
Encrypted and checked like everything else:
readFileverifies each file against its SHA-256. - Per key in access-controlled files: each file lists the groups that may see it, and a key never learns the names of files it can't see.
import { JazminKey, append, open, write } from '@smithsoft-studios/jazmin';
const owner = JazminKey.generate();
const clientA = owner.createAccessKey();
const clientB = owner.createAccessKey();
const pdf = Buffer.concat([Buffer.from('%PDF-1.7\n'), Buffer.alloc(600_000, 32)]); // a stand-in for a real 600 KB PDF
write('statements.jzm', [
{ client: 'A', line: 1, amount: 100 },
{ client: 'B', line: 1, amount: 200 },
], {
key: owner,
access: {
partitionBy: 'client',
grants: [{ key: clientA, rows: ['A'] }, { key: clientB, rows: ['B'] }],
},
files: [
{ path: 'index.html', content: '<h1>Statement</h1>' }, // groups default to '*': everyone
{ path: 'terms.pdf', content: pdf },
{ path: 'invoices/A.pdf', content: pdf, groups: ['A'] }, // the same bytes: stored once
{ path: 'img/logo-B.svg', content: '<svg xmlns="http://www.w3.org/2000/svg"/>', groups: ['B'] },
],
package: { entry: 'index.html', title: 'Statement' }, // settings for viewers
});
for (const [who, key] of [['owner', owner], ['client A', clientA], ['client B', clientB]]) {
const r = open('statements.jzm', { key: key === owner ? owner : key.export() });
console.log(who.padEnd(9), r.files.map((f) => `${f.path} (${f.type}, ${f.size.toLocaleString('en-US')} bytes)`).join('; '));
r.close();
}
const a = open('statements.jzm', { key: clientA.export() });
console.log(a.readFile('index.html').toString()); // a whole file, checked against its SHA-256
console.log(a.readFileRange('invoices/A.pdf', 0, 8).toString()); // bytes [0, 8): only the first block is read
a.close();
append('statements.jzm', { key: owner, addFiles: [{ path: 'news.html', content: '<p>New rates</p>' }], removeFiles: ['terms.pdf'] });
console.log(open('statements.jzm', { key: owner }).files.map((f) => f.path));owner index.html (text/html, 18 bytes); terms.pdf (application/pdf, 600,009 bytes); invoices/A.pdf (application/pdf, 600,009 bytes); img/logo-B.svg (image/svg+xml, 41 bytes)
client A index.html (text/html, 18 bytes); terms.pdf (application/pdf, 600,009 bytes); invoices/A.pdf (application/pdf, 600,009 bytes)
client B index.html (text/html, 18 bytes); terms.pdf (application/pdf, 600,009 bytes); img/logo-B.svg (image/svg+xml, 41 bytes)
<h1>Statement</h1>
%PDF-1.7
[ 'index.html', 'news.html', 'invoices/A.pdf', 'img/logo-B.svg' ]
| Who sees a file | groups |
|---|---|
| Everyone with a key to the file |
'*' (the default) |
| Keys for partitions A or B | ['A', 'B'] |
| Keys granted a named file group |
['template'], with the grant's files: ['template']
|
-
Reading:
fileslists what this key can see (path,type,size,sha256; the owner also seesgroups).readFile(path)returns the bytes,readFileRange(path, start, end)part of them, andopenFile(path)a stream, block by block. -
Media types come from the extension unless you give
type. -
Changing files:
addFilesandremoveFileswork withupdate()andappend(). A path that exists is replaced. Appends keep existing files by reference, and a replaced or removed file keeps its space untilcompact().
using System.Text;
using Jazmin;
var columns = new[] { new JazminColumn("id", JazminType.Int) };
var options = new JazminWriteOptions
{
Files =
[
new JazminFileInput("index.html", Encoding.UTF8.GetBytes("<h1>Report</h1>")),
new JazminFileInput("data/notes.txt", Encoding.UTF8.GetBytes("Quarterly notes")) { Type = "text/plain" },
],
Package = new JazminPackage { Entry = "index.html", Title = "Report" },
};
using (var writer = JazminWriter.Create("report.jzm", columns, options))
writer.WriteValues(1L);
using var reader = JazminReader.Open("report.jzm");
foreach (var f in reader.Files) Console.WriteLine($"{f.Path} {f.Type} {f.Size} bytes");
Console.WriteLine(Encoding.UTF8.GetString(reader.ReadFile("data/notes.txt")));
using var stream = reader.OpenFile("index.html"); // seekable; decodes one block at a time
Console.WriteLine(new StreamReader(stream).ReadToEnd());
Console.WriteLine($"entry {reader.Package!.Entry}, title {reader.Package.Title}");data/notes.txt text/plain 15 bytes
index.html text/html 15 bytes
Quarterly notes
<h1>Report</h1>
entry index.html, title Report
JazminFileInput.FromFile(path, filePathOnDisk, groups) embeds a file from disk, read in blocks so it is never wholly
in memory.
A package is a set of embedded files with an entry page. The viewer shows it as the file's
document, and renderPdf prints it on a server. The page runs in a sandbox with no network access (apart
from the package's allowedOrigins), no browser storage, and no sight of the key. It reads the data through
window.jazmin:
<!doctype html>
<html>
<head><link rel="stylesheet" href="style.css"></head>
<body>
<img id="logo" alt="">
<h1 id="title"></h1>
<table id="lines"><thead><tr><th>Date</th><th>Description</th><th>Amount</th></tr></thead><tbody></tbody></table>
<p>Total: <span id="total"></span></p>
<script>
(async () => {
document.getElementById('title').textContent = jazmin.metadata.title ?? 'Statement';
document.getElementById('logo').src = jazmin.asset('img/logo.svg'); // an embedded file
const rows = await jazmin.query(null, { orderBy: 'date', limit: 500, select: ['date', 'description', 'amount'] });
const body = document.querySelector('#lines tbody');
let total = 0;
for (const r of rows) {
const tr = body.insertRow();
tr.insertCell().textContent = new Date(r.date).toLocaleDateString();
tr.insertCell().textContent = r.description;
tr.insertCell().textContent = r.amount;
total += Number(r.amount);
}
document.getElementById('total').textContent = total.toFixed(2);
jazmin.ready({ rows: rows.length }); // tells renderPdf the page is complete
})();
</script>
</body>
</html>window.jazmin |
What it does |
|---|---|
metadata, columns, access, rowCount
|
About the file, as the key sees it |
await query(filter, { orderBy, offset, limit, select }) |
A page of rows (orderBy: '-amount' for descending) |
await count(filter) |
Matching rows |
rows() |
Every row, for small data sets (up to 20,000 rows) |
asset(path) |
An embedded file as an in-memory URL, for img.src, fonts, media |
await file(path) |
An embedded file as a Blob
|
download(name, content, type) |
Offers the person a file (CSV, say) |
print(), navigate(page), ready(info)
|
Print, open another page of the package, say the page is complete |
mode, filter (1.4)
|
'view', or 'print' when rendered as a PDF or image; the rows a PDF was limited to |
actions, setActions(changes), savePdf(settings) (1.4)
|
What the viewer offers for this page; turn it off or back on; save as PDF (below) |
edit, await saveChanges({ update, add, delete }) (1.4)
|
What the document may change; save changes (below) |
table, tables, the table option of query and count (1.6.0)
|
The document's own table; every table of the file ({ name, columns, rowCount }); read any of them by name (Several Tables) |
- Queries run in the viewer, which holds the reader and the key; only the requested rows cross into the sandbox.
-
Use relative paths (
img/logo.svg,about.html), plain scripts rather than modules, andwindow.jazminrather thanfetch(). Links to other.htmlfiles in the package open inside the viewer. -
Allowed origins (
package: { allowedOrigins: ['https://fonts.googleapis.com', 'https://fonts.gstatic.com'] }) let the page load scripts, styles, fonts, images and media from thosehttps://origins only. Each is a trade-off: that site learns when the document is opened, the document needs the network to show it, and its scripts run with the data in view. Files a template needs are better embedded: a font is typically 20–100 KB. -
WebAssembly is off unless
package: { allowWasm: true }.
New in 1.4. Versions 1.0 to 1.3 read files with these settings and ignore them, but drop them when they append to or update the file.
Not every page of a package is meant for paper, and not every file is meant to be saved. Each file can say what viewers
may do with it (actions, set when it is added), and the package can give page settings for its PDFs:
import { open, write } from '@smithsoft-studios/jazmin';
write('statement.jzm', [{ account: 'A1', amount: '12.50' }], {
files: [
{ path: 'index.html', content: '<h1>Statement</h1>', actions: { pdf: { format: 'A4', margin: { top: '15mm' } } } },
{ path: 'data.csv', content: 'account,amount\nA1,12.50\n', actions: { open: false, save: false } }, // the page reads it
],
package: { entry: 'index.html', title: 'Statement', pdf: { format: 'A4' } },
});
const reader = open('statement.jzm');
for (const f of reader.files) console.log(f.path, JSON.stringify(f.actions));
console.log(JSON.stringify(reader.package.pdf));
reader.close();data.csv {"open":false,"save":false}
index.html {"pdf":{"format":"A4","margin":{"top":"15mm"}}}
{"format":"A4"}
| Action | Viewers offer |
false for, say |
|---|---|---|
open |
showing the file | a data file only the document reads |
save |
saving (downloading) the file as it is | a logo, a script |
print |
printing the page | an on-screen dashboard |
pdf |
saving the page as PDF; page settings instead of true set its page |
a page that isn't print-ready |
image |
saving the page as an image | a long statement |
-
Each is allowed when left out. The web viewer hides Download for
save: false. - They steer viewers; they don't lock the file. A key that sees a file can always read it with the library. To keep a file from a key, use groups.
-
Page settings (
package.pdffor the document,actions.pdffor one page):format(A0–A6,Letter,Legal,Tabloid,Ledger),landscape,margin({ top, right, bottom, left }:'12mm','1cm','0.5in','20px'),scale(0.1–2),printBackground, andpreferCSSPageSize(the page's CSS@pagesize wins). Writers refuse anything else, so a file never holds settings a browser would reject. -
In .NET:
JazminFileInput.Actions(new JazminFileActions { Save = false, PdfSettings = new JazminPdfSettings { Format = "A4" } }),JazminPackage.Pdf, andreader.Files[i].Actions.
In the page, one template works on screen and on paper:
if (jazmin.mode === 'print') document.body.classList.add('print'); // 'print' when rendered as a PDF or an image
printButton.hidden = !jazmin.actions.print; // { print, pdf, image }: what the viewer offers for this page
jazmin.setActions({ pdf: { landscape: true } }); // this page, while shown: off, back on, or page settings
pdfButton.onclick = () => jazmin.savePdf(); // the web viewer opens the print dialog (Save as PDF)-
jazmin.actionscombines the page's file settings, what the viewer can do, and the page's ownsetActions. A page can turn off what its file allows, but can't allow what its file refuses. Another page starts again from its own file's settings. -
print()andsavePdf()throw when the action isn't allowed. -
On a server,
renderPdfuses the page settings: the package's, then the page's file's, then the page's own, then itspdfoption. See Server-Side.
New in 1.4. Versions 1.0 to 1.3 ignore
package.edit.
A document can let people change the rows it shows (change them, add some, delete some) when its author allows it in
the package: edit: { key, columns, add, delete }. The page calls jazmin.saveChanges(...); the viewer checks the
changes, shows them and asks.
- A file of your own (not shared), opened with its key: the changes are written into it.
-
A shared file: each person works on their own copy. Their changes go back to the owner in a change file,
sealed with their submission key, and the owner applies it with
applyChanges. A browser can't change a file on disk, so the web viewer always saves a change file.
A change file carries the values the person saw, so a row someone else changed in the meantime is held for the owner instead of overwritten:
import { applyChanges, open, update, write, writeChanges } from '@smithsoft-studios/jazmin';
write('tasks.jzm', [{ id: 1, task: 'Call the client', owner: null }, { id: 2, task: 'Send the quote', owner: null }], {
columns: [{ name: 'id', type: 'int', nullable: false }, { name: 'task', type: 'string' }, { name: 'owner', type: 'string' }],
files: [{ path: 'index.html', content: '<h1>Tasks</h1>' }],
package: { entry: 'index.html', edit: { key: ['id'], columns: ['owner'] } },
});
// What a viewer does when the document saves its changes: a change file, with the values the person saw.
const seen = open('tasks.jzm');
const change = writeChanges(seen, { update: [{ id: 1, owner: 'Ann' }, { id: 2, owner: 'Ann' }] });
seen.close();
// Meanwhile someone else gives task 2 to Ben.
update('tasks.jzm', { upsert: [{ id: 2, task: 'Send the quote', owner: 'Ben' }], keyColumns: ['id'] });
const result = applyChanges('tasks.jzm', change);
console.log(`${result.updated} updated`);
for (const c of result.conflicts) {
for (const col of c.columns) console.log(`task ${c.key.id} held: ${col.name} was ${col.before}, wanted ${col.wanted}, now ${col.now}`);
}1 updated
task 2 held: owner was null, wanted Ann, now Ben
- Checked again when applied: what the document allows now, and in a shared file the sender's access: only its partitions and columns. Changes that aren't allowed are refused, with the reason.
-
Held changes (
conflicts): rows changed since the sender's copy, an added row whose key is already there, or a row that's gone.applyChanges(file, change, { overwrite: true })applies them;dryRun: trueshows what would happen without writing. -
In .NET:
JazminPackage.Edit,JazminFile.WriteChangesandJazminFile.ApplyChanges, with the same change files.