Skip to content

User files and data storage

Start with user file selection, reopen using fileId, and read binary streams as needed. Store small settings separately from large files; do not put an entire novel into JSON storage.

Run the existing file example first#

  1. Build the complete toolkit using the quickstart, then select the paired Novel Reader component from PhoneSDK and novel_reader from GlassSDK in Studio.
  2. Prepare a small TXT file with content suitable for sharing, import it through the page's file picker, and confirm reading works on both phone and glasses.
  3. Stop and re-enter, then check the library and reading position. Cancel a file-selection attempt and confirm the page remains usable.
  4. Scan on the phone and repeat these steps with its file picker. Computer files do not automatically appear in the phone's library.

Where to store the three types of data#

Bundled assets versus data kept on the phoneSource and static assets are delivered in the application ZIP. In the current App, gm.storage data and user-selected files are stored by account and installation ownership, outside the ZIP. Studio's simulated storage is isolated by Web ID.MEMOMIND / DATA OWNERSHIPBundled assets versus data kept on the phoneIn the phone App, store data by purposeApplication staticassetsHTML / JS / imagesMMPKG → ZIPDelivered with theversionSettings and savedbusiness datagm.storageSmall JSON objectsStored underinstallationownershipUser SelectedFilesgm.files.pick →fileIdApplication-privatecopyRead file stream ondemandSaved data, user files, and local authorizations belong to the current installation.Deleting a private copy does not delete the original file.
Bundled assets versus data kept on the phoneSource and static assets are delivered in the application ZIP. In the current App, gm.storage data and user-selected files are stored by account and installation ownership, outside the ZIP. Studio's simulated storage is isolated by Web ID.MEMOMIND / DATA OWNERSHIPBundled assets versus datakept on the phoneIn the phone App, store databy purposeApplication staticassetsHTML / JS / imagesMMPKG → ZIPDelivered with the versionSettings and savedbusiness datagm.storageSmall JSON objectsStored under installationownershipUser Selected Filesgm.files.pick → fileIdApplication-private copyRead file stream on demandSaved data, user files, and localauthorizations belong to the currentinstallation. Deleting a private copydoes not delete the original file.
Keep bundled static assets separate from runtime data within the installation.
DataAPI or locationPurpose and retention
HTML, images, and protocol files bundled with the applicationProject static directory → MMPKG → ZIPDelivered with the application version; rebuild after changes.
Settings, reading position, and business stategm.storage.get / set / remove / clearSave small JSON-serializable objects. Show “Saved” only after the write succeeds.
Document selected by the usergm.files.pick / list / stat / openRead / deleteThe host copies files into the application's private file area and manages them by fileId. The original system path is not a read API.

Read a size-limited TXT preview#

Add this function to an existing plugin.js. Create gm and complete gm.ready(), then call readTextPreview(gm, signal) from a user click handler, catch errors, and show the result. signal comes from AbortController and cancels this read. Call abort() when the user cancels reading or exits the feature. The user still cancels the file picker through the system UI.

Preview only the first 64 KiB, decoding UTF-8 characters across chunk boundaries. A null result means the user canceled selection; truncated means some content remains unread. For a complete reader, follow Novel Reader's encoding detection, segment indexing, and on-demand reads.

< / >
export async function readTextPreview(gm, signal) {
  const { files } = await gm.files.pick({
    extensions: ['txt'], allowMultiple: false,
  });
  const file = files[0];
  if (!file) return null; // The user cancelled.
  if (file.size === 0) return { file, text: '', truncated: false };

  const opened = await gm.files.openRead(file.fileId, {
    offset: 0, length: Math.min(file.size, 64 * 1024), signal,
  });
  const reader = opened.stream.getReader();
  const decoder = new TextDecoder('utf-8');
  let text = '';
  try {
    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      text += decoder.decode(value, { stream: true });
    }
    const truncated = opened.length < file.size;
    if (!truncated) text += decoder.decode();
    return { file, text, truncated };
  } finally {
    try { await reader.cancel(); } catch { /* Already closed or aborted. */ }
    reader.releaseLock();
  }
}

Check the return structure when reading settings#

gm.storage.get(key) returns an object with a value field; value is null if nothing was saved. Version your settings structure, then validate and migrate old data when reading. Do not treat the entire object resolved by the Promise as the settings value.

< / >
await gm.storage.set('reader-preferences', { schema: 1, fontScale: 1 });
const { value } = await gm.storage.get('reader-preferences');
if (value?.schema === 1 && typeof value.fontScale === 'number') {
  console.log('saved font scale', value.fontScale);
}

File quotas and read ranges#

ItemCurrent reference implementationHandling
User files400 MiB per file and 400 MiB total private-file storage; at most 20 files per selection.Read getCapabilities().files, then check actual usage with files.getUsage(). These are not MMPKG or ZIP size limits.
Small JSON StorageThe current App allows up to 64 KiB per item, 1 MiB total, and keys up to 128 characters.Limits count the JSON's UTF-8 bytes. Chinese character counts are not byte counts. Handle QUOTA_EXCEEDED when a limit is exceeded.
offset / lengthRanges are measured in bytes. offset must be a nonnegative safe integer; length, when specified, must be a positive safe integer.Do not use character indices as byte offsets. If the range extends beyond the file, use the returned length. Do not pass length: 0 for an empty file.
Read streamsBound to the current runtime session; cancellable through AbortSignal.Suspension, reload, file deletion, or closing the application may terminate the stream. Reopen by fileId after recovery.

Checks for deletion, cancellation and updates#

  • An empty array from files.pick is normal cancellation, not “Import failed”. Files have already been copied into the application's private area; canceling a later read does not undo the import.
  • Releasing the reader lock does not cancel the stream. When ending a read early, call reader.cancel() or AbortController.abort() and handle completion of any pending read().
  • Delete with files.delete(fileId) and clean up your saved index or reading position. Deleting the App's private copy does not delete the original file selected by the user.
  • After updating the same installation, revalidate saved-data structures and fileId values. Offer file selection again if a file has been removed.
  • Read streams and large files do not pass through JSON/Base64. Save only the required index and current position; avoid concatenating hundreds of MiB of content.

Reference files in the toolkit#

  • PhoneSDK/packages/web-sdk/src/index.d.ts
  • PhoneSDK/examples/novel-reader/reader-file.js
  • PhoneSDK/examples/novel-reader/manifest.json
  • PhoneSDK/examples/permission-debug/plugin.js