# Files: one file at a time - JavaScript bridge API

> The system picker and chunked binary read/write, for files too large to hold in a string.

- **Applies to:** Appwright
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebsitetoapk.com/docs/api/files-io

### `AppMint.downloadFile`

```js
AppMint.downloadFile(data, fileName, mimeType?)
```

**Example**

Saves a file your page made - from a base64 string, a `data:` URL, a `blob:` URL or a Blob - with the file name you choose. Android's "Save as…" sheet opens with that name filled in.

**Returns:** (sync) `true` once the save was handed to Android (`false` only for a `data:` URL it could not parse). Throws `TypeError` if `data` is not a Blob or string. The user still picks the folder, and can cancel. **Needs:** nothing - it is always present in the app.

Base64 string plus a MIME type:

```js
function exportCsv(rows) {
  const csv = rows.map(function (r) { return r.join(','); }).join('\n');
  const b64 = btoa(unescape(encodeURIComponent(csv)));          // UTF-8 safe
  if (window.AppMint && typeof AppMint.downloadFile === 'function') {
    AppMint.downloadFile(b64, 'Ledger_Q3.csv', 'text/csv');
  } else {
    const a = document.createElement('a');                       // a normal browser
    a.href = 'data:text/csv;base64,' + b64;
    a.download = 'Ledger_Q3.csv';
    a.click();
  }
}
```

A `data:` URL (the MIME type comes from the URL) or a Blob:

```js
const png = document.querySelector('canvas').toDataURL('image/png');
AppMint.downloadFile(png, 'Chart.png');

const blob = new Blob([JSON.stringify(state)], { type: 'application/json' });
AppMint.downloadFile(blob, 'Backup.json');                        // same as AppMint.saveBlob
```

A base64 PDF your server returns in JSON; a name without an extension gets `.pdf` from the MIME type:

```js
async function downloadInvoice(id) {
  const res = await fetch('/api/invoices/' + id + '?format=base64');
  const body = await res.json();                                  // { base64: 'JVBERi0x…' }
  if (!window.AppMint || typeof AppMint.downloadFile !== 'function') {
    location.href = 'data:application/pdf;base64,' + body.base64;
    return;
  }
  AppMint.downloadFile(body.base64, 'Invoice_' + id, 'application/pdf');   // saved as Invoice_<id>.pdf
}
```

A `blob:` URL you already made for an on-screen preview:

```js
// earlier: preview.src = URL.createObjectURL(editedImageBlob);
document.getElementById('save-photo').addEventListener('click', function () {
  const preview = document.getElementById('preview');
  if (window.AppMint && typeof AppMint.downloadFile === 'function') {
    AppMint.downloadFile(preview.src, 'Edited_Photo.jpg');
  }
});
```

**Notes:** Only the last part of `fileName` is used (no folders). A name without an extension gets one from the MIME type. The third argument is used only for a plain base64 string. If the phone has no "Save as…" screen at all, the file is written straight into Downloads. Your normal `<a download>` code also works - see `downloads`.

### `AppMint.saveBlob`

```js
AppMint.saveBlob(blob, fileName)
```

**Example**

Saves a Blob (or File) with the name you choose. Android's "Save as…" sheet opens with that name filled in.

**Returns:** (sync) `true` once the save was handed to Android. Throws `TypeError` when the first argument is not a Blob. **Needs:** nothing - it is always present in the app.

```js
async function saveReport() {
  const blob = await makePdfBlob();                    // e.g. jsPDF: doc.output('blob')
  if (window.AppMint && typeof AppMint.saveBlob === 'function') {
    AppMint.saveBlob(blob, 'Site_Report_2026-08.pdf');
    return;
  }
  const url = URL.createObjectURL(blob);               // a normal browser
  const a = document.createElement('a');
  a.href = url;
  a.download = 'Site_Report_2026-08.pdf';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(url); }, 10000);
}
```

A `File` brings its own name, so the name can be left out:

```js
const file = new File(['name,score\n'], 'Scores.csv', { type: 'text/csv' });
AppMint.saveBlob(file);            // saved as Scores.csv
```

A file from your server that needs a sign-in header, saved under a name you choose:

```js
async function saveStatement(month, token) {
  const res = await fetch('/api/statements/' + month, {
    headers: { Authorization: 'Bearer ' + token }
  });
  if (!res.ok) { alert('Could not fetch the statement (' + res.status + ')'); return; }
  const blob = await res.blob();
  if (window.AppMint && typeof AppMint.saveBlob === 'function') {
    AppMint.saveBlob(blob, 'Statement_' + month + '.pdf');
  }
}
```

Several files bundled into one ZIP (here with JSZip) and saved in one go:

```js
async function exportAll(notes) {
  const zip = new JSZip();
  notes.forEach(function (n) { zip.file(n.title + '.txt', n.body); });
  zip.file('index.json', JSON.stringify(notes.map(function (n) { return n.title; })));
  const blob = await zip.generateAsync({ type: 'blob' });
  if (window.AppMint && typeof AppMint.saveBlob === 'function') {
    AppMint.saveBlob(blob, 'Notes_' + new Date().toISOString().slice(0, 10) + '.zip');
  }
}
```

**Notes:** The Blob is read into memory and sent to Android in one piece, so very large blobs (hundreds of MB) may run out of memory. The user picks the folder and may cancel; nothing tells the page which one happened.

### `AppMint.setDownloadName`

```js
AppMint.setDownloadName(url, fileName)
```

**Example**

Gives a `blob:` or `data:` URL the file name it should be saved under, before your code (or a library) triggers the download.

**Returns:** nothing. **Needs:** nothing - it is always present in the app.

Useful when a library downloads a URL you built earlier and you cannot pass it a name:

```js
const blob = new Blob([csvText], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
if (window.AppMint && typeof AppMint.setDownloadName === 'function') {
  AppMint.setDownloadName(url, 'Invoice_0042.csv');
}
location.href = url;           // saved as Invoice_0042.csv, not "download.csv"
```

A PDF your page opens with `window.open` goes to the phone's PDF viewer; name it first so it is not "download.pdf":

```js
function previewPdf(pdfBlob, invoiceNo) {
  const url = URL.createObjectURL(pdfBlob);
  if (window.AppMint && typeof AppMint.setDownloadName === 'function') {
    AppMint.setDownloadName(url, 'Invoice_' + invoiceNo + '.pdf');
  }
  window.open(url, '_blank');
}
```

A `data:` URL works the same way, e.g. a chart image a library links to without a `download` attribute:

```js
const dataUrl = chart.toBase64Image();         // e.g. Chart.js: 'data:image/png;base64,…'
if (window.AppMint && typeof AppMint.setDownloadName === 'function') {
  AppMint.setDownloadName(dataUrl, 'Sales_by_month.png');
}
const link = document.getElementById('chart-link');
link.href = dataUrl;                           // a tap on this link saves Sales_by_month.png
link.textContent = 'Save chart';
```

**Notes:** The name is remembered even after `URL.revokeObjectURL(url)`, because some libraries revoke before the save finishes. You rarely need this: `<a download="name">`, `URL.createObjectURL(file)` (a `File` has a name) and FileSaver's `saveAs(blob, name)` are all picked up automatically. The name `'download'` is ignored as "no name".

### `WebToApkFS.maxChunkSize`

```js
WebToApkFS.maxChunkSize()
```

**Example**

Tells you the most bytes one `readBytes` or `writeBytes` call can move (4 MB), so you can size your chunk loop.

**Returns:** (sync) a number of bytes, or `0` when the bridge method is missing. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build (that is what injects `WebToApkFS`).

```js
function copyFile(fromUri, toPath) {
  if (!window.WebToApkFS) return { ok: false, error: 'not in the app' };
  const step = WebToApkFS.maxChunkSize() || 1048576;   // fall back to 1 MB pieces
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(fromUri, offset, step);
    if (!r.ok) return r;
    if (r.bytesRead > 0) {
      const w = WebToApkFS.writeBytes(toPath, r.base64, offset, offset === 0 ? 'truncate' : 'patch');
      if (!w.ok) return w;
    }
    offset += r.bytesRead;
    if (r.eof || r.bytesRead <= 0) return { ok: true, size: offset };
  }
}
```

Show read progress: the chunk size tells you how many steps a file takes:

```js
function readWithProgress(uri, onProgress) {
  const info = WebToApkFS.stat(uri);
  if (!info.ok) return info;
  const step = WebToApkFS.maxChunkSize() || 1048576;
  const steps = Math.max(1, Math.ceil(info.size / step));
  for (let i = 0; i < steps; i++) {
    const r = WebToApkFS.readBytes(uri, i * step, step);
    if (!r.ok) return r;
    onProgress(Math.round(((i + 1) / steps) * 100), r.base64);
  }
  return { ok: true, size: info.size };
}
```

Append a large byte array to a file in the granted folder, one allowed piece at a time:

```js
function appendBytes(path, bytes) {                    // bytes: a Uint8Array
  const step = WebToApkFS.maxChunkSize();
  if (!step) return { ok: false, error: 'not in the app' };
  for (let at = 0; at < bytes.length; at += step) {
    const piece = bytes.subarray(at, at + step);
    let bin = '';
    for (let i = 0; i < piece.length; i += 0x8000) bin += String.fromCharCode.apply(null, piece.subarray(i, i + 0x8000));
    const w = WebToApkFS.writeBytes(path, btoa(bin), 0, 'append');   // 'append' ignores the offset
    if (!w.ok) return w;
  }
  return { ok: true };
}
```

**Notes:** A smaller chunk keeps the page more responsive (each call is synchronous); the cap only says how big one call may be.

### `WebToApkFS.pickFile`

```js
WebToApkFS.pickFile(mimeFilter)
```

**Example**

Opens the Android file picker so the user chooses ONE file; your page may then read and write that file's real bytes, even after the app restarts.

**Returns:** `Promise<{ok, uri, name, size, mime, error}>`. On success `ok` is `true` and `uri` is a `content://` address you pass to `stat`, `readBytes`, `writeBytes`. On failure `ok` is `false` and `error` is `'cancelled'`, `'disabled'`, `'no-picker'`, `'busy'` (a picker is already open for an earlier call, which keeps its own answer), `'foreign_origin'` or `'unsupported'` (no app). **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

```js
async function openOneFile() {
  if (!window.WebToApkFS) { alert('Open this page in the app to pick a file.'); return; }
  const f = await WebToApkFS.pickFile('application/pdf');   // '' = any file, 'image/*' = pictures
  if (!f.ok) {
    if (f.error !== 'cancelled') alert('Cannot open a file: ' + f.error);
    return;
  }
  console.log(f.name, f.size + ' bytes', f.mime);
  localStorage.setItem('lastFile', f.uri);   // the grant survives a restart
}
```

Use the saved `uri` again later, without asking the user a second time:

```js
const uri = localStorage.getItem('lastFile');
if (uri && window.WebToApkFS) {
  const info = WebToApkFS.stat(uri);
  if (info.ok) console.log('still here:', info.name);
  else console.log('file is gone or access was removed:', info.error);
}
```

The filter takes ONE pattern, so pick broadly and check the name yourself:

```js
async function importTable() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/*');        // CSV and TSV files are both text/*
  if (!f.ok) {
    if (f.error === 'busy') return;                     // a picker is already open
    if (f.error !== 'cancelled') alert('Cannot open a file: ' + f.error);
    return;
  }
  if (!/\.(csv|tsv)$/i.test(f.name)) {
    WebToApkFS.releaseFile(f.uri);
    alert(f.name + ' is not a CSV or TSV file.');
    return;
  }
  const r = WebToApkFS.readAll(f.uri);
  if (r.ok) showTable(atob(r.base64), f.name.toLowerCase().endsWith('.tsv') ? '\t' : ',');
  WebToApkFS.releaseFile(f.uri);
}
```

**Notes:** The filter is one MIME type pattern (`'image/*'`, `'text/csv'`), not a list. The picker asks for read AND write access and keeps it. Android limits how many files an app may keep this way, so call `WebToApkFS.releaseFile(uri)` when you are done with a file. For a whole folder use `WebToApkFS.requestAccess()` instead. `WebToApkFS` is installed when Native Folder Access or Secure Keys is ticked; its calls work only with Native Folder Access (`WebToApkFS.isAvailable()`).

### `WebToApkFS.readAll`

```js
WebToApkFS.readAll(pathOrUri)
```

**Example**

Reads a whole file into one base64 string. It loops over `readBytes` for you, 4 MB at a time.

**Returns:** (sync) `{ok:true, base64, size}`, or the first failing chunk's `{ok:false, error}`. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

Load a picked image into an `<img>`:

```js
async function showPicture() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('image/*');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);
  if (!r.ok) { alert('Read failed: ' + r.error); return; }
  document.getElementById('photo').src = 'data:' + f.mime + ';base64,' + r.base64;
}
```

Read a UTF-8 text file (works for any language, not only English):

```js
function readUtf8(pathOrUri) {
  const r = WebToApkFS.readAll(pathOrUri);
  if (!r.ok) throw new Error(r.error);
  const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
  return new TextDecoder('utf-8').decode(bytes);
}
```

Show a file's SHA-256 checksum, for example to compare a download with the published one:

```js
async function checksum() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);
  if (!r.ok) { alert('Read failed: ' + r.error); return; }
  const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
  const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));
  const hex = Array.from(hash, function (b) { return b.toString(16).padStart(2, '0'); }).join('');
  console.log(f.name, r.size + ' bytes', hex);
}
```

**Notes:** The whole file ends up in memory (as bytes and then as base64), and the call blocks the page while it runs. Use it for files that easily fit in memory; for large files loop `WebToApkFS.readBytes` and handle each chunk. For plain text inside the granted folder, `WebToApkFS.readText(path)` is simpler.

### `WebToApkFS.readBytes`

```js
WebToApkFS.readBytes(pathOrUri, offset, length)
```

**Example**

Reads the real bytes of a file, starting at a byte position, as base64. Safe for images, PDFs, ZIPs - nothing is changed by text decoding.

**Returns:** (sync) `{ok:true, base64, bytesRead, offset, size, eof}` or `{ok:false, error}` (`'not-found'`, `'disabled'`, `'cannot-open'`, `'offset past end of file (N bytes)'`, `'foreign_origin'`, `'unsupported'`). **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

Read the first 64 KB, for example to check a file header:

```js
async function checkPng() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('image/*');
  if (!f.ok) return;
  const r = WebToApkFS.readBytes(f.uri, 0, 65536);
  if (!r.ok) { alert('Read failed: ' + r.error); return; }
  const head = atob(r.base64).slice(1, 4);
  console.log(head === 'PNG' ? 'a PNG' : 'not a PNG', '- file is', r.size, 'bytes');
}
```

Read a big file piece by piece, so it never has to fit in memory at once:

```js
function eachChunk(uri, onChunk) {
  const step = WebToApkFS.maxChunkSize() || 1048576;
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(uri, offset, step);
    if (!r.ok) return r;                 // stop on the first error
    onChunk(r.base64, offset);
    offset += r.bytesRead;
    if (r.eof || r.bytesRead <= 0) return { ok: true, size: offset };
  }
}
```

Read only the end of a file, for example the 128-byte tag at the end of an MP3:

```js
function readMp3Title(uri) {
  const info = WebToApkFS.stat(uri);
  if (!info.ok || info.size < 128) return null;
  const r = WebToApkFS.readBytes(uri, info.size - 128, 128);
  if (!r.ok) return null;
  const tail = atob(r.base64);
  if (tail.slice(0, 3) !== 'TAG') return null;          // no old-style tag in this file
  return tail.slice(3, 33).replace(/\0+$/, '').trim();  // bytes 3-32 hold the title
}
```

**Notes:** One call reads at most `WebToApkFS.maxChunkSize()` bytes (4 MB); `length` 0 means "as much as one call allows". Loop on `bytesRead` and stop on `eof`. The call is synchronous, so keep chunks moderate on the UI thread. For small files `WebToApkFS.readAll(uri)` does the loop for you.

### `WebToApkFS.readSealed`

```js
WebToApkFS.readSealed(pathOrUri, alias)
```

**Example**

Reads a file written by `WebToApkFS.writeSealed` and decrypts it with the same keystore key.

**Returns:** (sync) `{ok:true, base64}` (the original bytes), or `{ok:false, error}` - `'keystore-disabled'`, `'corrupt-sealed-file'`, a keystore code (`'no-such-key'` after the key was deleted, `'auth-required'`, `'key-invalidated'`) or a file error (`'not-found'` …). **Needs:** tick **Native Folder Access (SAF)** and **Secure Keys (encryption)** in Step 4 (Access) when you build.

```js
function loadSecret() {
  if (!window.WebToApkFS || !window.AppMintKeystore || !AppMintKeystore.isAvailable()) return null;
  const out = WebToApkFS.readSealed('secret.bin', 'vault');
  if (out.ok) {
    const bytes = Uint8Array.from(atob(out.base64), function (c) { return c.charCodeAt(0); });
    return new TextDecoder().decode(bytes);
  }
  if (out.error === 'auth-required') {
    alert('Unlock your phone, then try again.');
  } else if (out.error === 'no-such-key') {
    alert('This note was deleted.');
  } else {
    alert('Could not open the note: ' + out.error);
  }
  return null;
}
```

Keep app settings in a sealed JSON file, with defaults before the first save:

```js
function loadSettings() {
  const defaults = { theme: 'light', pinHint: '' };
  const out = WebToApkFS.readSealed('settings.sealed', 'settings');
  if (!out.ok) {
    if (out.error === 'not-found') return defaults;      // nothing saved yet
    throw new Error('Settings could not be opened: ' + out.error);
  }
  const bytes = Uint8Array.from(atob(out.base64), function (c) { return c.charCodeAt(0); });
  return Object.assign(defaults, JSON.parse(new TextDecoder().decode(bytes)));
}
```

Move a sealed file to a new key, then destroy the old key:

```js
function rekey(path, oldAlias, newAlias) {
  const plain = WebToApkFS.readSealed(path, oldAlias);
  if (!plain.ok) return plain;
  if (!AppMintKeystore.hasKey(newAlias)) {
    const k = AppMintKeystore.generateKey(newAlias, {});
    if (!k.ok) return k;
  }
  const w = WebToApkFS.writeSealed(path, plain.base64, newAlias);
  if (!w.ok) return w;
  AppMintKeystore.deleteKey(oldAlias);                  // only after the new copy is written
  return { ok: true };
}
```

**Notes:** It fails for good once `AppMintKeystore.deleteKey(alias)` has run - that is the point. The whole file is read and decrypted in memory, synchronously; keep sealed files small (notes, keys, tokens).

### `WebToApkFS.releaseFile`

```js
WebToApkFS.releaseFile(uri)
```

**Example**

Gives back the long-term access your app got when the user picked a file with `WebToApkFS.pickFile`.

**Returns:** (sync) `true` when the grant was released, `false` otherwise (no grant, bad URI, not in the app). **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

```js
async function importOnce() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/csv');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);
  if (r.ok) importCsv(atob(r.base64));
  WebToApkFS.releaseFile(f.uri);       // done with it: do not keep the grant
}
```

When the user picks a new default file, give back the grant for the old one:

```js
async function chooseDefaultFile() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('');
  if (!f.ok) return;
  const old = localStorage.getItem('defaultFile');
  if (old && old !== f.uri) WebToApkFS.releaseFile(old);
  localStorage.setItem('defaultFile', f.uri);
}
```

A "Forget recent files" button that releases every file the app remembered:

```js
function forgetRecentFiles() {
  const recent = JSON.parse(localStorage.getItem('recentFiles') || '[]');
  let released = 0;
  for (const uri of recent) {
    if (WebToApkFS.releaseFile(uri)) released++;       // false: already gone or never granted
  }
  localStorage.removeItem('recentFiles');
  console.log('released', released, 'of', recent.length);
}
```

**Notes:** Android caps how many picked files one app may keep access to. An app that picks many files (an importer, a viewer) should release each one when finished. After release, `stat` / `readBytes` / `writeBytes` on that URI answer `not-found`. It does not delete the file.

### `WebToApkFS.stat`

```js
WebToApkFS.stat(pathOrUri)
```

**Example**

Tells you a file's name, size, type and date without reading its contents.

**Returns:** (sync) `{ok:true, name, size, mime, lastModified, uri, isDirectory, canWrite}`, or `{ok:false, error}` with `error` = `'not-found'`, `'disabled'`, `'foreign_origin'` or `'unsupported'`. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

```js
async function showInfo() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('');
  if (!f.ok) return;
  const info = WebToApkFS.stat(f.uri);
  if (!info.ok) { alert('Cannot read file info: ' + info.error); return; }
  console.log(info.name, info.size + ' bytes', info.mime,
              new Date(info.lastModified).toLocaleString(),
              info.canWrite ? 'writable' : 'read only');
}
```

A path inside the folder the user granted with `WebToApkFS.requestAccess()` works too:

```js
const info = WebToApkFS.stat('photos/cover.jpg');
if (info.ok && !info.isDirectory) console.log('cover is', info.size, 'bytes');
```

Re-import a remembered file only when it changed since the last import:

```js
function importIfChanged(uri) {
  const info = WebToApkFS.stat(uri);
  if (!info.ok) return 'gone: ' + info.error;
  const seen = JSON.parse(localStorage.getItem('lastImport') || '{}');
  if (seen.size === info.size && seen.lastModified === info.lastModified) return 'unchanged';
  const r = WebToApkFS.readAll(uri);
  if (!r.ok) return 'read failed: ' + r.error;
  importData(atob(r.base64));
  localStorage.setItem('lastImport', JSON.stringify({ size: info.size, lastModified: info.lastModified }));
  return 'imported';
}
```

**Notes:** `pathOrUri` is a `content://` URI from `pickFile()` or a path relative to the granted folder. A URI your app was never given returns `not-found`. `lastModified` is milliseconds since 1970 (0 when the provider does not say).

### `WebToApkFS.writeAll`

```js
WebToApkFS.writeAll(pathOrUri, base64)
```

**Example**

Replaces a file's whole contents with your bytes. It splits the data into 4 MB pieces for you: the first piece truncates the file, the rest are patched in after it.

**Returns:** (sync) `{ok:true, bytesWritten}`, or the first failing piece's `{ok:false, error}`. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

Save UTF-8 text (any language) back to a file the user picked:

```js
function bytesToBase64(bytes) {
  let s = '';
  for (let i = 0; i < bytes.length; i += 0x8000) {
    s += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
  }
  return btoa(s);
}

async function saveNote(text) {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/plain');
  if (!f.ok) return;
  const r = WebToApkFS.writeAll(f.uri, bytesToBase64(new TextEncoder().encode(text)));
  if (r.ok) console.log('saved', r.bytesWritten, 'bytes');
  else alert('Could not save: ' + r.error);
}
```

Write a canvas image into the granted folder (the file is created if missing):

```js
const b64 = document.querySelector('canvas').toDataURL('image/png').split(',')[1];
const r = WebToApkFS.writeAll('drawings/sketch.png', b64);
if (!r.ok) alert('Could not save: ' + r.error);
```

Save a JSON backup into the granted folder, asking for a folder first if there is none, then confirm its size on disk:

```js
async function backup(data) {
  if (!window.WebToApkFS || !WebToApkFS.isAvailable()) return;
  if (!WebToApkFS.hasAccess()) {
    const a = await WebToApkFS.requestAccess();
    if (!a.success) return;
  }
  const bytes = new TextEncoder().encode(JSON.stringify(data));
  let bin = '';
  for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
  const r = WebToApkFS.writeAll('backup.json', btoa(bin));
  if (!r.ok) { alert('Backup failed: ' + r.error); return; }
  const info = WebToApkFS.stat('backup.json');
  console.log('backup is', info.ok ? info.size : '?', 'bytes');
}
```

**Notes:** The whole input sits in memory while it is written, and the call blocks the page. If a later piece fails, the file already holds the earlier pieces - check `ok` and tell the user. To keep a file secret, use `WebToApkFS.writeSealed` instead.

### `WebToApkFS.writeBytes`

```js
WebToApkFS.writeBytes(pathOrUri, base64, offset, mode)
```

**Example**

Writes real bytes into a file and tells you how many landed. It can patch bytes in the middle, replace the whole file, or append to the end.

**Returns:** (sync) `{ok, bytesWritten, offset, size}` - `size` is read back from the file after the data is flushed to disk - or `{ok:false, error}`. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

**Patch** - change 4 bytes at position 100, keep the rest of the file:

```js
const r = WebToApkFS.writeBytes(fileUri, btoa('ABCD'), 100, 'patch');
if (r.ok) console.log('file is now', r.size, 'bytes');
else alert('Write failed: ' + r.error);
```

**Truncate** - replace the whole file (offset is ignored):

```js
const csv = 'name,score\nAsha,12\n';
const r = WebToApkFS.writeBytes('exports/scores.csv', btoa(csv), 0, 'truncate');
if (!r.ok) alert('Could not save: ' + r.error);
```

**Append** - add to the end (offset is ignored):

```js
const line = new Date().toISOString() + ' opened\n';
WebToApkFS.writeBytes('log.txt', btoa(line), 0, 'append');
```

**Notes:** `mode` is `'patch'` (default), `'truncate'` or `'append'`. At most 4 MB per call (`WebToApkFS.maxChunkSize()`); for bigger data use `WebToApkFS.writeAll`. A path inside the granted folder is created if it does not exist; a picked `content://` URI must be one the user chose. `btoa` only accepts Latin-1 text - for other text encode to bytes first (see `WebToApkFS.writeAll`). Errors: `disabled`, `foreign_origin` (the page on screen is not the app's own), `bad mode: x`, `bad base64`, `chunk too large (max N bytes)`, `not-found`, `cannot-open`, `short write`.

### `WebToApkFS.writeSealed`

```js
WebToApkFS.writeSealed(pathOrUri, base64, alias)
```

**Example**

Writes a file encrypted with a hardware-backed key from `AppMintKeystore`. Deleting that key later makes the file unreadable for everyone - the only real "secure delete" on a phone.

**Returns:** (sync) `{ok:true, bytesWritten}` (encrypted size on disk), or `{ok:false, error}` - `'keystore-disabled'`, a keystore code such as `'no-such-key'`, `'auth-required'`, `'key-invalidated'`, or a file error. **Needs:** tick **Native Folder Access (SAF)** and **Secure Keys (encryption)** in Step 4 (Access) when you build.

```js
function saveSecret(text) {
  if (!window.WebToApkFS || !window.AppMintKeystore || !AppMintKeystore.isAvailable()) {
    alert('Secure storage is not available in this build.');
    return false;
  }
  if (!AppMintKeystore.hasKey('vault')) {
    const k = AppMintKeystore.generateKey('vault', {});
    if (!k.ok) { alert('Could not create key: ' + k.error); return false; }
  }
  const bytes = new TextEncoder().encode(text);
  let bin = '';
  for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
  const r = WebToApkFS.writeSealed('secret.bin', btoa(bin), 'vault');
  if (!r.ok) alert('Could not save: ' + r.error);
  return r.ok;
}
```

Delete it for real - destroy the key, then remove the file:

```js
AppMintKeystore.deleteKey('vault');   // secret.bin can never be decrypted again
WebToApkFS.delete('secret.bin');
```

Seal a photo the user chose, as raw bytes, into the folder they granted:

```js
async function sealPhoto(file) {                  // a File from <input type="file" accept="image/*">
  if (!window.WebToApkFS || !window.AppMintKeystore || !AppMintKeystore.isAvailable()) return false;
  if (!WebToApkFS.hasAccess()) {
    const g = await WebToApkFS.requestAccess();
    if (!g.success) return false;
  }
  if (!AppMintKeystore.hasKey('vault')) AppMintKeystore.generateKey('vault', {});
  const dataUrl = await new Promise(function (resolve, reject) {
    const fr = new FileReader();
    fr.onload = function () { resolve(fr.result); };
    fr.onerror = reject;
    fr.readAsDataURL(file);
  });
  const b64 = dataUrl.slice(dataUrl.indexOf(',') + 1);
  const r = WebToApkFS.writeSealed('photo-' + Date.now() + '.sealed', b64, 'vault');
  if (!r.ok) alert('Not saved: ' + r.error);
  return r.ok;
}
```

A diary key that works only for 5 minutes after the phone was unlocked, with each error explained:

```js
if (!AppMintKeystore.hasKey('diary')) {
  AppMintKeystore.generateKey('diary', { requireAuth: true, authValiditySeconds: 300 });
}

function saveDiary(entry) {
  const b64 = btoa(unescape(encodeURIComponent(JSON.stringify(entry))));   // UTF-8 safe
  const r = WebToApkFS.writeSealed('diary.bin', b64, 'diary');
  if (r.ok) return true;
  if (r.error === 'auth-required') showMessage('Unlock your phone again, then tap Save.');
  else if (r.error === 'key-invalidated') showMessage('Your screen lock changed, so this key no longer works.');
  else showMessage('Could not save: ' + r.error);
  return false;
}
```

**Notes:** Do not "overwrite with zeros" to erase: phone flash storage keeps the old blocks. The file format is a list of length-prefixed AES-GCM pieces; read it back only with `WebToApkFS.readSealed` and the same alias. A key created with `{requireAuth:true}` answers `auth-required` until the user has unlocked recently.

### `downloads`

```js
<a download> and Blob downloads
```

**Example**

Your normal web download code works in the app, the way it does in Chrome. A WebView ignores `blob:` and `data:` downloads on its own; the app catches them and saves the file to the phone's **Downloads** folder under **your** file name, then shows "Downloaded <name> · Open" and a "Download complete" notification.

**Returns:** nothing to the page. **Needs:** nothing to switch on. Web-address downloads (`https://…`) go to the Downloads folder too (a PDF or image first asks "View or Download"), and so do files packed inside the app (`<a href="manual.pdf" download>`); on Android 9 and older the app asks for storage permission first. Large files are fine: the file is streamed to the app in pieces.

`<a download>` with a Blob - the most common pattern:

```html
<button id="save">Download report</button>
<script>
document.getElementById('save').addEventListener('click', function () {
  const blob = new Blob(['Name,Total\nAsha,120\n'], { type: 'text/csv' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'Report_2026-08.csv';     // this is the name the user sees
  document.body.appendChild(a);
  a.click();                             // caught by the app, saved to Downloads
  a.remove();
});
</script>
```

A `data:` URL, FileSaver.js, and `navigator.msSaveBlob` also work:

```js
const a = document.createElement('a');
a.href = canvas.toDataURL('image/png');
a.download = 'Drawing.png';
a.click();

if (typeof saveAs === 'function') saveAs(blob, 'Backup.json');   // FileSaver.js
if (navigator.msSaveBlob) navigator.msSaveBlob(blob, 'Backup.json');
```

`window.open` of a `blob:` or `data:` URL means "show it", not "save it": a PDF opens in the app's PDF viewer, other types in the phone's app for that type:

```js
const pdfUrl = URL.createObjectURL(pdfBlob);
const w = window.open(pdfUrl);    // never null in the app, so no "popup blocked" message
```

**Notes:** Without a name the file is saved as `download.<ext>`; a second file with the same name becomes `name (1).ext`. A `File` object brings its own name (`URL.createObjectURL(file)`). `<a href="/files/x" download="Name.pdf">` on your own site keeps `Name.pdf` (Chrome ignores the attribute on other sites' links, and so does the app). If no app on the phone can show a `window.open` file, it is saved to Downloads instead. To let the user choose the folder, use `showSaveFilePicker()`. To save without an `<a>` element, use `AppMint.saveBlob(blob, name)` or `AppMint.downloadFile(base64, name, mime)`.

### `input-file`

```js
<input type="file"> — camera, photos and files
```

**Example**

A normal file input works in the app like in Chrome: it opens the photo picker, the camera, or the file picker, and your page gets ordinary `File` objects.

**Returns:** the input's `change` event with `input.files` (empty when the user cancels). **Needs:** photos and files need nothing. For `capture` (the camera) tick **Camera** in Step 4 (Access) when you build; the app asks for the camera permission the first time.

Photos - opens the Android photo picker (no permission prompt; the page gets only what the user chose):

```html
<input type="file" id="pics" accept="image/*" multiple>
<div id="preview"></div>
<script>
document.getElementById('pics').addEventListener('change', function (e) {
  const box = document.getElementById('preview');
  for (let i = 0; i < e.target.files.length; i++) {
    const img = document.createElement('img');
    img.src = URL.createObjectURL(e.target.files[i]);
    img.style.width = '100px';
    box.appendChild(img);
  }
});
</script>
```

Camera - `capture` skips the picker and opens the camera straight away:

```html
<input type="file" id="snap" accept="image/*" capture="environment">
<input type="file" id="clip" accept="video/*" capture="environment">
<script>
document.getElementById('snap').addEventListener('change', function (e) {
  const photo = e.target.files[0];
  if (!photo) return;                        // user closed the camera
  const form = new FormData();
  form.append('photo', photo);
  fetch('/api/upload', { method: 'POST', body: form });
});
</script>
```

Any file, or several types (extensions work too):

```html
<input type="file" accept=".pdf,.csv,application/json" multiple>
```

| You write | What opens |
|---|---|
| `accept="image/*"` / `"video/*"` / `"image/*,video/*"` | the photo picker (photos, videos, or both) |
| add `multiple` | the user can pick several |
| add `capture` (with **Camera** ticked) | the camera directly - photo for `image/*`, video for `video/*` |
| `accept="image/*,.pdf"` or no `accept` | the system file picker |

**Notes:** Without **Camera** ticked, `capture` is ignored and the picker opens. When **Native Folder Access (SAF)** is ticked, the app shows its own "Select Upload Source" menu instead: Take Photo / Record Video (if Camera is ticked), Choose Files, and Upload Folder (sends every file in a folder). With **Storage** ticked on Android 12 and older, choosing a non-photo file may ask for storage permission first. Phones without the photo picker get the file picker, which shows the same photos.

### `showOpenFilePicker`

```js
showOpenFilePicker / showSaveFilePicker / showDirectoryPicker — File System Access
```

**Example**

The standard File System Access API works inside the app, over Android's own document picker: open a file and save back INTO THAT FILE, create a file with a Save-as sheet, or get a whole folder to read, create and delete files in.

**Returns:** `showOpenFilePicker(options)` → Promise of an array of `FileSystemFileHandle`; `showSaveFilePicker(options)` → one `FileSystemFileHandle`; `showDirectoryPicker(options)` → one `FileSystemDirectoryHandle`. A cancelled picker rejects `AbortError`; no user gesture rejects `SecurityError`; a second picker while one is open rejects `NotAllowedError`. **Needs:** nothing to switch on and no permission - the user picks.

```js
let handle = null;

document.getElementById('open').addEventListener('click', async () => {
  try {
    [handle] = await showOpenFilePicker({
      types: [{ description: 'Text', accept: { 'text/plain': ['.txt', '.md'] } }]
    });
    const file = await handle.getFile();
    editor.value = await file.text();
    title.textContent = file.name;
  } catch (e) {
    if (e.name !== 'AbortError') alert('Could not open: ' + e.message);
  }
});

document.getElementById('save').addEventListener('click', async () => {
  if (!handle) handle = await showSaveFilePicker({ suggestedName: 'note.txt' });
  const w = await handle.createWritable();   // writes go to a copy…
  await w.write(editor.value);
  await w.close();                           // …that replaces the file only here
});

document.getElementById('export').addEventListener('click', async () => {
  const dir = await showDirectoryPicker({ mode: 'readwrite' });
  const out = await dir.getFileHandle('export.csv', { create: true });
  const w = await out.createWritable();
  await w.write(csvText);
  await w.close();
  for await (const [name, entry] of dir.entries()) console.log(entry.kind, name);
});
```

Pick several photos at once, starting in the Pictures folder:

```js
document.getElementById('add-photos').addEventListener('click', async () => {
  try {
    const handles = await showOpenFilePicker({
      multiple: true,
      startIn: 'pictures',
      types: [{ description: 'Images', accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.webp'] } }]
    });
    for (const h of handles) {
      const img = document.createElement('img');
      img.src = URL.createObjectURL(await h.getFile());
      gallery.appendChild(img);
    }
  } catch (e) {
    if (e.name !== 'AbortError') alert('Could not open: ' + e.message);
  }
});
```

Add a line to the end of a log file without rewriting what is already there:

```js
async function appendLine(handle, line) {      // handle from showOpenFilePicker / showSaveFilePicker
  const size = (await handle.getFile()).size;
  const w = await handle.createWritable({ keepExistingData: true });
  await w.seek(size);
  await w.write(line + '\n');
  await w.close();
}
```

Clean a folder the user picked: delete old `.tmp` files and sub-folders named `cache`:

```js
document.getElementById('clean').addEventListener('click', async () => {
  const dir = await showDirectoryPicker({ mode: 'readwrite' });
  let removed = 0;
  for await (const entry of dir.values()) {
    if (entry.kind === 'file' && entry.name.endsWith('.tmp')) {
      await dir.removeEntry(entry.name);
      removed++;
    } else if (entry.kind === 'directory' && entry.name === 'cache') {
      await dir.removeEntry(entry.name, { recursive: true });
      removed++;
    }
  }
  alert(removed + ' item(s) removed');
});
```

**Notes:** `createWritable()` follows the spec's swap-file model - `abort()` (or the page going away) leaves the original untouched; `{ keepExistingData: true }` starts from the current content; `write({type:'seek'|'truncate'|'write', …})`, `seek()` and `truncate()` work. A file its app offers read-only answers `queryPermission({mode:'readwrite'})` with `'denied'` and `createWritable()` rejects `NoModificationAllowedError`. `getFile()` streams the document, so large files are fine. Handles last as long as the page - they cannot be stored in IndexedDB and reused after a restart. `types` become the picker's file filter. Only the top-level page has the pickers. Internal transports: `__fsPick` (answered with the `appmint:fs` event), `__fsFile`, `__fsList`, `__fsChild`, `__fsRemove`, `__fsWritableOpen`/`Write`/`Truncate`/`Close`/`Abort`. AI-built apps check `can('fileHandles')`.

### `__dlAbort`

```js
window.WebToApk.__dlAbort(token: String)
```

Drops download [token] (the page's read failed or it gave up).

**Example**

Documented together with `WebToApk.__dlBegin` - the example there shows this one too.

Internal transport behind `<a download>`, Blob downloads, `window.open(blobUrl)` and `AppMint.saveBlob` / `AppMint.downloadFile`: the file is streamed from the page to the app in slices, so a large export is never one giant string in memory. Pages use the standard download code; never call these directly, because the `__` methods may change without notice.

**Returns:** `__dlBegin(name, type, size)` returns a token (or `""` when the phone has no room); `__dlChunk(token, base64)` returns `false` if a slice could not be written; `__dlEnd(token, action)` finishes (`save` = Downloads, `open` = show it, `share`, `copy`). **Needs:** nothing.

**Use the public API:**

```js
const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"
```

Export a drawing from a canvas through the same route:

```js
canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');
```

Save a JSON backup the user can keep, with a message when it is done:

```js
function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}
```

**Notes:** Implemented in the app shell (ShellDownloads.kt).

### `__dlBegin`

```js
window.WebToApk.__dlBegin(name: String, type: String, size: Double): String
```

Opens a download of [size] bytes named [name] ([type] = its MIME type) that the page will stream with __dlChunk. Returns a token, or "" when there is no room.

**Example**

Internal transport behind `<a download>`, Blob downloads, `window.open(blobUrl)` and `AppMint.saveBlob` / `AppMint.downloadFile`: the file is streamed from the page to the app in slices, so a large export is never one giant string in memory. Pages use the standard download code; never call these directly, because the `__` methods may change without notice.

**Returns:** `__dlBegin(name, type, size)` returns a token (or `""` when the phone has no room); `__dlChunk(token, base64)` returns `false` if a slice could not be written; `__dlEnd(token, action)` finishes (`save` = Downloads, `open` = show it, `share`, `copy`). **Needs:** nothing.

**Use the public API:**

```js
const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"
```

Export a drawing from a canvas through the same route:

```js
canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');
```

Save a JSON backup the user can keep, with a message when it is done:

```js
function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}
```

**Notes:** Implemented in the app shell (ShellDownloads.kt).

### `__dlChunk`

```js
window.WebToApk.__dlChunk(token: String, base64: String): Boolean
```

One base64 slice of the download [token]; false when the transfer failed.

**Example**

Documented together with `WebToApk.__dlBegin` - the example there shows this one too.

Internal transport behind `<a download>`, Blob downloads, `window.open(blobUrl)` and `AppMint.saveBlob` / `AppMint.downloadFile`: the file is streamed from the page to the app in slices, so a large export is never one giant string in memory. Pages use the standard download code; never call these directly, because the `__` methods may change without notice.

**Returns:** `__dlBegin(name, type, size)` returns a token (or `""` when the phone has no room); `__dlChunk(token, base64)` returns `false` if a slice could not be written; `__dlEnd(token, action)` finishes (`save` = Downloads, `open` = show it, `share`, `copy`). **Needs:** nothing.

**Use the public API:**

```js
const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"
```

Export a drawing from a canvas through the same route:

```js
canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');
```

Save a JSON backup the user can keep, with a message when it is done:

```js
function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}
```

**Notes:** Implemented in the app shell (ShellDownloads.kt).

### `__dlEnd`

```js
window.WebToApk.__dlEnd(token: String, action: String): Boolean
```

Finishes download [token] and acts on it: [action] "save" (Downloads, with the "Downloaded · Open" bar and notification), "open" (PDF viewer or the phone's app for the type), "share" (share sheet) or "copy" (image to the clipboard).

**Example**

Documented together with `WebToApk.__dlBegin` - the example there shows this one too.

Internal transport behind `<a download>`, Blob downloads, `window.open(blobUrl)` and `AppMint.saveBlob` / `AppMint.downloadFile`: the file is streamed from the page to the app in slices, so a large export is never one giant string in memory. Pages use the standard download code; never call these directly, because the `__` methods may change without notice.

**Returns:** `__dlBegin(name, type, size)` returns a token (or `""` when the phone has no room); `__dlChunk(token, base64)` returns `false` if a slice could not be written; `__dlEnd(token, action)` finishes (`save` = Downloads, `open` = show it, `share`, `copy`). **Needs:** nothing.

**Use the public API:**

```js
const blob = new Blob([csvText], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'Report.csv';
a.click();   // lands in Downloads as "Report.csv"
```

Export a drawing from a canvas through the same route:

```js
canvas.toBlob(function (blob) {
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'drawing.png';
  a.click();
  setTimeout(function () { URL.revokeObjectURL(a.href); }, 10000);
}, 'image/png');
```

Save a JSON backup the user can keep, with a message when it is done:

```js
function exportBackup(data) {
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'backup-' + new Date().toISOString().slice(0, 10) + '.json';
  document.body.appendChild(a);
  a.click();
  a.remove();
  showMessage('Backup saved to Downloads.');
}
```

**Notes:** Implemented in the app shell (ShellDownloads.kt).

### `__fsChild`

```js
window.WebToApk.__fsChild(uri: String, name: String, kind: String, create: Boolean): String
```

getFileHandle / getDirectoryHandle (with `create`).

**Example**

Internal transport behind `getFileHandle(name, {create})` and `getDirectoryHandle(name, {create})`. Pages use the standard API; never call this directly - the `__` methods may change without notice.

**Returns:** JSON `{ok, kind, name, uri, canWrite}` or `{ok:false, error}` (`NotFoundError`, `TypeMismatchError`, `NoModificationAllowedError`). **Needs:** a folder from `showDirectoryPicker()`.

**Use the public API:**

```js
const dir = await showDirectoryPicker();
const photos = await dir.getDirectoryHandle('photos', { create: true });
const file = await photos.getFileHandle('list.json', { create: true });
```

Check whether a file already exists in the folder before creating it:

```js
async function exists(dir, name) {
  try {
    await dir.getFileHandle(name);                     // no {create} = look up only
    return true;
  } catch (e) {
    if (e.name === 'NotFoundError') return false;
    if (e.name === 'TypeMismatchError') return true;   // a folder already has that name
    throw e;
  }
}
```

Save the app settings as JSON into a "config" sub-folder, creating both as needed:

```js
async function saveSettings(root, settings) {
  const config = await root.getDirectoryHandle('config', { create: true });
  const file = await config.getFileHandle('settings.json', { create: true });
  const w = await file.createWritable();
  await w.write(JSON.stringify(settings, null, 2));
  await w.close();
}
// root = await showDirectoryPicker()  (from a button tap)
```

**Notes:** Implemented in the app shell (FileSystemAccess.kt).

### `__fsFile`

```js
window.WebToApk.__fsFile(uri: String): String
```

getFile(): `{ok, name, type, size, lastModified, url}` (the body streams from url).

**Example**

Internal transport behind `FileSystemFileHandle.getFile()`. Pages use the standard API; never call this directly - the `__` methods may change without notice.

**Returns:** JSON `{ok, name, type, size, lastModified, url}` - the body streams from `url` (the app's own origin, with ranges) - or `{ok:false, error}` (`NotFoundError`, `TypeMismatchError`, `foreign_origin`). **Needs:** a handle from one of the pickers.

**Use the public API:**

```js
const [handle] = await showOpenFilePicker();
const file = await handle.getFile();
console.log(file.name, file.size, await file.text());
```

Show a picked image on the page without reading it into a string:

```js
document.querySelector('#choose').addEventListener('click', async function () {
  const [handle] = await showOpenFilePicker({ types: [{ accept: { 'image/*': [] } }] });
  const file = await handle.getFile();                    // a real File, streamed from the app
  const img = document.querySelector('#preview');
  if (img.src) URL.revokeObjectURL(img.src);
  img.src = URL.createObjectURL(file);
  img.alt = file.name + ' (' + file.type + ')';
});
```

Re-read a file you opened earlier and notice when it has changed or been deleted:

```js
async function reloadIfChanged(handle, lastSeen) {
  try {
    const file = await handle.getFile();
    if (file.lastModified === lastSeen) return null;      // unchanged
    return { data: JSON.parse(await file.text()), lastModified: file.lastModified };
  } catch (e) {
    if (e.name === 'NotFoundError') alert(handle.name + ' was moved or deleted.');
    else throw e;
    return null;
  }
}
```

**Notes:** Implemented in the app shell (FileSystemAccess.kt).

### `__fsList`

```js
window.WebToApk.__fsList(uri: String): String
```

A directory handle's entries: `{ok, entries:[{kind, name, uri, canWrite}]}`.

**Example**

Internal transport behind iterating a `FileSystemDirectoryHandle` (`entries()`, `keys()`, `values()`, `for await`). Pages use the standard API; never call this directly - the `__` methods may change without notice.

**Returns:** JSON `{ok, entries:[{kind, name, uri, canWrite}]}` or `{ok:false, error}`. **Needs:** a folder from `showDirectoryPicker()`.

**Use the public API:**

```js
const dir = await showDirectoryPicker();
for await (const [name, entry] of dir.entries()) console.log(entry.kind, name);
```

Count the files in a folder and all its sub-folders:

```js
async function countFiles(dir) {
  let n = 0;
  for await (const entry of dir.values()) {
    if (entry.kind === 'file') n++;
    else n += await countFiles(entry);                 // entry is a FileSystemDirectoryHandle
  }
  return n;
}
document.querySelector('#scan').addEventListener('click', async function () {
  const dir = await showDirectoryPicker();
  console.log(dir.name, 'holds', await countFiles(dir), 'files');
});
```

Build a sorted list of the photos in a folder the user chose:

```js
document.querySelector('#open-album').addEventListener('click', async function () {
  const dir = await showDirectoryPicker({ startIn: 'pictures' });
  const names = [];
  for await (const name of dir.keys()) {
    if (/\.(jpe?g|png|webp)$/i.test(name)) names.push(name);
  }
  names.sort(function (a, b) { return a.localeCompare(b); });
  renderAlbum(dir, names);                             // dir.getFileHandle(name) opens each one
});
```

**Notes:** Implemented in the app shell (FileSystemAccess.kt).

### `__fsPermission`

```js
window.WebToApk.__fsPermission(rootUri: String, uri: String): String
```

queryPermission() / requestPermission(): `{ok, state:'granted'|'prompt'}` for a handle whose picked root is [rootUri] - 'granted' while the app holds that pick's grant, across relaunches (the grant is persisted; a handle comes back from IndexedDB).

**Example**

Internal transport behind `FileSystemHandle.queryPermission()` and `requestPermission()`. Pages use the standard API; never call this directly - the `__` methods may change without notice.

**Returns:** JSON `{ok:true, state:'granted'}` while the app holds the grant of the picked folder or file the handle comes from (kept across restarts), `{ok:true, state:'prompt'}` once it is gone, or `{ok:false, error:'foreign_origin'}`. A read-only file answers `'denied'` for `{mode:'readwrite'}` in the page, without a call. **Needs:** a handle from one of the pickers, also one read back from IndexedDB.

**Use the public API:**

```js
const dir = await kvGet('backupDir');                 // a handle stored in IndexedDB earlier
if ((await dir.queryPermission({ mode: 'readwrite' })) === 'granted') {
  const f = await dir.getFileHandle('backup.json', { create: true });
  const w = await f.createWritable();
  await w.write(JSON.stringify(state));
  await w.close();
}
```

Ask again from a tap when the grant is gone - the folder picker opens at that folder, and only the same folder grants:

```js
document.getElementById('reconnect').addEventListener('click', async () => {
  const dir = await kvGet('backupDir');
  const state = await dir.requestPermission({ mode: 'readwrite' });   // 'granted' | 'denied'
  status.textContent = state === 'granted' ? 'Backups resumed' : 'Choose a backup folder';
});
```

**Notes:** Implemented in the app shell (FileSystemAccess.kt). See `showOpenFilePicker` for the whole File System Access API.

### `__fsPick`

```js
window.WebToApk.__fsPick(requestId: String, kind: String, optionsJson: String): String
```

Opens the [kind] picker (open | save | directory); handles arrive as `appmint:fs`.

**Example**

Internal transport behind `showOpenFilePicker()`, `showSaveFilePicker()` and `showDirectoryPicker()`. Pages use the standard API; never call this directly - the `__` methods may change without notice.

**Returns:** `{ok:true}` once the picker is on screen, or `{ok:false, error}` (`NotAllowedError` when one is already open, `foreign_origin`). The handles arrive as the `appmint:fs` event `{requestId, handles:[{kind, name, uri, canWrite}]}` or `{requestId, error:'cancelled'}`. **Needs:** nothing to switch on.

**Use the public API:**

```js
const [handle] = await showOpenFilePicker({ types: [{ accept: { 'image/*': ['.png', '.jpg'] } }] });
const file = await handle.getFile();
```

Save a report with a suggested name, from a button tap (the pickers need a user gesture):

```js
document.querySelector('#export').addEventListener('click', async function () {
  try {
    const handle = await showSaveFilePicker({
      suggestedName: 'report.csv',
      types: [{ description: 'CSV', accept: { 'text/csv': ['.csv'] } }]
    });
    const w = await handle.createWritable();
    await w.write(buildCsv());
    await w.close();
  } catch (e) {
    if (e.name !== 'AbortError') alert('Export failed: ' + e.message);   // AbortError = cancelled
  }
});
```

Let the user pick several photos at once, starting in the Pictures folder:

```js
document.querySelector('#add-photos').addEventListener('click', async function () {
  let handles;
  try {
    handles = await showOpenFilePicker({
      multiple: true,
      startIn: 'pictures',
      types: [{ accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.webp'] } }]
    });
  } catch (e) { return; }                                 // cancelled or already open
  for (const h of handles) addToGallery(await h.getFile());
});
```

**Notes:** Implemented in the app shell (FileSystemAccess.kt). The same page code works unchanged in desktop Chrome.

### `__fsRemove`

```js
window.WebToApk.__fsRemove(uri: String, name: String, recursive: Boolean): String
```

removeEntry(name, {recursive}).

**Example**

Internal transport behind `FileSystemDirectoryHandle.removeEntry(name, {recursive})`. Pages use the standard API; never call this directly - the `__` methods may change without notice.

**Returns:** JSON `{ok:true}` or `{ok:false, error}` (`NotFoundError`, `InvalidModificationError` for a non-empty folder without `recursive`). **Needs:** a folder from `showDirectoryPicker()`.

**Use the public API:**

```js
const dir = await showDirectoryPicker();
await dir.removeEntry('old-drafts', { recursive: true });
```

Delete one file and treat "already gone" as done:

```js
async function deleteFile(dir, name) {
  try {
    await dir.removeEntry(name);
  } catch (e) {
    if (e.name !== 'NotFoundError') throw e;   // NotFoundError: nothing left to delete
  }
}
```

Remove a folder, asking first only when it still has something in it:

```js
async function removeFolder(dir, name) {
  try {
    await dir.removeEntry(name);                          // succeeds for an empty folder
  } catch (e) {
    if (e.name !== 'InvalidModificationError') throw e;
    if (!confirm('"' + name + '" is not empty. Delete it and everything inside?')) return false;
    await dir.removeEntry(name, { recursive: true });
  }
  return true;
}
```

**Notes:** Implemented in the app shell (FileSystemAccess.kt).

### `__fsWritableAbort`

```js
window.WebToApk.__fsWritableAbort(token: String)
```

abort(): the swap is dropped; the document is untouched.

**Example**

Internal transport behind `FileSystemWritableFileStream.abort()` - the swap copy is dropped and the file stays as it was. Pages use the standard API; never call this directly - the `__` methods may change without notice.

**Returns:** nothing. **Needs:** an open writable stream.

**Use the public API:**

```js
const w = await handle.createWritable();
try { await w.write(await buildExport()); await w.close(); }
catch (e) { await w.abort(); }
```

Let the user cancel a long export; nothing half-written reaches the file:

```js
let cancelled = false;
document.getElementById('cancel').onclick = function () { cancelled = true; };

async function exportAll(handle, pages) {
  const w = await handle.createWritable();
  for (const page of pages) {
    if (cancelled) { await w.abort(); return 'cancelled'; }   // the file keeps its old content
    await w.write(await renderPage(page));
  }
  await w.close();
  return 'saved';
}
```

Download straight into a file with `pipeTo`; stopping the download aborts the write:

```js
const ctl = new AbortController();
document.getElementById('stop').onclick = function () { ctl.abort(); };

async function downloadInto(handle, url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error('HTTP ' + res.status);
  const w = await handle.createWritable();
  try {
    await res.body.pipeTo(w, { signal: ctl.signal });     // closes the file when the body ends
    return 'saved';
  } catch (e) {
    return e.name === 'AbortError' ? 'stopped, file unchanged' : 'failed: ' + e.message;
  }
}
```

**Notes:** Implemented in the app shell (FileSystemAccess.kt).

### `__fsWritableClose`

```js
window.WebToApk.__fsWritableClose(token: String): String
```

close(): the swap replaces the document.

**Example**

Internal transport behind `FileSystemWritableFileStream.close()` - the swap copy replaces the file. Pages use the standard API; never call this directly - the `__` methods may change without notice.

**Returns:** JSON `{ok:true}` or `{ok:false, error}` (`NoModificationAllowedError`, `InvalidModificationError` when the file's app cannot shorten it). **Needs:** an open writable stream.

**Use the public API:**

```js
const w = await handle.createWritable();
await w.write(json);
await w.close();   // nothing changes on disk before this
```

Tell the user why a save did not go through (`close()` is where it can fail):

```js
async function saveText(handle, text) {
  const w = await handle.createWritable();
  await w.write(text);
  try {
    await w.close();
    return true;
  } catch (e) {
    if (e.name === 'InvalidModificationError') alert('This file\'s app cannot make it shorter. Save a copy instead.');
    else if (e.name === 'NoModificationAllowedError') alert('This file cannot be changed.');
    else alert('Save failed: ' + e.message);
    return false;
  }
}
```

Write a file in several pieces; readers still see the old content until the one `close()`:

```js
async function saveRows(handle, rows) {
  const w = await handle.createWritable();
  await w.write('id,name,total\n');
  for (const r of rows) await w.write(r.id + ',' + r.name + ',' + r.total + '\n');
  console.log('before close:', (await handle.getFile()).size, 'bytes (old file)');
  await w.close();
  console.log('after close:', (await handle.getFile()).size, 'bytes');
}
```

**Notes:** Implemented in the app shell (FileSystemAccess.kt).

### `__fsWritableOpen`

```js
window.WebToApk.__fsWritableOpen(uri: String, keepExisting: Boolean): String
```

createWritable(): a swap file, copied from the file when [keepExisting].

**Example**

Internal transport behind `FileSystemFileHandle.createWritable({keepExistingData})`. Pages use the standard API; never call this directly - the `__` methods may change without notice.

**Returns:** JSON `{ok, token}` for a private swap copy, or `{ok:false, error}` (`NoModificationAllowedError` for a read-only file). **Needs:** a handle from one of the pickers.

**Use the public API:**

```js
const handle = await showSaveFilePicker({ suggestedName: 'report.csv' });
const w = await handle.createWritable({ keepExistingData: false });
await w.write('a,b\n1,2\n');
await w.close();
```

Append a line to the end of an existing log file, keeping what is already in it:

```js
async function appendLine(handle, line) {
  const size = (await handle.getFile()).size;
  const w = await handle.createWritable({ keepExistingData: true });
  await w.seek(size);                                     // write after the old content
  await w.write(new Date().toISOString() + ' ' + line + '\n');
  await w.close();                                        // only now does the file change
}
```

Save a canvas as a PNG, and leave the file untouched if anything goes wrong:

```js
document.querySelector('#save-png').addEventListener('click', async function () {
  const handle = await showSaveFilePicker({
    suggestedName: 'drawing.png',
    types: [{ accept: { 'image/png': ['.png'] } }]
  });
  const blob = await new Promise(function (r) { document.querySelector('canvas').toBlob(r, 'image/png'); });
  const w = await handle.createWritable();
  try {
    await w.write(blob);
    await w.close();
  } catch (e) {
    try { await w.abort(); } catch (x) {}                 // nothing reaches the file before close()
    alert('Could not save: ' + e.message);
  }
});
```

**Notes:** Implemented in the app shell (FileSystemAccess.kt).

### `__fsWritableTruncate`

```js
window.WebToApk.__fsWritableTruncate(token: String, size: Double): String
```

truncate(size) on a writable stream.

**Example**

Internal transport behind `FileSystemWritableFileStream.truncate(size)`. Pages use the standard API; never call this directly - the `__` methods may change without notice.

**Returns:** JSON `{ok:true}` or `{ok:false, error}`. **Needs:** an open writable stream.

**Use the public API:**

```js
const w = await handle.createWritable({ keepExistingData: true });
await w.truncate(0);
await w.write('fresh start');
await w.close();
```

Keep only the first 1,000 bytes of a file and cut everything after them:

```js
async function keepFirstKilobyte(handle) {
  const w = await handle.createWritable({ keepExistingData: true });
  await w.truncate(1000);   // a file shorter than this grows instead (see the next example)
  await w.close();
}
```

Reserve space: truncating to a larger size grows the file, and the new bytes read back as zeros:

```js
async function makeEmptySlotFile(handle, slots, slotSize) {
  const w = await handle.createWritable();
  await w.truncate(slots * slotSize);                  // e.g. 100 slots of 256 bytes, all zero
  await w.write({ type: 'write', position: 0, data: new Uint8Array([1, 0, 0, 0]) });
  await w.close();
  console.log('file is now', (await handle.getFile()).size, 'bytes');
}
```

**Notes:** Implemented in the app shell (FileSystemAccess.kt).

### `__fsWritableWrite`

```js
window.WebToApk.__fsWritableWrite(token: String, position: Double, base64: String): String
```

One chunk of a writable stream's write(), at [position].

**Example**

Internal transport behind `FileSystemWritableFileStream.write()` - one chunk at a position in the swap copy. Pages use the standard API; never call this directly - the `__` methods may change without notice.

**Returns:** JSON `{ok:true}` or `{ok:false, error}`. **Needs:** an open writable stream.

**Use the public API:**

```js
const w = await handle.createWritable({ keepExistingData: true });
await w.write({ type: 'write', position: 0, data: new Blob(['HEADER\n']) });
await w.close();
```

Add a line to the end of a log file the user picked (a file picker needs a tap):

```js
document.getElementById('addLine').addEventListener('click', async () => {
  const [handle] = await showOpenFilePicker({
    types: [{ description: 'Text', accept: { 'text/plain': ['.txt', '.log'] } }]
  });
  const size = (await handle.getFile()).size;
  const w = await handle.createWritable({ keepExistingData: true });
  await w.seek(size);                                   // start writing after the old content
  await w.write(new Date().toISOString() + ' checked in\n');
  await w.close();
});
```

Save a canvas drawing as a new PNG with "Save as" (a Blob is written as its raw bytes):

```js
document.getElementById('saveDrawing').addEventListener('click', async () => {
  const handle = await showSaveFilePicker({
    suggestedName: 'drawing.png',
    types: [{ description: 'PNG image', accept: { 'image/png': ['.png'] } }]
  });
  const blob = await new Promise(function (ok) { document.querySelector('canvas').toBlob(ok, 'image/png'); });
  const w = await handle.createWritable();
  await w.write(blob);
  await w.close();
});
```

**Notes:** Implemented in the app shell (FileSystemAccess.kt). Large data is sent in chunks.

### `getMaxIoChunkSize`

```js
window.WebToApk.getMaxIoChunkSize(): Long
```

The per-call byte cap for [readFileBase64] / [writeFileBase64], for chunk maths.

**Example**

The per-call byte limit of `readFileBase64` / `writeFileBase64` (4 MB). Pages normally use `WebToApkFS.maxChunkSize()`.

**Returns:** (sync) a number (4194304). **Needs:** nothing to call it; the read/write methods it describes need **Native Folder Access (SAF)** ticked in Step 4 (Access).

The public way:

```js
const step = window.WebToApkFS ? WebToApkFS.maxChunkSize() : 0;
```

The raw call:

```js
let step = 1048576;
if (window.WebToApk && typeof WebToApk.getMaxIoChunkSize === 'function') {
  step = Number(WebToApk.getMaxIoChunkSize()) || step;
}
console.log('read/write up to', step, 'bytes per call');
```

Work out how many bridge calls a file will take, to size a progress bar before copying it:

```js
function plannedSteps(fileUri) {
  const step = WebToApkFS.maxChunkSize();          // 0 when the helper is not available
  const info = WebToApkFS.stat(fileUri);
  if (!step || !info.ok) return 0;
  const steps = Math.max(1, Math.ceil(info.size / step));
  document.querySelector('#progress').max = steps;
  return steps;
}
```

Copy one file into another, one maximum-size chunk at a time:

```js
function copyFile(fromUri, toPath) {
  const step = WebToApkFS.maxChunkSize() || 1048576;
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(fromUri, offset, step);
    if (!r.ok) return r;
    const w = WebToApkFS.writeBytes(toPath, r.base64, offset, offset === 0 ? 'truncate' : 'patch');
    if (!w.ok) return w;
    offset += r.bytesRead;
    if (r.eof || r.bytesRead <= 0) return { ok: true, bytes: offset };
  }
}
// copyFile(pickedUri, 'photo-backup.jpg')  (a file in the granted folder)
```

**Notes:** A write larger than this is refused with `chunk too large (max N bytes)`; a larger read is cut to this size.

### `pickFile`

```js
window.WebToApk.pickFile(requestId: String, mimeFilter: String)
```

Opens the system single-file picker (ACTION_OPEN_DOCUMENT). Unlike [requestFolderAccess] this grants access to exactly one file, which is what an editor or a shredder should ask for; the read/write grant is persisted so the URI keeps working after the process is killed. The answer arrives on `WebToApkOnFilePickResult(success, fileJson, requestId)`; a failure's `error` is "cancelled", "disabled", "no-picker", "foreign_origin", or "busy" (a picker is already open for an earlier request, which keeps its own answer). @param mimeFilter e.g. "image/\*", "application/pdf", or "" for any file. (The star is backslash-escaped only because Kotlin block comments nest.)

**Example**

Opens the system picker for ONE file. Pages normally use the Promise helper `WebToApkFS.pickFile(mimeFilter)`; this raw method answers through a callback.

**Returns:** nothing now. Later the shell calls `window.WebToApkOnFilePickResult(success, fileJson, requestId)`, where `fileJson` is a JSON **string** `{ok, uri, name, size, mime}` on success or `{ok:false, uri:'', error}` with `error` = `'cancelled'`, `'disabled'`, `'no-picker'`, `'busy'` or `'foreign_origin'`. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

The public way (recommended):

```js
const f = await WebToApkFS.pickFile('image/*');
if (f.ok) console.log('picked', f.name, f.uri);
```

The raw call, matching on your own `requestId`:

```js
function pickRaw(mime) {
  return new Promise(function (resolve) {
    if (!window.WebToApk || typeof WebToApk.pickFile !== 'function') { resolve(null); return; }
    const myId = 'pick_' + Date.now();
    window.WebToApkOnFilePickResult = function (success, fileJson, requestId) {
      if (requestId !== myId) return;
      const r = JSON.parse(fileJson);           // fileJson is a string
      resolve(success && r.ok ? r : null);      // null = cancelled or not allowed
    };
    WebToApk.pickFile(myId, mime || '');        // '' = any file
  });
}
```

Attach a PDF and show its name and size, telling a cancel apart from a real problem:

```js
document.querySelector('#attach').addEventListener('click', async function () {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('application/pdf');
  if (!f.ok) {
    if (f.error === 'cancelled' || f.error === 'busy') return;   // user closed it / one already open
    alert('Could not open the file picker: ' + f.error);         // 'disabled', 'no-picker', ...
    return;
  }
  document.querySelector('#attachment').textContent =
    f.name + ' (' + Math.round(f.size / 1024) + ' KB)';
  localStorage.setItem('attachmentUri', f.uri);   // the grant is kept across restarts
});
```

Open a text file and load its contents into an editor:

```js
async function openTextFile() {
  if (!window.WebToApkFS) return;
  const f = await WebToApkFS.pickFile('text/*');
  if (!f.ok) return;
  const r = WebToApkFS.readAll(f.uri);            // {ok, base64, size} or {ok:false, error}
  if (!r.ok) { alert('Could not read ' + f.name + ': ' + r.error); return; }
  const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
  document.querySelector('#editor').value = new TextDecoder('utf-8').decode(bytes);
  document.querySelector('#editor').dataset.uri = f.uri;   // save back to the same file later
}
```

**Notes:** Only one pick can be open at a time: a call made while one is open is answered at once with `error:'busy'`, and the open one keeps its own answer. The grant is kept (read and write) until you call `releaseFileAccess(uri)`. The file is not read here - use `readFileBase64` / `WebToApkFS.readBytes` with the `uri`.

### `readFileBase64`

```js
window.WebToApk.readFileBase64(pathOrUri: String, offset: Long, length: Long): String
```

Reads up to [length] raw bytes starting at [offset] and returns them base64. @param pathOrUri a `content://` URI from [pickFile], or a path relative to the folder granted by [requestFolderAccess]. @param length bytes to read; clamped to 4 MB per call. 0 or negative means "as much as one call allows". @return JSON `{ok, base64, bytesRead, offset, size, eof, error}`. `eof` tells a chunking loop when to stop without having to compare against `size` itself.

**Example**

Reads raw bytes from a file at a byte offset and returns them as base64. Pages normally use `WebToApkFS.readBytes(pathOrUri, offset, length)` (same result, already parsed) or `WebToApkFS.readAll(pathOrUri)`.

**Returns:** (sync) a JSON **string** `{ok:true, base64, bytesRead, offset, size, eof}` or `{ok:false, error}`. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

The public way:

```js
const r = WebToApkFS.readBytes(fileUri, 0, 65536);
if (r.ok) console.log(r.bytesRead, 'bytes read, end of file:', r.eof);
```

The raw call - note the result is a string you must `JSON.parse`, not bare base64:

```js
if (window.WebToApk && typeof WebToApk.readFileBase64 === 'function') {
  const r = JSON.parse(WebToApk.readFileBase64(fileUri, 0, 0));   // length 0 = up to 4 MB
  if (r.ok) {
    const bytes = Uint8Array.from(atob(r.base64), function (c) { return c.charCodeAt(0); });
    console.log('first byte', bytes[0], 'of', r.size);
  } else {
    console.log('read failed:', r.error);
  }
}
```

Check a picked file really is a PNG by reading only its first 8 bytes:

```js
function isPng(fileUri) {
  const r = WebToApkFS.readBytes(fileUri, 0, 8);
  if (!r.ok || r.bytesRead < 8) return false;
  const head = atob(r.base64);
  const sig = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
  return sig.every(function (b, i) { return head.charCodeAt(i) === b; });
}
```

Read a large file chunk by chunk with a progress bar, never holding it all in memory:

```js
function scanInChunks(fileUri, onChunk) {
  const step = WebToApkFS.maxChunkSize() || 1048576;
  let offset = 0;
  for (;;) {
    const r = WebToApkFS.readBytes(fileUri, offset, step);
    if (!r.ok) throw new Error('read failed: ' + r.error);
    onChunk(r.base64);                                    // process this piece, then drop it
    offset += r.bytesRead;
    document.querySelector('#progress').value = r.size ? offset / r.size : 1;
    if (r.eof || r.bytesRead <= 0) break;
  }
  return offset;                                          // total bytes read
}
```

**Notes:** `pathOrUri` is a `content://` URI from `pickFile` or a path relative to the folder from `requestFolderAccess`. At most 4 MB per call (`getMaxIoChunkSize()`); loop on `bytesRead` until `eof`. Errors: `not-found`, `disabled`, `cannot-open`, `offset must be >= 0`, `offset past end of file (N bytes)`, `foreign_origin` (a page from a site other than the app's own).

### `releaseFileAccess`

```js
window.WebToApk.releaseFileAccess(uriString: String): String
```

Releases a persisted single-file grant taken by [pickFile]. Android caps how many URI grants an app may persist, so an app that picks many files should release the ones it is done with.

**Example**

Releases the kept read/write grant for a file chosen with `pickFile`. Pages normally use `WebToApkFS.releaseFile(uri)`, which returns a real boolean.

**Returns:** (sync) the **string** `"true"` or `"false"`. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build (always `"false"` otherwise).

The public way:

```js
if (window.WebToApkFS) WebToApkFS.releaseFile(fileUri);
```

The raw call:

```js
if (window.WebToApk && typeof WebToApk.releaseFileAccess === 'function') {
  const released = WebToApk.releaseFileAccess(fileUri) === 'true';   // a string, not a boolean
  console.log(released ? 'access released' : 'nothing to release');
}
```

Keep a "recent files" list of at most 10 and release the grant of any file that falls off it:

```js
function rememberRecent(uri) {
  let recent = JSON.parse(localStorage.getItem('recentFiles') || '[]');
  recent = [uri].concat(recent.filter(function (u) { return u !== uri; }));
  while (recent.length > 10) {
    const dropped = recent.pop();
    if (window.WebToApkFS) WebToApkFS.releaseFile(dropped);   // Android caps kept grants
  }
  localStorage.setItem('recentFiles', JSON.stringify(recent));
}
```

Close the current document: release its grant and forget the saved URI:

```js
document.querySelector('#close-doc').addEventListener('click', function () {
  const uri = localStorage.getItem('openDocUri');
  if (uri && window.WebToApkFS) {
    const released = WebToApkFS.releaseFile(uri);   // true / false, already a boolean
    console.log(released ? 'grant released' : 'no grant was held for this file');
  }
  localStorage.removeItem('openDocUri');
  document.querySelector('#editor').value = '';
});
```

**Notes:** Android limits how many file grants an app may keep, so release files you no longer need. The file itself is not touched.

### `statFile`

```js
window.WebToApk.statFile(pathOrUri: String): String
```

File or folder metadata without reading any of it. A relative path names a file or a sub-folder of the granted folder; a folder answers `isDirectory: true` with `size` 0 and `mime` `vnd.android.document/directory`. @return JSON `{ok, name, size, mime, lastModified, uri, isDirectory, canWrite}`, or `{ok:false, error}` with `not-found`, `disabled` or `foreign_origin`.

**Example**

Reads a file's metadata (name, size, type) without reading its bytes. Pages normally use `WebToApkFS.stat(pathOrUri)`, which parses the JSON for you.

**Returns:** (sync) a JSON **string** `{ok, name, size, mime, lastModified, uri, isDirectory, canWrite}`, or `{ok:false, error}` (`'not-found'`, `'disabled'`, `'foreign_origin'`). **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

The public way:

```js
const info = WebToApkFS.stat(fileUri);
if (info.ok) console.log(info.name, info.size);
```

The raw call:

```js
if (window.WebToApk && typeof WebToApk.statFile === 'function') {
  const info = JSON.parse(WebToApk.statFile(fileUri));   // or 'notes/today.txt' in the granted folder
  if (info.ok) console.log(info.name, info.size + ' bytes', info.mime, info.canWrite);
  else console.log('stat failed:', info.error);
}
```

Refuse a file that is too big before reading any of it:

```js
const MAX_BYTES = 20 * 1024 * 1024;
function canImport(fileUri) {
  const info = WebToApkFS.stat(fileUri);
  if (!info.ok) { alert('File not available: ' + info.error); return false; }
  if (info.size > MAX_BYTES) {
    alert(info.name + ' is ' + Math.round(info.size / 1048576) + ' MB; the limit is 20 MB.');
    return false;
  }
  return true;
}
```

Look up a sub-folder of the granted folder and only offer "Save here" when it is writable:

```js
function checkExportFolder() {
  const info = WebToApkFS.stat('exports');          // a path relative to the granted folder
  const btn = document.querySelector('#save-here');
  if (!info.ok) {                                    // 'not-found' = the folder is not there yet
    btn.textContent = 'Create "exports" and save';
    return WebToApkFS.mkdir('exports');
  }
  btn.disabled = !(info.isDirectory && info.canWrite);
  console.log('exports last changed', new Date(info.lastModified).toLocaleString());
  return true;
}
```

**Notes:** Accepts a `content://` URI from `pickFile` or a path relative to the folder granted with `requestFolderAccess`. It only answers the app's own pages or its configured website; a page from any other site gets `foreign_origin`.

### `writeFileBase64`

```js
window.WebToApk.writeFileBase64(pathOrUri: String, base64: String, offset: Long, mode: String): String
```

Writes raw bytes and confirms how many landed. @param mode `"patch"` writes at [offset] leaving the rest of the file intact (what a multi-pass overwrite needs), `"truncate"` replaces the file, `"append"` ignores [offset] and writes at the end. @return JSON `{ok, bytesWritten, offset, size, error}` - `size` is re-read from the file after an fsync, so a caller can verify the write for real rather than trust a boolean.

**Example**

Writes base64 bytes into a file at an offset (`patch`), over the whole file (`truncate`), or at the end (`append`). Pages normally use `WebToApkFS.writeBytes(pathOrUri, base64, offset, mode)` or `WebToApkFS.writeAll(pathOrUri, base64)`.

**Returns:** (sync) a JSON **string** `{ok, bytesWritten, offset, size}` (`size` re-read after an fsync) or `{ok:false, error}`. **Needs:** tick **Native Folder Access (SAF)** in Step 4 (Access) when you build.

The public way:

```js
const r = WebToApkFS.writeBytes(fileUri, btoa('hello'), 0, 'truncate');
if (r.ok) console.log('saved', r.bytesWritten, 'bytes');
```

The raw call:

```js
if (window.WebToApk && typeof WebToApk.writeFileBase64 === 'function') {
  const r = JSON.parse(WebToApk.writeFileBase64('notes/today.txt', btoa('Buy milk\n'), 0, 'append'));
  if (!r.ok) console.log('write failed:', r.error);
  else console.log('file is now', r.size, 'bytes');
}
```

Save a canvas drawing as a PNG into a file the user picked, replacing what was there:

```js
document.querySelector('#save').addEventListener('click', async function () {
  const f = await WebToApkFS.pickFile('image/png');
  if (!f.ok) return;
  const base64 = document.querySelector('canvas').toDataURL('image/png').split(',')[1];
  const r = WebToApkFS.writeAll(f.uri, base64);   // truncates, then writes in 4 MB chunks
  if (r.ok) console.log('saved', r.bytesWritten, 'bytes to', f.name);
  else alert('Save failed: ' + r.error);
});
```

Overwrite a file in place with zeros before deleting it, and confirm every pass landed:

```js
function zeroFill(fileUri) {
  const info = WebToApkFS.stat(fileUri);
  if (!info.ok) return false;
  const slice = 32768;
  for (let at = 0; at < info.size; at += slice) {
    const len = Math.min(slice, info.size - at);
    const zeros = btoa(String.fromCharCode.apply(null, new Uint8Array(len)));
    const w = WebToApkFS.writeBytes(fileUri, zeros, at, 'patch');   // the rest of the file is kept
    if (!w.ok || w.bytesWritten !== len) { console.log('write failed at', at, w.error); return false; }
  }
  return true;   // each call is fsync'ed before it answers
}
```

**Notes:** Mode is `'patch'` (default when empty), `'truncate'` or `'append'`. One call takes at most 4 MB of decoded bytes (`getMaxIoChunkSize()`). A relative path is created inside the granted folder when missing. Errors: `disabled`, `foreign_origin` (the page on screen is not the app's own), `bad mode: x`, `offset must be >= 0`, `bad base64`, `chunk too large (max N bytes)`, `not-found`, `cannot-open`, `short write`.

