cellar_flutter 1.0.0-dev.0 copy "cellar_flutter: ^1.0.0-dev.0" to clipboard
cellar_flutter: ^1.0.0-dev.0 copied to clipboard

Object storage for Flutter — one openCellar() call, six platforms. Re-exports the full cellar API: partitions, lifecycle, encryption, streaming, materialize.

[cellar_flutter — object storage for Flutter apps]

pub package likes pub points GitHub stars license: MIT

Object storage for Flutter apps. One openCellar() call gives you a ready-to-use store on iOS, Android, macOS, Windows, Linux, and web — real files on native, IndexedDB in the browser, identical API everywhere.

Named partitions with self-cleaning lifecycle rules, tenant scoping, bring-your-own encryption, streaming I/O that never buffers a whole object, atomic writes, typed errors, and platform-local handles for FFI and browser APIs. No kIsWeb, no platform branches, no setup — anywhere in your code.

like it? a ⭐ star or 👍 like is the entire marketing budget. Bugs & features →


👀 Peek inside

Install #

dependencies:
  cellar_flutter: ^1.0.0-dev.0

No permissions, no manifest entries, no per-platform Dart — storage lands in your app's private area, which every OS grants for free.


Quick start #

One call opens the store; then it's keys in, bytes out:

import 'package:cellar_flutter/cellar_flutter.dart';

final cellar = await openCellar(name: 'my_app');

await cellar.write('notes/hello', bytes, contentType: 'text/plain');
final back = await cellar.read('notes/hello');
final exists = await cellar.exists('notes/hello');
await cellar.delete('notes/hello');

await cellar.close(); // app shutdown, profile switch, etc.

openCellar is byte-identical on all six platforms. That's the shape of every call after it: a /-separated key in, bytes or metadata out — write, read, head, list, copy, materialize.

🧩 what openCellar actually does

Three steps: resolve where your app's storage lives, construct the Cellar, await its open() — then hand it back ready.

The resolution step is the reason this package exists. The core never guesses where data lives — locations must be supplied. On native platforms openCellar asks path_provider for two directories: the support directory (persistent partitions) and the cache directory (partitions the OS may also evict) — the app's official locations, straight from the OS. On web there's nothing to resolve at all: storage is IndexedDB, and the ask is skipped (the path_provider code sits behind a conditional import, so web builds never even compile it).

You never see any of this — it's the machinery the one call wraps.


Usage #

The full API comes re-exported from cellar — one import serves everything below. Highlights here; every method and full signature lives in the API reference.

Store #

Bytes with optional content type and custom metadata; metadata reads never touch bodies:

await cellar.write(
  'docs/report',
  bytes,
  contentType: 'application/pdf',
  metadata: {'source': 'export', 'v': '2'},
);

final info = await cellar.head('docs/report');   // size, type, metadata, lastModified
final all = await cellar.list('docs/');          // ObjectInfo per key, body-free
await cellar.copy('docs/report', 'docs/report_backup');
await cellar.deletePrefix('docs/');              // bulk delete by prefix

Keys are a grammar, not file paths: /-separated segments, same rules on every platform, validated up front. Prefixes are raw string prefixes, S3-style — end with / to scope to a pseudo-directory.

The full facade, all the same shape: write / writeStream, read / readStream / readRange, head, exists, list / listKeys, delete / deletePrefix, copy, move, copyAcrossPartitions / moveAcrossPartitions, wipePartition, movePartition, updateMetadata, materialize, totalSize, fileSize.

Partitions #

Named categories of data, each with its own rules — permanent vs cache vs scratch, backed-up vs excluded:

final cellar = await openCellar(
  name: 'my_app',
  partitions: {
    'main': PartitionConfig(),                                    // permanent, backed up
    'image_cache': PartitionConfig(
      lifecycle: Lifecycle.cache(maxBytes: 100 * 1024 * 1024),    // 100 MB cap
      osBackup: false,                                            // skip iCloud / Time Machine
    ),
    'pending_uploads': PartitionConfig(lifecycle: Lifecycle.scratch()), // wipe on open
  },
  defaultPartition: 'main',
);

await cellar.write('messages/abc', bytes);                        // default partition
await cellar.write('thumb/abc', smallBytes, partition: 'image_cache');
await cellar.wipePartition('image_cache');                        // one atomic wipe

Cross-partition copyAcrossPartitions / moveAcrossPartitions stream through with metadata.

Lifecycle #

Self-cleaning caches. A background timer evaluates every partition's rules; runLifecycleNow() runs a pass on demand; eviction failures surface through onEvictionError and never wedge the sweep:

'image_cache': PartitionConfig(
  lifecycle: Lifecycle.cache(maxBytes: 100 * 1024 * 1024),  // oldest evicted first
),
'downloads': PartitionConfig(
  lifecycle: Lifecycle(maxAge: Duration(days: 30), runInterval: Duration(hours: 6)),
),
'scratch': PartitionConfig(lifecycle: Lifecycle.scratch()), // wiped on every open

Lifecycle.cache also opts into OS-assisted eviction (osManaged) — on iOS and Android the partition lives in the cache directory the OS may sweep under storage pressure. Your rules run everywhere regardless.

Stream #

For audio, video, model weights, downloads — anything that shouldn't live in memory:

// Write from a stream — only one chunk in memory at a time.
await cellar.writeStream(
  'models/llama-7b.gguf',
  downloadStream,
  onProgress: (written, total) => updateBar(written),
);

// Read as a stream — playback, processing, forwarding.
await for (final chunk in cellar.readStream('models/llama-7b.gguf')) { /* … */ }

// Read just a byte range — seeking in media, header parsing.
final slice = await cellar.readRange('audio/song.mp3', start: 44100, length: 8192);

Memory stays constant regardless of object size. Writes are atomic on every platform: a crash mid-write leaves the previous object fully intact, never a torn file.

Encrypt #

Cellar ships zero cryptography. Implement two small interfaces over the crypto of your choice and every write is encrypted transparently — streaming chunks, per-chunk MACs, tamper detection that names the exact chunk:

final cellar = await openCellar(
  name: 'my_app',
  encryption: CellarEncryption(
    encryptor: myEncryptor,        // your FileEncryptor
    keyResolver: myKeyResolver,    // your EncryptionKeyResolver
    encryptByDefault: true,
  ),
);

await cellar.write('diary/today', entryBytes);   // stored as ciphertext
final entry = await cellar.read('diary/today');  // decrypted automatically

await cellar.write('cache/thumb', data, encrypt: false);  // per-write opt-out

The example app contains a complete working FileEncryptor — what you see there is exactly what implementing the seam takes.

Materialize #

Some consumers need a real path or URL, not Dart bytes — native libraries via FFI, plugins that take a File, <img>/<audio> elements:

final handle = await cellar.materialize('models/llama-7b.gguf');

nativeLib.loadModel(handle.localPath);  // native: a real filesystem path
// imgElement.src = handle.localPath;   // web: a Blob URL

await handle.release();                 // frees temps / revokes the URL

Decryption is transparent; deleting a key never invalidates a live handle — on any OS.

Tenant scoping #

One keyPrefix stamps a namespace onto every key — per-user data, test isolation:

final cellar = await openCellar(name: 'my_app', keyPrefix: 'user/$uid');

await cellar.write('photos/cat', bytes);   // stored at user/<uid>/photos/cat
final all = await cellar.list('');         // keys come back scope-relative: photos/cat

Results speak the same scope-relative keys you write with; the prefix exists only at rest. One cellar = one namespace — apps needing several open several cellars.


Error handling #

Failures are a sealed hierarchy — pattern-match the ones you can act on:

try {
  await cellar.read('missing');
} on FileNotFoundError catch (e) {
  print('missing key: ${e.key}');
} on ChunkVerificationError catch (e) {
  print('tampered chunk ${e.chunkIndex}');
} on StorageError catch (e) {
  print('storage error: $e');
}

FileNotFoundError, EncryptionKeyMissingError (never silent ciphertext), CorruptedFileError, CorruptedMetadataError, ChunkVerificationError, InvalidHeaderError, InvalidKeyError, WriteError. Plain throws are reserved for programmer error (StateError on a closed cellar, ArgumentError on bad configuration).


Platform support #

iOS Android macOS Windows Linux Web
✅ IndexedDB

Where the data lands #

Platform Persistent partitions osManaged partitions
iOS, Android, macOS, Windows, Linux app support directory app cache directory (OS may evict on mobile)
Web IndexedDB (one database per partition) same

Both directories come from path_provider — app-private, no permissions, cleaned up on uninstall. Need a location outside them (Downloads, external storage)? Drop to Cellar.atPath with any directory you've obtained — it's re-exported here too.


How this relates to cellar #

cellar is the pure-Dart core — the entire engine, with zero Flutter in it, usable from servers and CLIs. This package adds the one thing a Flutter app needs on top: storage-root resolution through path_provider (Android's directories are only knowable by asking the OS), wrapped in openCellar, plus a re-export of the full core API.

Rule of thumb: Flutter app → this package. Anything else → cellar. You never need both in one pubspec.


Not in the box #

  • Queries, key-value prefs, sync, user-visible files — the core's Not in the box covers what cellar deliberately isn't, with a reasoned WONT_DO row each.
  • Widgets — this package is storage, not UI; pair materialize with your image/video widgets.
  • Permission flows — cellar's locations need none on any platform.

Missing something? Open an issue.


Example #

The example app exercises every capability in one file — seven tabs: store, streaming with live progress, partitions, lifecycle eviction, encryption (with raw-ciphertext peek and a tamper demo), materialization, and a custom in-memory backend with two tenant-scoped cellars.


Docs #

Doc What's inside
Architecture This package's contract, tree, and test model
Capabilities Status per capability
Updating Maintenance recipes and the pinned-behavior watchlist
Contributing Setup, PR workflow, keeping openCellar in lockstep with the core
cellar's docs The engine's architecture, durability mechanics, roadmap

License #

MIT. See LICENSE.

1
likes
0
points
136
downloads

Publisher

verified publisherwhuppi.com

Weekly Downloads

Object storage for Flutter — one openCellar() call, six platforms. Re-exports the full cellar API: partitions, lifecycle, encryption, streaming, materialize.

Repository (GitHub)
View/report issues

Topics

#storage #cache #encryption #indexeddb #cross-platform

License

unknown (license)

Dependencies

cellar, flutter, path_provider

More

Packages that depend on cellar_flutter