rivium_storage 0.1.0 copy "rivium_storage: ^0.1.0" to clipboard
rivium_storage: ^0.1.0 copied to clipboard

Official Flutter SDK for RiviumStorage - File storage and image transformation service

example/lib/main.dart

import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:rivium_storage/rivium_storage.dart';

/// RiviumStorage Flutter SDK - Complete Example
///
/// This example demonstrates ALL capabilities of the RiviumStorage SDK:
/// - Bucket operations (list, get by ID, get by name)
/// - File operations (upload, list, get, download, delete)
/// - URL generation (public URL, transform URL, download URL)
/// - Image transformations (resize, format, quality, effects)
/// - Error handling
///
/// How it works:
/// - Only the API key and bucket name are configured manually.
/// - All IDs (bucket ID, file IDs, paths) are captured from API responses
///   and reused by subsequent operations — nothing is hardcoded.
/// - Run the buttons top-to-bottom for the best experience.

void main() {
  runApp(const RiviumStorageExampleApp());
}

// ============================================================
// Configuration — only these two values need to be set
// ============================================================

// Replace with your actual API key from the Rivium Console
const String apiKey = 'YOUR_API_KEY';
const String bucketName = 'my-bucket';

// User ID for bucket policy enforcement (sent as x-user-id header)
// Set to null to test unauthenticated access
const String userId = 'demo-user-123';

class RiviumStorageExampleApp extends StatelessWidget {
  const RiviumStorageExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'RiviumStorage Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const ExampleHomePage(),
    );
  }
}

class ExampleHomePage extends StatefulWidget {
  const ExampleHomePage({super.key});

  @override
  State<ExampleHomePage> createState() => _ExampleHomePageState();
}

class _ExampleHomePageState extends State<ExampleHomePage> {
  final RiviumStorage storage =
      RiviumStorage(apiKey: apiKey, userId: userId, timeout: 30);
  final List<LogEntry> logs = [];
  final ScrollController _scrollController = ScrollController();

  // State captured from API responses — no hardcoded IDs
  String? _lastBucketId;
  String? _lastFileId;
  String? _lastFilePath;
  String? _lastImageFileId;
  final List<String> _uploadedFileIds = [];

  void log(String message, {bool isError = false}) {
    setState(() {
      logs.add(LogEntry(message, isError: isError));
    });
    // Auto-scroll to bottom
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_scrollController.hasClients) {
        _scrollController.animateTo(
          _scrollController.position.maxScrollExtent,
          duration: const Duration(milliseconds: 300),
          curve: Curves.easeOut,
        );
      }
    });
  }

  void clearLogs() {
    setState(() {
      logs.clear();
    });
  }

  @override
  void dispose() {
    storage.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('RiviumStorage SDK Example'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        actions: [
          IconButton(
            icon: const Icon(Icons.delete_outline),
            onPressed: clearLogs,
            tooltip: 'Clear logs',
          ),
        ],
      ),
      body: Column(
        children: [
          // Buttons section
          Expanded(
            flex: 2,
            child: SingleChildScrollView(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  // Bucket Operations
                  const SectionHeader(title: '📦 Bucket Operations'),
                  ExampleButton(
                    label: 'List All Buckets',
                    onPressed: () => _listBuckets(),
                  ),
                  ExampleButton(
                    label: 'Get Bucket by ID',
                    onPressed: () => _getBucketById(),
                  ),
                  ExampleButton(
                    label: 'Get Bucket by Name',
                    onPressed: () => _getBucketByName(),
                  ),

                  // File Operations
                  const SectionHeader(title: '📄 File Operations'),
                  ExampleButton(
                    label: 'Upload Text File',
                    onPressed: () => _uploadTextFile(),
                  ),
                  ExampleButton(
                    label: 'Upload Image (PNG)',
                    onPressed: () => _uploadImage(),
                  ),
                  ExampleButton(
                    label: 'List Files',
                    onPressed: () => _listFiles(),
                  ),
                  ExampleButton(
                    label: 'Get File by ID',
                    onPressed: () => _getFileById(),
                  ),
                  ExampleButton(
                    label: 'Get File by Path',
                    onPressed: () => _getFileByPath(),
                  ),
                  ExampleButton(
                    label: 'Download File',
                    onPressed: () => _downloadFile(),
                  ),
                  ExampleButton(
                    label: 'Delete File',
                    onPressed: () => _deleteFile(),
                  ),
                  ExampleButton(
                    label: 'Delete by Path',
                    onPressed: () => _deleteByPath(),
                  ),
                  ExampleButton(
                    label: 'Delete Multiple Files',
                    onPressed: () => _deleteMany(),
                  ),

                  // URL Generation
                  const SectionHeader(title: '🔗 URL Generation'),
                  ExampleButton(
                    label: 'Generate All URL Types',
                    onPressed: () => _generateUrls(),
                  ),

                  // Image Transforms
                  const SectionHeader(title: '🖼️ Image Transformations'),
                  ExampleButton(
                    label: 'Show All Transform Options',
                    onPressed: () => _showTransforms(),
                  ),

                  // Policy Testing
                  const SectionHeader(title: '🛡️ Policy Testing'),
                  ExampleButton(
                    label: 'Test: No Rules (allow all)',
                    onPressed: () => _testNoRules(),
                  ),
                  ExampleButton(
                    label: 'Test: Private (login required)',
                    onPressed: () => _testPrivate(),
                  ),
                  ExampleButton(
                    label: 'Test: Public Read',
                    onPressed: () => _testPublicRead(),
                  ),
                  ExampleButton(
                    label: 'Test: User Folders',
                    onPressed: () => _testUserFolders(),
                  ),
                  ExampleButton(
                    label: 'Test: Images Only',
                    onPressed: () => _testImagesOnly(),
                  ),

                  // Error Handling
                  const SectionHeader(title: '⚠️ Error Handling'),
                  ExampleButton(
                    label: 'Demonstrate Error Handling',
                    onPressed: () => _demonstrateErrorHandling(),
                  ),

                  const SizedBox(height: 16),
                ],
              ),
            ),
          ),

          // Divider
          const Divider(height: 1),

          // Log output section
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            color: Colors.grey[100],
            child: Row(
              children: [
                const Icon(Icons.terminal, size: 16),
                const SizedBox(width: 8),
                const Text(
                  'Output Log',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
                const Spacer(),
                Text(
                  '${logs.length} entries',
                  style: TextStyle(color: Colors.grey[600], fontSize: 12),
                ),
              ],
            ),
          ),
          Expanded(
            flex: 3,
            child: Container(
              color: Colors.grey[900],
              child: ListView.builder(
                controller: _scrollController,
                padding: const EdgeInsets.all(12),
                itemCount: logs.length,
                itemBuilder: (context, index) {
                  final entry = logs[index];
                  return Text(
                    entry.message,
                    style: TextStyle(
                      fontFamily: 'monospace',
                      fontSize: 12,
                      color:
                          entry.isError ? Colors.red[300] : Colors.green[300],
                    ),
                  );
                },
              ),
            ),
          ),
        ],
      ),
    );
  }

  // ============================================================
  // Bucket Operations
  // ============================================================

  Future<void> _listBuckets() async {
    log('Listing all buckets...');
    try {
      final buckets = await storage.listBuckets();
      log('✅ Found ${buckets.length} bucket(s):');
      for (final bucket in buckets) {
        log('   - ${bucket.name} (${bucket.visibility}) [${bucket.id}]');
      }
      // Store the first bucket ID for subsequent operations
      if (buckets.isNotEmpty) {
        _lastBucketId = buckets.first.id;
        log('');
        log('   ℹ️ Stored bucket ID: ${_lastBucketId!} for next operations');
      }
    } on RiviumStorageException catch (e) {
      log('❌ Error: ${e.message}', isError: true);
    }
  }

  Future<void> _getBucketById() async {
    if (_lastBucketId == null) {
      log('⚠️ No bucket ID available. Run "List All Buckets" first.',
          isError: true);
      return;
    }
    log('Getting bucket by ID: $_lastBucketId');
    try {
      final bucket = await storage.getBucket(_lastBucketId!);
      log('✅ Bucket: ${bucket.name}');
      log('   - ID: ${bucket.id}');
      log('   - Visibility: ${bucket.visibility}');
      log('   - Policies Enabled: ${bucket.policiesEnabled}');
      log('   - Active: ${bucket.isActive}');
      if (bucket.allowedMimeTypes != null) {
        log('   - Allowed MIME: ${bucket.allowedMimeTypes!.join(", ")}');
      }
      if (bucket.maxFileSize != null) {
        log('   - Max File Size: ${_formatBytes(bucket.maxFileSize!)}');
      }
    } on RiviumStorageException catch (e) {
      log('❌ Error: ${e.message}', isError: true);
    }
  }

  Future<void> _getBucketByName() async {
    log('Getting bucket by name: $bucketName');
    try {
      final bucket = await storage.getBucketByName(bucketName);
      log('✅ Found: ${bucket.name} (${bucket.id})');
      // Also store the bucket ID from this response
      _lastBucketId = bucket.id;
      log('   ℹ️ Stored bucket ID: ${bucket.id}');
    } on RiviumStorageException catch (e) {
      log('❌ Error: ${e.message}', isError: true);
    }
  }

  // ============================================================
  // File Operations
  // ============================================================

  Future<void> _uploadTextFile() async {
    if (_lastBucketId == null) {
      log('⚠️ No bucket ID available. Run "List All Buckets" or "Get Bucket by Name" first.',
          isError: true);
      return;
    }

    final content =
        'Hello, RiviumStorage! Timestamp: ${DateTime.now().millisecondsSinceEpoch}';
    final data = Uint8List.fromList(content.codeUnits);
    final path = 'examples/test-${DateTime.now().millisecondsSinceEpoch}.txt';

    log('Uploading text file: $path');
    try {
      final file = await storage.upload(
        _lastBucketId!,
        path,
        data,
        options: const UploadOptions(
          contentType: 'text/plain',
          metadata: {'author': 'Flutter Example', 'version': '1.0'},
        ),
      );
      log('✅ Uploaded: ${file.fileName}');
      log('   - ID: ${file.id}');
      log('   - Path: ${file.path}');
      log('   - Size: ${_formatBytes(file.size)}');
      log('   - MIME: ${file.mimeType}');
      if (file.url != null) {
        log('   - URL: ${file.url}');
      }
      // Store for subsequent operations
      _lastFileId = file.id;
      _lastFilePath = file.path;
      _uploadedFileIds.add(file.id);
      log('   ℹ️ Stored file ID: ${file.id}');
    } on RiviumStorageException catch (e) {
      log('❌ Error: ${e.message}', isError: true);
    }
  }

  Future<void> _uploadImage() async {
    if (_lastBucketId == null) {
      log('⚠️ No bucket ID available. Run "List All Buckets" or "Get Bucket by Name" first.',
          isError: true);
      return;
    }

    // 1x1 red PNG
    final redPixelPNG = Uint8List.fromList([
      0x89,
      0x50,
      0x4E,
      0x47,
      0x0D,
      0x0A,
      0x1A,
      0x0A,
      0x00,
      0x00,
      0x00,
      0x0D,
      0x49,
      0x48,
      0x44,
      0x52,
      0x00,
      0x00,
      0x00,
      0x01,
      0x00,
      0x00,
      0x00,
      0x01,
      0x08,
      0x02,
      0x00,
      0x00,
      0x00,
      0x90,
      0x77,
      0x53,
      0xDE,
      0x00,
      0x00,
      0x00,
      0x0C,
      0x49,
      0x44,
      0x41,
      0x54,
      0x08,
      0xD7,
      0x63,
      0xF8,
      0xCF,
      0xC0,
      0x00,
      0x00,
      0x00,
      0x03,
      0x00,
      0x01,
      0x00,
      0x05,
      0xFE,
      0xD4,
      0xEF,
      0x00,
      0x00,
      0x00,
      0x00,
      0x49,
      0x45,
      0x4E,
      0x44,
      0xAE,
      0x42,
      0x60,
      0x82
    ]);
    final path =
        'examples/images/sample-${DateTime.now().millisecondsSinceEpoch}.png';

    log('Uploading image: $path');
    try {
      final file = await storage.upload(
        _lastBucketId!,
        path,
        redPixelPNG,
        options: const UploadOptions(contentType: 'image/png'),
      );
      log('✅ Uploaded: ${file.fileName}');
      log('   - ID: ${file.id}');
      log('   - Size: ${_formatBytes(file.size)}');
      // Store image file ID for URL generation and transforms
      _lastImageFileId = file.id;
      _lastFileId = file.id;
      _lastFilePath = file.path;
      _uploadedFileIds.add(file.id);
      log('   ℹ️ Stored image file ID: ${file.id}');
    } on RiviumStorageException catch (e) {
      log('❌ Error: ${e.message}', isError: true);
    }
  }

  Future<void> _listFiles() async {
    if (_lastBucketId == null) {
      log('⚠️ No bucket ID available. Run "List All Buckets" or "Get Bucket by Name" first.',
          isError: true);
      return;
    }

    log('Listing files (prefix: examples/, limit: 10)...');
    try {
      final result = await storage.listFiles(
        _lastBucketId!,
        options: const ListFilesOptions(prefix: 'examples/', limit: 10),
      );
      log('✅ Found ${result.files.length} file(s):');
      for (final file in result.files) {
        log('   - ${file.path} (${_formatBytes(file.size)}) [${file.id}]');
      }
      if (result.nextCursor != null) {
        log('   (More files available, cursor: ${result.nextCursor!.substring(0, 20)}...)');
      }
      // Store first file from listing if we don't have one yet
      if (result.files.isNotEmpty && _lastFileId == null) {
        _lastFileId = result.files.first.id;
        _lastFilePath = result.files.first.path;
        log('');
        log('   ℹ️ Stored file ID: ${_lastFileId!} from listing');
      }
    } on RiviumStorageException catch (e) {
      log('❌ Error: ${e.message}', isError: true);
    }
  }

  Future<void> _getFileById() async {
    if (_lastFileId == null) {
      log('⚠️ No file ID available. Upload a file or run "List Files" first.',
          isError: true);
      return;
    }
    log('Getting file by ID: $_lastFileId');
    try {
      final file = await storage.getFile(_lastFileId!);
      log('✅ Found: ${file.fileName}');
      log('   - Path: ${file.path}');
      log('   - Size: ${_formatBytes(file.size)}');
      log('   - MIME: ${file.mimeType}');
      log('   - Created: ${file.createdAt}');
      log('   - Updated: ${file.updatedAt}');
    } on RiviumStorageException catch (e) {
      log('❌ Error: ${e.message}', isError: true);
    }
  }

  Future<void> _getFileByPath() async {
    if (_lastBucketId == null || _lastFilePath == null) {
      log('⚠️ No bucket or file path available. Upload a file first.',
          isError: true);
      return;
    }
    log('Getting file by path: $_lastFilePath');
    try {
      final file = await storage.getFileByPath(_lastBucketId!, _lastFilePath!);
      log('✅ Found: ${file.fileName} (${file.id})');
      log('   - Size: ${_formatBytes(file.size)}');
      log('   - MIME: ${file.mimeType}');
    } on RiviumStorageException catch (e) {
      log('❌ Error: ${e.message}', isError: true);
    }
  }

  Future<void> _downloadFile() async {
    if (_lastFileId == null) {
      log('⚠️ No file ID available. Upload a file first.', isError: true);
      return;
    }
    log('Downloading file: $_lastFileId');
    try {
      final data = await storage.download(_lastFileId!);
      log('✅ Downloaded ${_formatBytes(data.length)}');
      // Show content preview for small text files
      if (data.length < 200) {
        try {
          final content = String.fromCharCodes(data);
          log('   Content: "$content"');
        } catch (_) {
          log('   (Binary content)');
        }
      }
    } on RiviumStorageException catch (e) {
      log('❌ Error: ${e.message}', isError: true);
    }
  }

  Future<void> _deleteFile() async {
    if (_lastFileId == null) {
      log('⚠️ No file ID available. Upload a file first.', isError: true);
      return;
    }
    log('Deleting file: $_lastFileId');
    try {
      await storage.delete(_lastFileId!);
      log('✅ Deleted successfully');
      _uploadedFileIds.remove(_lastFileId);
      _lastFileId = _uploadedFileIds.isNotEmpty ? _uploadedFileIds.last : null;
      _lastFilePath = null;
    } on RiviumStorageException catch (e) {
      log('❌ Error: ${e.message}', isError: true);
    }
  }

  Future<void> _deleteByPath() async {
    if (_lastBucketId == null || _lastFilePath == null) {
      log('⚠️ No bucket or file path available. Upload a file first.',
          isError: true);
      return;
    }
    log('Deleting file by path: $_lastFilePath');
    try {
      await storage.deleteByPath(_lastBucketId!, _lastFilePath!);
      log('✅ Deleted successfully');
      _uploadedFileIds.remove(_lastFileId);
      _lastFileId = _uploadedFileIds.isNotEmpty ? _uploadedFileIds.last : null;
      _lastFilePath = null;
    } on RiviumStorageException catch (e) {
      log('❌ Error: ${e.message}', isError: true);
    }
  }

  Future<void> _deleteMany() async {
    if (_uploadedFileIds.isEmpty) {
      log('⚠️ No uploaded file IDs tracked. Upload some files first.',
          isError: true);
      return;
    }
    // Copy the list since we'll clear it
    final idsToDelete = List<String>.from(_uploadedFileIds);
    log('Deleting ${idsToDelete.length} file(s): ${idsToDelete.join(', ')}');
    try {
      final result = await storage.deleteMany(idsToDelete);
      log('✅ Deleted ${result.deleted} file(s)');
      _uploadedFileIds.clear();
      _lastFileId = null;
      _lastFilePath = null;
      _lastImageFileId = null;
    } on RiviumStorageException catch (e) {
      log('❌ Error: ${e.message}', isError: true);
    }
  }

  // ============================================================
  // URL Generation
  // ============================================================

  void _generateUrls() {
    if (_lastFileId == null) {
      log('⚠️ No file ID available. Upload a file first.', isError: true);
      return;
    }
    final fileId = _lastFileId!;

    log('Generating URLs for file: $fileId');
    log('');

    // Public URL
    final publicUrl = storage.getUrl(fileId);
    log('📎 Public URL:');
    log('   $publicUrl');

    // Download URL
    final downloadUrl = storage.getDownloadUrl(fileId);
    log('');
    log('📥 Download URL:');
    log('   $downloadUrl');

    // Transform URL (thumbnail)
    final thumbnailUrl = storage.getTransformUrl(
      fileId,
      transforms: const ImageTransforms(width: 200, height: 200),
    );
    log('');
    log('🖼️ Thumbnail URL (200x200):');
    log('   $thumbnailUrl');

    // Transform URL (advanced)
    final advancedUrl = storage.getTransformUrl(
      fileId,
      transforms: const ImageTransforms(
        width: 800,
        height: 600,
        fit: 'cover',
        format: 'webp',
        quality: 85,
      ),
    );
    log('');
    log('🎨 Advanced Transform URL:');
    log('   $advancedUrl');
  }

  // ============================================================
  // Image Transformations
  // ============================================================

  void _showTransforms() {
    // Prefer the image file, fall back to any file
    final fileId = _lastImageFileId ?? _lastFileId;
    if (fileId == null) {
      log('⚠️ No file ID available. Upload an image first.', isError: true);
      return;
    }

    log('Image Transform Examples (file: $fileId):');
    log('=' * 50);

    final transforms = <String, ImageTransforms>{
      'Resize 200x200': const ImageTransforms(width: 200, height: 200),
      'Width only (auto height)': const ImageTransforms(width: 400),
      'Height only (auto width)': const ImageTransforms(height: 300),
      'Fit: cover':
          const ImageTransforms(width: 200, height: 200, fit: 'cover'),
      'Fit: contain':
          const ImageTransforms(width: 200, height: 200, fit: 'contain'),
      'Fit: fill': const ImageTransforms(width: 200, height: 200, fit: 'fill'),
      'Format: WebP': const ImageTransforms(width: 200, format: 'webp'),
      'Format: AVIF': const ImageTransforms(width: 200, format: 'avif'),
      'Format: JPEG': const ImageTransforms(width: 200, format: 'jpeg'),
      'Quality: 50%':
          const ImageTransforms(width: 200, format: 'jpeg', quality: 50),
      'Quality: 90%':
          const ImageTransforms(width: 200, format: 'jpeg', quality: 90),
      'Blur effect': const ImageTransforms(width: 200, blur: 10),
      'Sharpen effect': const ImageTransforms(width: 200, sharpen: 50),
      'Rotate 90°': const ImageTransforms(rotate: 90),
      'Rotate 180°': const ImageTransforms(rotate: 180),
      'Rotate 270°': const ImageTransforms(rotate: 270),
      'Combined transforms': const ImageTransforms(
        width: 400,
        height: 300,
        fit: 'cover',
        format: 'webp',
        quality: 80,
        sharpen: 20,
      ),
    };

    for (final entry in transforms.entries) {
      final url = storage.getTransformUrl(fileId, transforms: entry.value);
      log('');
      log('${entry.key}:');
      log('   $url');
    }
  }

  // ============================================================
  // Policy Testing
  // ============================================================
  //
  // These tests show what each template allows/blocks.
  // Apply the matching template in Dashboard > Bucket > Security Rules
  // before running each test.

  /// Helper: try an upload and report result
  Future<String> _tryUpload(String path, Uint8List data,
      {String? contentType}) async {
    try {
      final file = await storage.upload(
        _lastBucketId!,
        path,
        data,
        options: UploadOptions(contentType: contentType),
      );
      // Clean up: delete the uploaded file
      try {
        await storage.delete(file.id);
      } catch (_) {}
      return '✅ ALLOWED';
    } on RiviumStorageException catch (e) {
      if (e.statusCode == 403) return '❌ DENIED (${e.message})';
      return '⚠️ ERROR (${e.message})';
    }
  }

  /// Helper: try listing files and report result
  Future<String> _tryList() async {
    try {
      await storage.listFiles(_lastBucketId!);
      return '✅ ALLOWED';
    } on RiviumStorageException catch (e) {
      if (e.statusCode == 403) return '❌ DENIED';
      return '⚠️ ERROR (${e.message})';
    }
  }

  /// Helper: create test data
  Uint8List _textData() => Uint8List.fromList('test content'.codeUnits);

  Uint8List _pngData() => Uint8List.fromList([
        0x89,
        0x50,
        0x4E,
        0x47,
        0x0D,
        0x0A,
        0x1A,
        0x0A,
        0x00,
        0x00,
        0x00,
        0x0D,
        0x49,
        0x48,
        0x44,
        0x52,
        0x00,
        0x00,
        0x00,
        0x02,
        0x00,
        0x00,
        0x00,
        0x02,
        0x08,
        0x02,
        0x00,
        0x00,
        0x00,
        0xFD,
        0xD4,
        0x9A,
        0x73,
        0x00,
        0x00,
        0x00,
        0x14,
        0x49,
        0x44,
        0x41,
        0x54,
        0x78,
        0x9C,
        0x62,
        0xF8,
        0x0F,
        0x00,
        0x01,
        0x01,
        0x00,
        0x05,
        0x18,
        0xD8,
        0x4D,
        0x00,
        0x00,
        0x00,
        0x00,
        0x49,
        0x45,
        0x4E,
        0x44,
        0xAE,
        0x42,
        0x60,
        0x82,
      ]);

  Future<void> _ensureBucket() async {
    if (_lastBucketId != null) return;
    log('⚠️ No bucket. Running "List All Buckets" first...');
    await _listBuckets();
    if (_lastBucketId == null) {
      log('❌ No bucket available. Create one in the dashboard first.',
          isError: true);
    }
  }

  // ----------------------------------------------------------
  // Test: No Rules
  // ----------------------------------------------------------
  Future<void> _testNoRules() async {
    await _ensureBucket();
    if (_lastBucketId == null) return;

    log('');
    log('═══════════════════════════════════════');
    log('  TEST: No Rules (no policy on bucket)');
    log('  Dashboard: Delete the policy from bucket');
    log('  When no policy exists, all access is allowed');
    log('═══════════════════════════════════════');
    log('');
    log('Current userId: ${storage.userId ?? "none (unauthenticated)"}');
    log('');

    final ts = DateTime.now().millisecondsSinceEpoch;
    log('Upload text file:   ${await _tryUpload('test/no-rules-$ts.txt', _textData())}');
    log('Upload image:       ${await _tryUpload('test/no-rules-$ts.png', _pngData(), contentType: 'image/png')}');
    log('List files:         ${await _tryList()}');
    log('');
    log('Expected: Everything ✅ ALLOWED (no policy = no restrictions)');
  }

  // ----------------------------------------------------------
  // Test: Private template
  // ----------------------------------------------------------
  Future<void> _testPrivate() async {
    await _ensureBucket();
    if (_lastBucketId == null) return;

    log('');
    log('═══════════════════════════════════════');
    log('  TEST: Private Template');
    log('  Dashboard: Apply "Private" template');
    log('  Rule: Allow only authenticated users');
    log('  (default-deny: unauthenticated = denied)');
    log('═══════════════════════════════════════');
    log('');

    final ts = DateTime.now().millisecondsSinceEpoch;

    // Test WITH userId
    log('── With userId: ${storage.userId} ──');
    log('Upload text:   ${await _tryUpload('test/private-$ts.txt', _textData())}');
    log('Upload image:  ${await _tryUpload('test/private-$ts.png', _pngData(), contentType: 'image/png')}');
    log('List files:    ${await _tryList()}');
    log('');

    // Test WITHOUT userId
    final savedUserId = storage.userId;
    storage.setUserId(null);
    log('── Without userId (unauthenticated) ──');
    log('Upload text:   ${await _tryUpload('test/private-anon-$ts.txt', _textData())}');
    log('List files:    ${await _tryList()}');
    storage.setUserId(savedUserId);

    log('');
    log('Expected:');
    log('  With userId:    Everything ✅ ALLOWED');
    log('  Without userId: Everything ❌ DENIED');
  }

  // ----------------------------------------------------------
  // Test: Public Read template
  // ----------------------------------------------------------
  Future<void> _testPublicRead() async {
    await _ensureBucket();
    if (_lastBucketId == null) return;

    log('');
    log('═══════════════════════════════════════');
    log('  TEST: Public Read Template');
    log('  Dashboard: Apply "Public Read" template');
    log('  Rule: Anyone can read/list,');
    log('        auth required to write/delete');
    log('═══════════════════════════════════════');
    log('');

    final ts = DateTime.now().millisecondsSinceEpoch;

    // Test WITH userId
    log('── With userId: ${storage.userId} ──');
    log('Upload text:   ${await _tryUpload('test/public-$ts.txt', _textData())}');
    log('List files:    ${await _tryList()}');
    log('');

    // Test WITHOUT userId
    final savedUserId = storage.userId;
    storage.setUserId(null);
    log('── Without userId (unauthenticated) ──');
    log('List files:    ${await _tryList()}');
    log('Upload text:   ${await _tryUpload('test/public-anon-$ts.txt', _textData())}');
    storage.setUserId(savedUserId);

    log('');
    log('Expected:');
    log('  With userId:    Upload ✅, List ✅');
    log('  Without userId: List ✅, Upload ❌');
  }

  // ----------------------------------------------------------
  // Test: User Folders template
  // ----------------------------------------------------------
  Future<void> _testUserFolders() async {
    await _ensureBucket();
    if (_lastBucketId == null) return;

    log('');
    log('═══════════════════════════════════════');
    log('  TEST: User Folders Template');
    log('  Dashboard: Apply "User Folders" template');
    log('  Rule: Auth users can read/list all,');
    log('        write/delete only in users/{userId}/');
    log('═══════════════════════════════════════');
    log('');

    final uid = storage.userId ?? 'demo-user-123';
    final ts = DateTime.now().millisecondsSinceEpoch;

    log('── With userId: $uid ──');
    log('');
    log('Upload to own folder (users/$uid/):');
    log('  users/$uid/photo.txt:        ${await _tryUpload('users/$uid/photo-$ts.txt', _textData())}');
    log('  users/$uid/sub/doc.txt:      ${await _tryUpload('users/$uid/sub/doc-$ts.txt', _textData())}');
    log('');
    log('Upload to OTHER user folder:');
    log('  users/other-user/hack.txt:   ${await _tryUpload('users/other-user/hack-$ts.txt', _textData())}');
    log('');
    log('Upload to root (no user folder):');
    log('  test/random.txt:             ${await _tryUpload('test/random-$ts.txt', _textData())}');
    log('');
    log('List files:                    ${await _tryList()}');

    // Test without userId
    final savedUserId = storage.userId;
    storage.setUserId(null);
    log('');
    log('── Without userId (unauthenticated) ──');
    log('Upload:   ${await _tryUpload('users/anon/test-$ts.txt', _textData())}');
    log('List:     ${await _tryList()}');
    storage.setUserId(savedUserId);

    log('');
    log('Expected:');
    log('  Own folder:     ✅ ALLOWED');
    log('  Other folder:   ❌ DENIED');
    log('  Root path:      ❌ DENIED');
    log('  List:           ✅ ALLOWED');
    log('  No userId:      ❌ DENIED (all)');
  }

  // ----------------------------------------------------------
  // Test: Images Only template
  // ----------------------------------------------------------
  Future<void> _testImagesOnly() async {
    await _ensureBucket();
    if (_lastBucketId == null) return;

    log('');
    log('═══════════════════════════════════════');
    log('  TEST: Images Only Template');
    log('  Dashboard: Apply "Images Only" template');
    log('  Rule: Anyone can read/list/delete,');
    log('        only auth users can upload images');
    log('        (JPEG/PNG/GIF/WebP, 5MB max)');
    log('═══════════════════════════════════════');
    log('');

    final ts = DateTime.now().millisecondsSinceEpoch;

    log('── With userId: ${storage.userId} ──');
    log('');
    log('Upload PNG image:        ${await _tryUpload('test/image-$ts.png', _pngData(), contentType: 'image/png')}');
    log('Upload text file:        ${await _tryUpload('test/doc-$ts.txt', _textData(), contentType: 'text/plain')}');
    log('Upload PDF:              ${await _tryUpload('test/doc-$ts.pdf', _textData(), contentType: 'application/pdf')}');
    log('List files:              ${await _tryList()}');

    // Test without userId
    final savedUserId = storage.userId;
    storage.setUserId(null);
    log('');
    log('── Without userId (unauthenticated) ──');
    log('Upload PNG:   ${await _tryUpload('test/anon-$ts.png', _pngData(), contentType: 'image/png')}');
    log('Upload text:  ${await _tryUpload('test/anon-$ts.txt', _textData())}');
    log('List files:   ${await _tryList()}');
    storage.setUserId(savedUserId);

    log('');
    log('Expected:');
    log('  PNG image:      ✅ ALLOWED');
    log('  Text file:      ❌ DENIED (not an image)');
    log('  PDF file:       ❌ DENIED (not an image)');
    log('  List:           ✅ ALLOWED (read is open)');
    log('  No userId PNG:  ❌ DENIED (auth required for upload)');
    log('  No userId List: ✅ ALLOWED (read is open)');
  }

  // ============================================================
  // Error Handling
  // ============================================================

  Future<void> _demonstrateErrorHandling() async {
    log('Testing error handling with invalid file ID...');
    try {
      await storage.getFile('non-existent-file-id');
    } on RiviumStorageException catch (e) {
      log('');
      log('Caught RiviumStorageException:', isError: true);
      log('   Message: ${e.message}', isError: true);
      if (e.statusCode != null) {
        log('   Status Code: ${e.statusCode}', isError: true);
      }
      if (e.code != null) {
        log('   Error Code: ${e.code}', isError: true);
      }
      log('');
      log('Error handling example complete!');
    }
  }

  // ============================================================
  // Helpers
  // ============================================================

  String _formatBytes(int bytes) {
    if (bytes < 1024) return '$bytes B';
    final kb = bytes / 1024;
    if (kb < 1024) return '${kb.toStringAsFixed(2)} KB';
    final mb = kb / 1024;
    return '${mb.toStringAsFixed(2)} MB';
  }
}

// ============================================================
// UI Components
// ============================================================

class LogEntry {
  final String message;
  final bool isError;

  LogEntry(this.message, {this.isError = false});
}

class SectionHeader extends StatelessWidget {
  final String title;

  const SectionHeader({super.key, required this.title});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 16, bottom: 8),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            title,
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.bold,
              color: Theme.of(context).colorScheme.primary,
            ),
          ),
          const Divider(),
        ],
      ),
    );
  }
}

class ExampleButton extends StatelessWidget {
  final String label;
  final VoidCallback onPressed;

  const ExampleButton({
    super.key,
    required this.label,
    required this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: ElevatedButton(
        onPressed: onPressed,
        style: ElevatedButton.styleFrom(
          minimumSize: const Size(double.infinity, 44),
        ),
        child: Text(label),
      ),
    );
  }
}
0
likes
150
points
43
downloads

Documentation

API reference

Publisher

verified publisherrivium.co

Weekly Downloads

Official Flutter SDK for RiviumStorage - File storage and image transformation service

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, http, http_parser, mime

More

Packages that depend on rivium_storage