Skip to content

File Storage — vyuh_server_plugin_storage

StoragePlugin for files / blobs — avatars, document uploads, exported reports, generated PDFs. Distinct from DbPlugin (relational data); this is the file-storage half of the runtime, addressable as vyuh.storage.

Operations are keyed by bucket (top-level container) + path (slash-delimited object key within it). The package ships a SupabaseStoragePlugin implementation; the same plugin shape works for S3, GCS, or any other backend you wire.

Install

yaml
dependencies:
  vyuh_server_plugin_storage:
    hosted: https://pub.vyuh.tech
    version: ^0.1.1

Wiring

The Supabase implementation resolves its SupabaseClient from vyuh.di by default — the canonical pattern is to put a an app-provided plugin that registers SupabaseClient ahead of it in the plugins list.

dart
import 'package:vyuh_server/vyuh_server.dart';
import 'package:vyuh_server_plugin_storage/vyuh_server_plugin_storage.dart';

final runtime = await VyuhServer.bootstrap(
  name: 'media-api',
  plugins: [
    EnvPlugin(),
    YourSupabaseClientPlugin(),   // registers SupabaseClient in vyuh.di
    SupabaseStoragePlugin(),      // resolves it from vyuh.di
    // ...
  ],
);

For tests or for hosts that own the SupabaseClient lifecycle themselves, pass an explicit client:

dart
SupabaseStoragePlugin(client: myExplicitClient);

Using vyuh.storage

Upload

dart
final stored = await vyuh.storage.upload(
  bucket: 'avatars',
  path: 'users/$userId.png',
  bytes: pngBytes,
  contentType: 'image/png',
  upsert: true,  // overwrite if exists; default false → 409 on collision
);

Returns the canonical stored path.

Download

dart
final bytes = await vyuh.storage.download(
  bucket: 'reports',
  path: 'q3/summary.pdf',
);

Public URL (sync — no IO)

For objects in publicly readable buckets, getPublicUrl is pure URL construction and synchronous:

dart
final url = vyuh.storage.getPublicUrl(
  bucket: 'public-assets',
  path: 'logos/brand.svg',
);

Use this for assets the client should fetch directly from the CDN without going through the API.

Signed URL (async, TTL)

For private buckets, mint a short-lived signed URL:

dart
final signed = await vyuh.storage.createSignedUrl(
  bucket: 'reports',
  path: 'q3/$tenantId-summary.pdf',
  expiresIn: Duration(minutes: 5),
);
return Response(
  statusCode: 307,
  headers: Headers({'location': signed}),
);

The default expiry is 15 minutes; tune to match your URL-sharing profile.

List

dart
final objects = await vyuh.storage.list(
  bucket: 'avatars',
  prefix: 'users/',
  limit: 100,
);

for (final obj in objects) {
  obj.name;          // 'users/abc123.png'
  obj.size;          // bytes
  obj.contentType;   // 'image/png'
  obj.lastModified;  // DateTime
  obj.etag;          // backend etag, opaque
}

StorageObject keeps only the fields every reasonable backend can produce — your handler can serialize them directly to the client.

Move and Copy

dart
// Move within a bucket (rename)
await vyuh.storage.move(
  bucket: 'tmp',
  fromPath: 'upload-$nonce.bin',
  toPath: 'archive/$id.bin',
);

// Copy within a bucket
await vyuh.storage.copy(
  bucket: 'reports',
  fromPath: 'live.pdf',
  toPath: 'history/$timestamp.pdf',
);

Delete

dart
await vyuh.storage.delete(
  bucket: 'avatars',
  paths: ['users/$userId.png'],
);

The list accepts multiple paths for batch delete. Backends typically no-op (rather than throwing) for already-missing paths — check return values from any higher-level wrapper if you need exact accounting.

Error Handling

The framework translates the backend's error type into a single StorageException at the boundary, so handlers catch one shape regardless of which backend is wired:

dart
try {
  await vyuh.storage.download(bucket: 'reports', path: missingPath);
} on StorageException catch (e) {
  // e.code (e.g. 'not_found'), e.message, e.cause
  throw StructuredException(
    code: 'reports.not_found',
    message: 'Report $missingPath was not found',
  );
}

Pair with ErrorCodesDescriptor so a thrown StructuredException formats into the right HTTP status — see Error Handling.

Wiring an Upload Endpoint

A complete upload route, with size cap and content-type guard:

dart
final mediaModule = RouteModule(
  name: 'app.media',
  basePath: '/media',
  protectedPaths: ['/'],
  setup: (scope) {
    scope.post('/avatar', _uploadAvatar);
  },
);

Future<Response> _uploadAvatar(Request req) async {
  final actor = req.actor;
  final bytes = await req.readAsBytes();

  if (bytes.length > 2 * 1024 * 1024) {
    throw StructuredException(
      code: 'media.too_large',
      message: 'Avatar must be 2 MB or smaller',
    );
  }

  final contentType = req.headers.value('content-type') ?? 'image/png';
  if (!const ['image/png', 'image/jpeg', 'image/webp'].contains(contentType)) {
    throw StructuredException(
      code: 'media.bad_content_type',
      message: 'Avatar must be png, jpeg, or webp',
    );
  }

  await vyuh.storage.upload(
    bucket: 'avatars',
    path: 'users/${actor.id}',
    bytes: bytes,
    contentType: contentType,
    upsert: true,
  );

  vyuh.telemetry.counter('media.avatar.uploaded', delta: 1);
  return jsonResponse(
    statusCode: 201,
    body: {
      'success': true,
      'data': {
        'url': vyuh.storage.getPublicUrl(
          bucket: 'avatars',
          path: 'users/${actor.id}',
        ),
      },
    },
  );
}

Storage vs DB

The two slots are complementary and unrelated; each is independently optional.

SlotVerbPlugin typeBackend examples
Relationalvyuh.dbDbPluginPostgresDbPlugin, future SQLite, etc.
File / blobvyuh.storageStoragePluginSupabaseStoragePlugin, future S3, GCS, etc.

A typical web service uses both — relational rows referencing storage paths (e.g., a users.avatar_path TEXT column whose value is passed to vyuh.storage.download(...) or getPublicUrl(...)).

Where to Go Next