Cloud Storage For Images: A User Story

Alex Johnson
-
Cloud Storage For Images: A User Story

The Need for Cloud Storage: Preserving Memories and Freeing Up Space

As a user, the desire to store photos and videos in the cloud is a common one. It's about more than just convenience; it's about preserving precious memories and ensuring they are safe from loss. This user story outlines the implementation of cloud storage for images, addressing the limitations of local storage and the risks associated with it. The core of this story revolves around giving users the ability to back up their photos and videos, freeing up valuable space on their devices, and providing a secure and accessible archive of their memories. This involves not only uploading images to a cloud service but also ensuring that the process is efficient, reliable, and user-friendly.

The Current Challenges

Currently, images are primarily saved locally on a user's device. This presents several problems:

  • Limited Storage: Devices have finite storage capacity, which can quickly fill up with photos and videos.
  • Device Space Consumption: Locally stored images consume valuable device space, leading to slower performance and the need for frequent cleanups.
  • Risk of Data Loss: If the app is uninstalled or the device is lost, the images stored locally are at risk of being lost.

Implementing cloud storage resolves these issues, offering a more robust and user-friendly experience.

Proposed Solution and Benefits

By implementing cloud storage, users can:

  • Free Up Device Space: Offload photos and videos to the cloud, freeing up storage on their devices.
  • Ensure Data Safety: Back up their memories, protecting them from loss due to device failure or app uninstallation.
  • Access Memories from Anywhere: Access their photos and videos from any device with an internet connection.

Technical Implementation: Setting Up Cloud Storage

Acceptance Criteria

To ensure a successful implementation, several criteria must be met:

  • Firebase Storage Configuration: Firebase Storage should be correctly configured with appropriate buckets for storing images.
  • Image Upload: Implementation of image upload functionality to the cloud storage.
  • Thumbnail Generation: Generation of thumbnails to optimize loading times and storage efficiency.
  • Image Compression: Compression of images before upload to reduce storage size and improve upload speed.
  • Progress Indicators: A progress indicator to provide feedback during the upload process.
  • Upload Cancellation: Ability to cancel uploads in progress.
  • Automatic Retry: Automatic retry mechanism in case of upload failures.
  • Image Download with Local Caching: Efficient image download with local caching to improve loading times.
  • Cleanup of Old Local Files: A mechanism to clean up old local files to manage storage usage effectively.
  • Upload/Download Tests: Thorough tests for upload and download functionalities to ensure reliability.

Storage Structure

The structure for storing images in the cloud is as follows:

gs://crazytrip-bucket/
├── captures/
│   ├── {userId}/
│   │   ├── {captureId}\_full.jpg      # Full Image
│   │   └── {captureId}\_thumb.jpg     # Thumbnail
├── profiles/
│   └── {userId}/
│       └── avatar.jpg
└── temp/
    └── {uploadId}.jpg                 # Temporary Uploads

Code Implementation

The code snippets below illustrate the implementation details using Dart and Firebase. This includes the StorageService class, the UploadProgressWidget, and the CachedStorageImage widget.

Storage Service:

// storage_service.dart
import 'dart:io';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:image/image.dart' as img;
import 'package:path_provider/path_provider.dart';
import 'dart:typed_data';

class StorageService {
  final FirebaseStorage storage;

  StorageService(this.storage);

  Future<String> uploadCaptureImage({
    required String userId,
    required String captureId,
    required File imageFile,
    Function(double)? onProgress,
  }) async {
    try {
      // 1. Compress image
      final compressedFile = await _compressImage(imageFile);

      // 2. Upload full image
      final fullPath = 'captures/$userId/${captureId}\_full.jpg';
      final uploadTask = storage.ref(fullPath).putFile(compressedFile);

      // 3. Monitor progress
      uploadTask.snapshotEvents.listen((snapshot) {
        final progress = snapshot.bytesTransferred / snapshot.totalBytes;
        onProgress?.call(progress);
      });

      await uploadTask;

      // 4. Generate and upload thumbnail
      await _generateAndUploadThumbnail(
        userId: userId,
        captureId: captureId,
        imageFile: compressedFile,
      );

      // 5. Get download URL
      final downloadUrl = await storage.ref(fullPath).getDownloadURL();

      return downloadUrl;
    } on FirebaseException catch (e) {
      throw StorageException('Upload failed: ${e.message}');
    }
  }

  Future<File> _compressImage(File imageFile) async {
    final bytes = await imageFile.readAsBytes();
    final image = img.decodeImage(bytes)!;

    // Resize if too large
    img.Image resized = image;
    if (image.width > 1920 || image.height > 1920) {
      resized = img.copyResize(
        image,
        width: image.width > image.height ? 1920 : null,
        height: image.height > image.width ? 1920 : null,
      );
    }

    // Compress as JPEG with quality 85
    final compressed = img.encodeJpg(resized, quality: 85);

    // Save to temp file
    final tempDir = await getTemporaryDirectory();
    final tempFile = File('${tempDir.path}/compressed_${DateTime.now().millisecondsSinceEpoch}.jpg');
    await tempFile.writeAsBytes(compressed);

    return tempFile;
  }

  Future<void> _generateAndUploadThumbnail({
    required String userId,
    required String captureId,
    required File imageFile,
  }) async {
    final bytes = await imageFile.readAsBytes();
    final image = img.decodeImage(bytes)!;

    // Generate thumbnail (300x300)
    final thumbnail = img.copyResize(
      image,
      width: 300,
      height: 300,
      interpolation: img.Interpolation.average,
    );

    final thumbnailBytes = img.encodeJpg(thumbnail, quality: 80);

    // Upload thumbnail
    final thumbPath = 'captures/$userId/${captureId}\_thumb.jpg';
    await storage.ref(thumbPath).putData(Uint8List.fromList(thumbnailBytes));
  }

  Future<File> downloadImage({
    required String downloadUrl,
    required String localPath,
  }) async {
    try {
      // Check if already cached
      final cachedFile = File(localPath);
      if (await cachedFile.exists()) {
        return cachedFile;
      }

      // Download from Storage
      final ref = storage.refFromURL(downloadUrl);
      await ref.writeToFile(cachedFile);

      return cachedFile;
    } on FirebaseException catch (e) {
      throw StorageException('Download failed: ${e.message}');
    }
  }

  Future<void> deleteImage(String downloadUrl) async {
    try {
      final ref = storage.refFromURL(downloadUrl);
      await ref.delete();
    } on FirebaseException catch (e) {
      throw StorageException('Delete failed: ${e.message}');
    }
  }

  Future<void> cleanupOldLocalFiles() async {
    final appDir = await getApplicationDocumentsDirectory();
    final capturesDir = Directory('${appDir.path}/captures');

    if (!await capturesDir.exists()) return;

    final now = DateTime.now();
    final files = capturesDir.listSync();

    for (final file in files) {
      if (file is File) {
        final stat = await file.stat();
        final age = now.difference(stat.modified);

        // Delete files older than 30 days
        if (age.inDays > 30) {
          await file.delete();
        }
      }
    }
  }
}

This class handles uploading, downloading, and deleting images from Firebase Storage. It also includes methods for image compression, thumbnail generation, and local file cleanup. The uploadCaptureImage function compresses the image, uploads the full-size image, generates and uploads a thumbnail, and returns the download URL. The downloadImage function downloads images with local caching, and the deleteImage function deletes images from the cloud.

Upload Progress Widget:

import 'package:flutter/material.dart';

class UploadProgressWidget extends StatelessWidget {
  final double progress;
  final VoidCallback? onCancel;

  const UploadProgressWidget({
    required this.progress,
    this.onCancel,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Row(
              children: [
                Icon(Icons.cloud_upload),
                SizedBox(width: 12),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('Uploading image...'),
                      SizedBox(height: 8),
                      LinearProgressIndicator(value: progress),
                    ],
                  ),
                ),
                if (onCancel != null)
                  IconButton(
                    icon: Icon(Icons.close),
                    onPressed: onCancel,
                  ),
              ],
            ),
            SizedBox(height: 8),
            Text('${(progress * 100).toStringAsFixed(0)}%'),
          ],
        ),
      ),
    );
  }
}

The UploadProgressWidget displays a progress bar during the upload process, providing visual feedback to the user and including a cancel button if needed.

Cached Image Widget:

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CachedStorageImage extends StatelessWidget {
  final String downloadUrl;
  final BoxFit fit;

  const CachedStorageImage({
    required this.downloadUrl,
    this.fit = BoxFit.cover,
  });

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<File>(
      future: context.read<StorageService>().downloadImage(
        downloadUrl: downloadUrl,
        localPath: _getLocalPath(downloadUrl),
      ),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return Center(child: CircularProgressIndicator());
        }

        if (snapshot.hasError) {
          return Center(child: Icon(Icons.error));
        }

        return Image.file(
          snapshot.data!,
          fit: fit,
        );
      },
    );
  }

  String _getLocalPath(String url) {
    final hash = url.hashCode.toString();
    return '/path/to/cache/$hash.jpg';
  }
}

The CachedStorageImage widget handles the download and display of images from cloud storage. It caches the images locally to improve performance, and it uses a FutureBuilder to display a loading indicator while the image is being downloaded.

Impact and Estimation

Impact: 🟠 HIGH - This feature has a high impact by providing reliable media backups and freeing up device space.

Estimation: 5 story points

Conclusion

Implementing cloud storage for images is a critical step in modernizing the user experience. By following the steps outlined in this user story, developers can create a seamless and efficient system for managing and protecting user-generated content. From initial setup to local file cleanup, each component plays a crucial role in delivering a robust and user-friendly feature. This approach not only addresses current challenges but also sets the stage for future enhancements and improvements in the application.

For more in-depth information and best practices, consider exploring the official documentation on Firebase Storage. Firebase Storage Documentation

You may also like