Skip to content

File Storage

View raw .md

The Storage facade provides a unified API for reading, writing, and deleting files across multiple named disks, with built-in support for remote downloads and URL generation.

On this page

Introduction#

Magic provides a powerful filesystem abstraction thanks to the Storage facade. It provides a unified API for interacting with local filesystems across all platforms (iOS, Android, macOS, Windows, Web).

Key features:

  • Cross-Platform: Same API on mobile, desktop, and web.
  • Disk System: Configure multiple storage locations.
  • MagicFile Integration: One file wrapper for stored, downloaded and picked files.

Picking files (gallery, camera, file and save dialogs) and the share sheet behind Storage.download on mobile and desktop live in the magic_picker plugin: flutter pub add magic_picker, then dart run magic:dowel plugin:install magic_picker. Its Pick facade returns a MagicFile you can store with the methods below, and its service provider sets the LocalAdapterContract.downloadHandler seam. Without it, Storage.download throws UnsupportedError on mobile and desktop; the web download is unchanged. See the installation guide and the download page.

Configuration#

Configure storage disks in lib/config/filesystems.dart:

dart
// lib/config/filesystems.dart
Map<String, dynamic> get filesystemsConfig => {
  'filesystems': {
    'default': 'local',

    'disks': {
      'local': {
        'driver': 'local',
        'root': 'storage/app',
      },

      'public': {
        'driver': 'local',
        'root': 'storage/app/public',
      },

      'temp': {
        'driver': 'local',
        'root': 'storage/temp',
      },

      'downloads': {
        'driver': 'local',
        'root': 'downloads',
        'exclude_from_backup': true,
      },
    },
  },
};

Pass it to Magic.init(configFactories: [...]) with your other config files. local is the only driver; a disk with any other driver throws when it is first resolved.

A local disk writes under the app's documents directory, which iOS backs up to iCloud and macOS to Time Machine. Set exclude_from_backup to true on a disk holding large files the app can fetch again, such as downloaded media. On iOS and macOS the disk's root directory is then marked with kCFURLIsExcludedFromBackupKey the first time the disk resolves a path, and everything stored under it is left out of the backup. If the platform refuses the mark, that first call throws a FileSystemException and the next one tries again. The key defaults to false and has no effect on Android, Windows, Linux or the web.

Storing Files#

From Bytes#

dart
await Storage.put('avatars/1.jpg', imageBytes);

// With MIME type (important for Web blob URLs)
await Storage.put('docs/resume.pdf', bytes, mimeType: 'application/pdf');

From String#

dart
await Storage.put('logs/app.log', 'Log entry...');

From MagicFile#

Any MagicFile stores itself, whether it came from a picker, a download or MagicFile(...):

dart
// Manual path
await file.store('avatars/user.jpg');

// Auto-generated timestamped name
await file.storeAs('avatars');
// Result: 'avatars/1704067200000_photo.jpg'

Retrieving Files#

As Bytes#

dart
final Uint8List? bytes = await Storage.get('avatars/1.jpg');

As MagicFile#

dart
final MagicFile? file = await Storage.getFile('avatars/1.jpg');
if (file != null) {
  print(file.name);      // '1.jpg'
  print(file.size);      // 102400
  print(file.mimeType);  // 'image/jpeg'
  print(file.extension); // 'jpg'
}

Check Existence#

dart
if (await Storage.exists('avatars/1.jpg')) {
  // File exists
}

File URLs & Downloads#

Display URLs#

Get a URL for use in Image.network() or similar widgets:

dart
final url = await Storage.url('avatars/1.jpg');
Platform URL Format
Mobile/Desktop file:///path/to/file.jpg
Web blob:http://localhost/...

Downloading#

Trigger a file download (Web) or share sheet (Mobile):

dart
await Storage.download('reports/2024.pdf', name: 'Annual Report.pdf');

On mobile and desktop the share sheet comes from the magic_picker plugin. When nothing sets LocalAdapterContract.downloadHandler, download throws UnsupportedError.

Deleting Files#

dart
await Storage.delete('avatars/1.jpg');

Cached Network Images#

MagicImage draws a network image from a copy kept on the device. The first draw of an address fetches it and stores the bytes; every draw after that, in this launch or a later one, reads the file and makes no request. Flutter's own NetworkImage keeps images in memory only, so a poster grid asks the host again on every launch.

The copies live on a local disk named images. Declare it in your filesystems config (defaultFilesystemsConfig carries the entry, but Magic.init does not merge that map by itself, so spread it or copy it), and keep it out of backup, since every file in it can be fetched again:

dart
'images': {
  'driver': 'local',
  'root': 'images',
  'exclude_from_backup': true,
},

Draw through MagicImage.of, which answers an ImageProvider or null when there is no address:

dart
final ImageProvider? poster = MagicImage.of(item.posterUrl, cacheWidth: 300);

if (poster != null) Image(image: poster);

A Stand-In Address#

An item often has two pictures: one a third party sent with a listing, and a better one from your own server once it knows the item. Pass the better one as the address and the other as fallback:

dart
MagicImage.of(item.catalogueUrl, fallback: item.listedUrl);

The fallback draws at once when it is already on the device and the address is not; the address replaces it when it lands. When the address cannot be fetched, the fallback is fetched instead and stays, and the next resolve after retryAfter (five minutes) asks the address again.

Size, Failures and Cleaning Up#

  • Size. The folder is capped at 300 MiB. It is trimmed, least recently used first, on the first use of each launch and again after a quarter of the cap has been written. Pass maxBytes to a MagicImageCache of your own to change the cap.
  • Failures. A URL that failed is not asked again for retryAfter, so a grid scrolled past a dead picture does not hit the host on every frame. A body that is not a JPEG, PNG, GIF or WebP, or is larger than 8 MiB, is refused. An error message never carries the address.
  • Decoding. cacheWidth decodes no wider than that many pixels and never scales up. Only the first frame of an animated GIF is drawn.
  • A full disk. An image the disk will not keep is still drawn, from memory, and the host is not blamed: MagicImageCache.file throws a MagicImageNotStored that carries the bytes.
  • A disk that is not there. When the disk is not declared, is not a local disk, or the platform gives no folder for it, images are drawn as NetworkImage would draw them, with no copy kept. One warning is logged, and the disk is opened again on the next call.
  • Cleaning up. await MagicImageCache.instance.clear() removes every stored image and keeps the folder, for a sign-out or a "clear cache" action. It exists on the web too, as a no-op.
  • Another disk. MagicImageCache(disk: 'pictures') keeps the files on another local disk. Assign it to MagicImageCache.instance before the first draw.
  • Web. There is no file store in a browser, so MagicImage.of answers a plain NetworkImage and the stand-in is not drawn.

In a test, assign MagicImageCache.instance a cache built with a store over a temporary folder and a fetch that answers bytes, and restore the previous one in tearDown.

Multiple Disks#

Switch between configured disks:

dart
// Use the 'public' disk
await Storage.disk('public').put('file.txt', 'contents');

// Get from 'temp' disk
final bytes = await Storage.disk('temp').get('cache.dat');

MagicFile Reference#

MagicFile is the unified file wrapper used throughout Magic. It's what Storage.getFile returns, what the magic_picker plugin's Pick methods return, and it can be used with Storage.

Properties#

Property Type Description
path String? Original file path (null on Web)
name String File name with extension
size int? File size in bytes
mimeType String? MIME type (e.g., 'image/jpeg')
extension String Extension without dot (e.g., 'jpg')
isImage bool True for jpg, png, gif, webp, etc.
isVideo bool True for mp4, mov, avi, etc.

Methods#

readAsBytes()

Read file contents as Uint8List:

dart
final bytes = await file.readAsBytes();

store(path, {disk})

Store to Magic Storage:

dart
final storedPath = await file.store('uploads/photo.jpg');
// Returns: 'uploads/photo.jpg'

storeAs(directory, {disk})

Store with auto-generated unique name:

dart
final storedPath = await file.storeAs('uploads');
// Returns: 'uploads/1704067200000_photo.jpg'

upload(url, {fieldName, data, headers})

Upload directly to server:

dart
final response = await file.upload(
  '/api/upload',
  fieldName: 'avatar',
  data: {'user_id': 1},
);

if (response.successful) {
  print(response['url']);
}

Creating MagicFile Manually#

dart
final file = MagicFile(
  name: 'document.pdf',
  size: 1024,
  mimeType: 'application/pdf',
  bytes: pdfBytes,
);

Testing#

Replacing the manager#

Inject a custom StorageManager subclass with Storage.setManager() to control disk behaviour in tests:

dart
class FakeStorageManager extends StorageManager {
  // Override disk() to return a test double
}

setUp(() {
  Storage.setManager(FakeStorageManager());
});

Resetting state between tests#

Call Storage.flush() to clear the cached manager and all cached disk instances. This ensures no state leaks between tests:

dart
tearDown(() {
  Storage.flush();
});

flush() sets the internal manager to null and calls flush() on it first (clearing any cached disk instances), so the next call to any Storage method initialises a fresh manager from config.

esc

Search index did not load.

No match for "".

Try a class like rounded-lg or a widget like WButton.