materialize method

  1. @override
Future<MaterializedFile> materialize(
  1. String key, {
  2. bool decrypt = true,
  3. bool exclusive = false,
})
override

Make a stored object available as a platform-local accessible resource.

Returns a MaterializedFile whose localPath is a filesystem path on native (usable for FFI, platform channels, native plugins taking a File/path, OS file dialogs, mmap) or a Blob URL on web (usable in <audio>/<img>/<video> src, fetch(), anchor downloads, Web Workers). The path is LOCAL to this device — it MUST NOT be persisted, synced, or shared across devices.

Parameters

decrypt — if true and the underlying file is encrypted, the materialized file contains plaintext. If false, the raw stored bytes (possibly ciphertext) are returned. For unencrypted files the flag has no effect. Default: true (callers usually want readable content).

exclusive — if true, the caller gets a private copy they may modify or delete. If false, the caller gets shared access; on local backends this returns the original file path (zero copy). Default: false.

Behavior matrix

Backend encrypted? decrypt exclusive Result
Local no any false Original path (zero copy)
Local no any true Copy to temp
Local yes true false Decrypt to cache (reusable)
Local yes true true Decrypt to temp (caller owns)
Local yes false false Original encrypted path
Local yes false true Copy encrypted to temp
IndexedDB no any any Blob URL
IndexedDB yes true any Decrypt + blob URL
IndexedDB yes false any Raw blob URL

Lifecycle

Call MaterializedFile.release when done.

  • Local, exclusive: false, unencrypted: no-op (original stays).
  • Local, exclusive: false, decrypted: cached; cleaned on eviction.
  • Local, exclusive: true: temp deleted.
  • IndexedDB: blob URL revoked.

Implementation

@override
Future<MaterializedFile> materialize(
  String key, {
  bool decrypt = true,
  bool exclusive = false,
}) async {
  checkNotDisposed();
  final info = await _inner.head(key);
  if (info == null) throw FileNotFoundError(key);

  final isEncrypted = info.metadata[_metaEncrypted] == 'true';

  // Unencrypted or caller wants raw ciphertext — pass through.
  // `decrypt: false` on an encrypted file is a deliberate "give me the
  // ciphertext bytes" request; callers handle their own decryption or
  // simply need the raw blob for re-upload.
  if (!isEncrypted || !decrypt) {
    return _inner.materialize(key, decrypt: decrypt, exclusive: exclusive);
  }

  // Encrypted + decrypt=true. Plaintext doesn't live on disk — we must
  // stream-decrypt to a platform-local location. Two paths:
  //
  // 1. _decryptCache present → shared entries are content-addressed by
  //    {key, mtime, size} and reused across calls; exclusive entries are
  //    one-shot keys the caller owns, deleted on release. Either way the
  //    plaintext lands in the cache — the one store the app designated
  //    for plaintext-at-rest — never in the primary store.
  //
  // 2. _decryptCache absent → one-shot plaintext temp in the INNER
  //    backend under the reserved [decryptTempMarker] namespace (hidden
  //    from list(), crash leftovers swept on the next materialize).
  //    Every call re-decrypts. Correct, just not reused.
  final cache = _decryptCache;
  if (cache != null && !exclusive) {
    return _materializeViaCache(cache, key, info);
  }
  return _materializeFresh(key);
}