materialize method
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);
}