Skip to content

Arcade Storage

The arcade_storage package provides a flexible storage abstraction for Arcade applications, allowing you to implement various object storage backends with a consistent API. It defines the standard interface that all storage implementations must follow.

Add arcade_storage to your pubspec.yaml:

dependencies:
arcade_storage: ^<latest-version>

The BaseStorageManager<C> abstract interface defines the standard interface that all storage implementations must follow:

abstract interface class BaseStorageManager<C> {
// Initialize the storage connection
Future<void> init(C connectionInfo);
// Dispose the storage connection
Future<void> dispose();
// Bucket operations
Future<void> createBucket(String bucketName);
Future<bool> bucketExists(String bucketName);
Future<List<BucketInfo>> listBuckets();
Future<void> deleteBucket(String bucketName);
// Object operations
Future<void> putObject(
String bucket,
String objectName,
Stream<List<int>> data, {
int? length,
});
Future<Stream<List<int>>> getObject(String bucket, String objectName);
Future<void> deleteObject(String bucket, String objectName);
Future<void> deleteObjects(String bucket, List<String> objectNames);
Future<ObjectMetadata> statObject(String bucket, String objectName);
Future<List<ObjectInfo>> listObjects(String bucket, {String? prefix});
// Copy and file operations
Future<void> copyObject(String bucket, String source, String destination);
Future<void> fPutObject(String bucket, String objectName, String filePath);
Future<void> fGetObject(String bucket, String objectName, String filePath);
}

The package defines several data models:

class BucketInfo {
final String name;
final DateTime? creationDate;
}
class ObjectInfo {
final String name;
final int size;
final DateTime? lastModified;
final String? etag;
}
class ObjectMetadata {
final int size;
final String? etag;
final DateTime? lastModified;
final String? contentType;
final Map<String, String?>? metaData;
}
import 'package:arcade_storage/arcade_storage.dart';
// Use any BaseStorageManager implementation
void main() async {
final storage = MinioStorageManager(); // or any other implementation
await storage.init(connectionInfo);
// Create a bucket
if (!await storage.bucketExists('my-bucket')) {
await storage.createBucket('my-bucket');
}
// Upload an object
final dataStream = Stream.value('Hello, Storage!'.codeUnits);
await storage.putObject(
'my-bucket',
'my-object.txt',
dataStream,
length: 'Hello, Storage!'.length,
);
// Download an object
final downloadStream = await storage.getObject('my-bucket', 'my-object.txt');
final chunks = await downloadStream.toList();
final content = String.fromCharCodes(chunks.expand((chunk) => chunk));
// Get object metadata
final metadata = await storage.statObject('my-bucket', 'my-object.txt');
print('Size: ${metadata.size}');
print('Content-Type: ${metadata.contentType}');
// List objects
final objects = await storage.listObjects('my-bucket', prefix: 'my-');
// Delete object
await storage.deleteObject('my-bucket', 'my-object.txt');
// Clean up
await storage.dispose();
}

To create a custom storage implementation, implement the BaseStorageManager interface:

class MyCustomStorageManager implements BaseStorageManager<MyConnectionInfo> {
@override
Future<void> init(MyConnectionInfo connectionInfo) async {
// Initialize your storage connection
}
@override
Future<void> dispose() async {
// Clean up resources
}
@override
Future<void> createBucket(String bucketName) async {
// Implement bucket creation
}
// Implement all other required methods...
}
import 'package:arcade/arcade.dart';
import 'package:arcade_storage/arcade_storage.dart';
import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;
void main() async {
// Initialize storage
final storage = MinioStorageManager();
await storage.init((
endPoint: 'localhost:9000',
accessKey: 'minioadmin',
secretKey: 'minioadmin',
useSSL: false,
));
// Register with get_it
getIt.registerSingleton<BaseStorageManager>(storage);
await runServer(
port: 3000,
init: () {
// Your routes can now use getIt<BaseStorageManager>()
},
);
}
route.post('/upload')
.handle((context) async {
final storage = getIt<BaseStorageManager>();
final multipart = await context.multipart();
await for (final file in multipart.files) {
final fileName = file.filename ?? 'upload-${DateTime.now().millisecondsSinceEpoch}';
await storage.putObject(
'uploads',
fileName,
file.data,
length: file.contentLength,
);
}
return {'status': 'uploaded'};
});
route.get('/download/:fileName')
.handle((context) async {
final storage = getIt<BaseStorageManager>();
final fileName = context.pathParameters['fileName']!;
// Get metadata first
final metadata = await storage.statObject('uploads', fileName);
// Set response headers
context.responseHeaders.set('content-type', metadata.contentType ?? 'application/octet-stream');
context.responseHeaders.set('content-length', metadata.size.toString());
// Stream the file
final stream = await storage.getObject('uploads', fileName);
return stream;
});
  1. Connection Management: Always dispose storage managers when done
  2. Error Handling: Handle storage errors gracefully
  3. Stream Handling: For large files, use streams to avoid memory issues
  4. Bucket Naming: Use consistent naming conventions for buckets
  5. Metadata: Leverage metadata for content-type detection and validation
  6. Prefix Filtering: Use prefix filtering when listing large numbers of objects