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.

Descriptor-Based Uploads

Use vyuh_server_plugin_file_storage when a feature owns file or image fields and should expose standard signed-upload routes. It is a capability plugin layered on top of the storage backend:

RoutePurpose
POST /{routePrefix}/signValidate extension/type and return a signed upload URL
POST /{routePrefix}/commitVerify the object exists and return a scheme-prefixed storage path
POST /{routePrefix}/sign-readBatch-sign scheme paths for previews or downloads

The descriptor owns the bucket, URI scheme, route prefix, allowed file types, size limits, path strategy, access check, and signed URL lifetime. Pair it with the Flutter vyuh_plugin_file_storage client package.

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