Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement resumable upload for google drive #117

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

cp-pratik-k
Copy link
Collaborator

@cp-pratik-k cp-pratik-k commented Jan 13, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Added ability to pause and resume uploads.
    • Introduced upload session management for more granular file uploads.
    • Enhanced upload process control with new status tracking.
    • Added support for resumable uploads in Google Drive and Dropbox.
  • Improvements

    • Refined upload and download process error handling.
    • Updated content type management across services.
    • Improved upload progress tracking and notifications.
    • Enhanced user interface for managing upload states.
  • Bug Fixes

    • Adjusted media process status management.
    • Updated unique ID generation for uploads and downloads.

These updates provide more robust and flexible media transfer capabilities, giving users greater control over their upload and download processes.

Copy link

coderabbitai bot commented Jan 13, 2025

Warning

Rate limit exceeded

@cp-pratik-k has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 21 minutes and 56 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between d22e8cb and 5f17beb.

📒 Files selected for processing (2)
  • .github/workflows/ios_deploy.yml (1 hunks)
  • app/lib/ui/flow/media_transfer/components/transfer_item.dart (4 hunks)

Walkthrough

This pull request introduces enhancements to the media upload functionality, focusing on adding pause and resume capabilities for upload processes. New callback functions are implemented to manage upload states, and the user interface is refined to provide clearer controls for users. Additionally, the changes include the introduction of upload session management for cloud services like Google Drive and Dropbox, along with updates to data models and repositories to support these features.

Changes

File Change Summary
app/lib/ui/flow/media_transfer/components/transfer_item.dart Added onPausedTap and onResumeTap callbacks; Updated UI to show pause/resume buttons based on upload state.
app/lib/ui/flow/media_transfer/media_transfer_screen.dart Implemented new callback handlers for resuming and pausing uploads.
app/lib/ui/flow/media_transfer/media_transfer_view_model.dart Added onPauseUploadProcess and onResumeUploadProcess methods.
data/lib/models/media_process/media_process.dart Removed deleting status, added paused status; Added upload_session_id to UploadMediaProcess.
data/lib/services/dropbox_services.dart Added startUploadSession, appendUploadSession, and finishUploadSession methods for chunked uploads.
data/lib/services/google_drive_service.dart Added startUploadSession and appendUploadSession methods for resumable uploads.
data/lib/repositories/media_process_repository.dart Added methods for managing upload session IDs and pausing/resuming uploads.
data/lib/apis/dropbox/dropbox_content_endpoints.dart Introduced new classes for Dropbox upload session management and updated existing methods.
data/lib/apis/google_drive/google_drive_endpoint.dart Added new classes for Google Drive upload session management and updated existing methods.
data/lib/domain/config.dart Added ApiConfigs class with configuration constants for upload handling.
data/lib/models/media_content/media_content.dart Updated AppMediaContent class to replace contentType with range and type.
data/lib/models/media_process/media_process.freezed.dart Updated AppMediaContent mixin to reflect changes in properties.
data/lib/models/media_process/media_process.g.dart Updated serialization logic to include new status and session ID.

Possibly related PRs

  • Improve auto backup flow #60: The changes in the main PR regarding the UploadProcessItem class and its new callback functions for pausing and resuming uploads relate to the modifications in the HomeViewStateNotifier class, which also deals with media management and state updates.
  • Remove ad id usage #72: The main PR's enhancements to upload management and user interface controls for upload states connect with the changes in the workflow for deploying the application, which may involve similar media management functionalities.
  • Account Ui Improvement #76: The UI improvements in the main PR that enhance user interaction with upload processes are relevant to the changes made in the AccountsScreen, which also focuses on user interface enhancements.

Poem

🐰 In the cloud, our uploads play,
Pause and resume, hip-hip-hooray!
A session started, chunks in flight,
With every byte, our dreams take flight.
Control the flow, let worries cease,
Upload magic, a joyful peace! 🚀


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🔭 Outside diff range comments (1)
data/lib/repositories/media_process_repository.dart (1)

Line range hint 721-723: Fix incorrect method call for updating upload process status

In the catch block of _uploadInDropbox, the method updateDownloadProcessStatus is incorrectly called instead of updateUploadProcessStatus. This needs to be corrected to update the upload queue appropriately upon failure.

Apply this diff to correct the method call:

           await updateDownloadProcessStatus(
+          await updateUploadProcessStatus(
             status: MediaQueueProcessStatus.failed,
             id: process.id,
           );
🧹 Nitpick comments (6)
data/lib/models/media_process/media_process.freezed.dart (1)

731-731: Consider adding documentation for the upload_session_id field.

While the toString implementation is correct, it would be helpful to add documentation explaining the purpose and lifecycle of the upload_session_id field in the original class definition.

data/lib/repositories/media_process_repository.dart (1)

454-468: Prevent multiple resumptions of the same upload process

In resumeUploadProcess, ensure that an upload process is not started multiple times if resumeUploadProcess is called repeatedly.

Add a check to see if the process is already running before initiating the upload.

if (process.status.isUploading) {
  // The upload is already in progress.
  return;
}
data/lib/domain/config.dart (1)

22-28: LGTM! The configuration values are well-chosen.

  • uploadRequestByteSize of 256KB (262144 bytes) is a good balance between network efficiency and memory usage for resumable uploads.
  • processProgressUpdateDuration of 300ms provides smooth progress updates without overwhelming the UI.

Consider making these values configurable through environment variables or build configuration for different environments (development, production) if needed.

app/lib/ui/flow/media_transfer/media_transfer_screen.dart (1)

125-130: Add error handling for pause/resume actions

While the implementation of pause/resume callbacks is correct, consider adding error handling to gracefully handle failures during these operations.

 onResumeTap: () {
+  try {
     notifier.onResumeUploadProcess(uploadProcesses[index].id);
+  } catch (e) {
+    context.showErrorSnackBar(message: 'Failed to resume upload: $e');
+  }
 },
 onPausedTap: () {
+  try {
     notifier.onPauseUploadProcess(uploadProcesses[index].id);
+  } catch (e) {
+    context.showErrorSnackBar(message: 'Failed to pause upload: $e');
+  }
 },
data/lib/apis/google_drive/google_drive_endpoint.dart (1)

141-144: Add validation for required headers

Consider adding validation to ensure that required headers (Content-Type, Content-Length, Content-Range) are not null or empty before making the request.

 @override
 Map<String, dynamic> get headers => {
+    if (content.type.isEmpty) throw ArgumentError('Content-Type cannot be empty'),
+    if (content.length == null) throw ArgumentError('Content-Length is required'),
+    if (content.range == null) throw ArgumentError('Content-Range is required for resumable uploads'),
     'Content-Type': content.type,
     'Content-Length': content.length.toString(),
     'Content-Range': content.range,
   };
app/lib/ui/flow/media_transfer/components/transfer_item.dart (1)

141-142: Consider using localization for the pause message.

Other status messages in this method use localization (context.l10n), but the pause message is hardcoded. Consider using localization for consistency.

-      return "Upload paused";
+      return context.l10n.upload_status_paused;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6fd0c09 and 4e5551f.

📒 Files selected for processing (15)
  • app/lib/ui/flow/media_transfer/components/transfer_item.dart (4 hunks)
  • app/lib/ui/flow/media_transfer/media_transfer_screen.dart (1 hunks)
  • app/lib/ui/flow/media_transfer/media_transfer_view_model.dart (1 hunks)
  • data/.flutter-plugins-dependencies (1 hunks)
  • data/lib/apis/dropbox/dropbox_content_endpoints.dart (1 hunks)
  • data/lib/apis/google_drive/google_drive_endpoint.dart (3 hunks)
  • data/lib/domain/config.dart (1 hunks)
  • data/lib/models/media_content/media_content.dart (1 hunks)
  • data/lib/models/media_content/media_content.freezed.dart (10 hunks)
  • data/lib/models/media_process/media_process.dart (3 hunks)
  • data/lib/models/media_process/media_process.freezed.dart (14 hunks)
  • data/lib/models/media_process/media_process.g.dart (3 hunks)
  • data/lib/repositories/media_process_repository.dart (12 hunks)
  • data/lib/services/dropbox_services.dart (5 hunks)
  • data/lib/services/google_drive_service.dart (6 hunks)
✅ Files skipped from review due to trivial changes (1)
  • data/.flutter-plugins-dependencies
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (21)
data/lib/models/media_process/media_process.freezed.dart (2)

417-417: LGTM! The upload_session_id field is properly integrated.

The new optional String? field is correctly implemented across all necessary locations in the Freezed-generated code, including:

  • Field declarations
  • Constructor parameters
  • copyWith method
  • toString, equality, and hashCode implementations

Also applies to: 450-450, 509-512, 682-682, 705-706, 731-731, 746-747, 771-771, 804-804, 827-828


417-417: Verify session ID persistence across app restarts.

Since upload_session_id is used for resumable uploads, ensure that it's properly persisted and can survive app restarts to maintain upload progress.

Also applies to: 705-706

✅ Verification successful

Upload session ID is properly persisted for resumable uploads

The implementation uses SQLite database storage which ensures upload_session_id persists across app restarts. The session ID is stored in a dedicated column in the upload_queue_table and is properly managed through the updateUploadSessionId method. This allows for reliable resumption of uploads from their last position.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if upload_session_id is being persisted in any storage implementations
rg -A 5 "upload_session_id.*persist|persist.*upload_session_id"

# Check if there are any database schema or migration files
fd -e sql -e dart ".*schema.*|.*migration.*"

Length of output: 112


Script:

#!/bin/bash
# Find the original MediaProcess class file
fd -e dart "media_process" --exclude "*.freezed.dart" --exclude "*.g.dart"

# Search for all occurrences of upload_session_id
rg -l "upload_session_id"

# Look for storage/persistence related files
fd -e dart "storage|repository|dao|database"

Length of output: 509


Script:

#!/bin/bash
# Check the repository implementation
cat data/lib/repositories/media_process_repository.dart

# Look for any persistence-related code in the model
cat data/lib/models/media_process/media_process.dart

Length of output: 35603

data/lib/apis/dropbox/dropbox_content_endpoints.dart (3)

164-199: Well-structured implementation of DropboxStartUploadEndpoint

The DropboxStartUploadEndpoint class is well-implemented, properly setting up the initial upload session with correct headers and data handling.


200-245: Effective implementation of DropboxAppendUploadEndpoint

The DropboxAppendUploadEndpoint class efficiently handles appending data to an ongoing upload session, with appropriate management of headers and content.


246-312: Comprehensive implementation of DropboxFinishUploadEndpoint

Apart from the minor issue with the endpoint path, the DropboxFinishUploadEndpoint class is thoroughly implemented, correctly finalizing the upload session with proper handling of commit details and cursor information.

data/lib/services/google_drive_service.dart (1)

220-220: Correct property name for content type

Ensure consistency in property naming. The AppMediaContent class uses type as the property, but if any part of the code still references contentType, it should be updated to type to prevent errors.

Run the following script to verify all usages of contentType:

✅ Verification successful

Property naming is correct and consistent

The codebase maintains proper separation between HTTP-standard contentType in the network layer and type in application models. The mapping between these conventions is handled correctly in media_content.dart. No changes needed.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Find all references to 'contentType' in the codebase.

# Search for 'contentType' in Dart files
rg --glob '*.dart' 'contentType'

Length of output: 702

data/lib/models/media_content/media_content.dart (2)

11-12: LGTM! The changes support resumable uploads.

The addition of the range property and renaming of contentType to type align well with implementing resumable uploads. The range property will be useful for specifying byte ranges during resumable uploads.


18-18: Verify type compatibility with Google Drive API.

The factory method correctly maps Google Drive's contentType to the new type property.

Let's verify the type compatibility:

✅ Verification successful

Type compatibility verified successfully

The contentType from Google Drive API is correctly mapped to the type property, as both represent MIME types and are used consistently in Content-Type headers across the API implementations.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any other usages of media.contentType to ensure consistency
rg "media\.contentType" --type dart

Length of output: 119


Script:

#!/bin/bash
# Find MediaContent class definition and type property
ast-grep --pattern 'class MediaContent {
  $$$
}'

# Search for type-related definitions
rg "(?i)content.*type" --type dart -A 3

# Look for Google Drive API related files
fd "drive.*\.dart$"

Length of output: 4871

data/lib/models/media_process/media_process.dart (3)

20-20: LGTM! The paused status supports resumable uploads.

The addition of the paused status is essential for implementing resumable uploads.


37-37: LGTM! The isPaused getter is well-implemented.

The getter provides a clean way to check for paused status.


125-125: Verify session ID persistence.

The upload_session_id is crucial for resumable uploads. Ensure it's properly persisted and recovered after app restarts.

Let's check the persistence implementation:

✅ Verification successful

Upload session ID persistence implementation verified.

The upload_session_id is properly persisted in the SQLite database and recovered through JSON serialization, ensuring it survives app restarts for resumable uploads.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for upload_session_id usage in database operations
rg "upload_session_id" --type dart -A 5

# Search for session-related operations
ast-grep --pattern 'class $_ {
  $$$
  upload_session_id
  $$$
}'

Length of output: 12168

data/lib/models/media_process/media_process.g.dart (2)

56-56: LGTM: Status enum updated for pause functionality

The addition of MediaQueueProcessStatus.paused status is correctly implemented and aligns with the resumable upload feature.


67-67: LGTM: Upload session tracking implemented

The upload_session_id field is properly integrated into both JSON serialization and deserialization methods, enabling proper tracking of resumable upload sessions.

Also applies to: 91-91

data/lib/models/media_content/media_content.freezed.dart (1)

21-22: LGTM: Media content model enhanced for resumable uploads

The model has been correctly updated to support resumable uploads:

  • Replaced contentType with a more specific type field
  • Added optional range field for tracking upload progress
  • All related methods are properly updated

Also applies to: 147-149

data/lib/apis/google_drive/google_drive_endpoint.dart (2)

87-115: LGTM: Start upload endpoint implementation

The GoogleDriveStartUploadEndpoint correctly implements the resumable upload initiation as per Google Drive API specifications.


117-160: Verify Content-Range header format

The GoogleDriveAppendUploadEndpoint implementation looks good, but ensure that the Content-Range header follows the format specified by Google Drive API:

  • For first chunk: bytes 0-524287/2000000
  • For final chunk: bytes 524288-2000000/2000000
✅ Verification successful

Content-Range header format is correctly implemented

The Content-Range header follows the required Google Drive API format. The implementation in google_drive_service.dart correctly generates the range as bytes start-end/total with proper zero-based byte ranges.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if Content-Range header format matches Google Drive API specs
ast-grep --pattern 'content.range = "bytes $_-$_/$_"'

Length of output: 55


Script:

#!/bin/bash
# Find AppMediaContent class definition and implementation
ast-grep --pattern 'class AppMediaContent {
  $$$
}'

# Also search for any mentions of range property or method
rg -A 5 'range' --type dart

Length of output: 9948

app/lib/ui/flow/media_transfer/components/transfer_item.dart (3)

14-15: LGTM! Well-structured implementation of pause/resume callbacks.

The addition of onPausedTap and onResumeTap callbacks provides the necessary control for implementing resumable uploads.

Also applies to: 22-23


93-101: LGTM! Clean implementation of the pause button.

The pause button is appropriately shown only when the upload is running, using platform-specific icons.


84-92: LGTM! Well-designed state-specific resume buttons.

The implementation thoughtfully uses different icons based on the upload state:

  • Play icon for paused uploads
  • Refresh icon for terminated/failed uploads
    This provides clear visual feedback to users about the available actions.

Also applies to: 113-121

data/lib/services/dropbox_services.dart (2)

295-295: LGTM! Consistent renaming of content type property.

The change from contentType to type is applied consistently across all AppMediaContent instantiations.

Also applies to: 348-349, 389-390, 409-410, 539-539, 564-564, 598-598


328-361: LGTM! Robust implementation of upload session initialization.

The implementation includes:

  • File existence validation
  • Proper error handling
  • Clear session ID return for subsequent operations

HttpMethod get method => HttpMethod.post;

@override
String get path => 'files/upload_session/finish';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix missing leading slash in the endpoint path

In DropboxFinishUploadEndpoint, the path property is missing a leading slash. This could lead to incorrect API endpoint resolution. It should be '/files/upload_session/finish' to ensure the correct endpoint is called.

Apply this diff to correct the path:

-      String get path => 'files/upload_session/finish';
+      String get path => '/files/upload_session/finish';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String get path => 'files/upload_session/finish';
String get path => '/files/upload_session/finish';

Comment on lines +257 to +258
return Uri.parse(res.headers.value('location')!)
.queryParameters['upload_id']!;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add null safety checks when parsing headers

When accessing the 'Location' header and parsing the URI, there is a possibility of encountering null values, which can lead to runtime exceptions. Implement null checks to handle cases where the header might be missing or improperly formatted.

Apply this diff to enhance null safety:

-          return Uri.parse(res.headers.value('location')!)
-              .queryParameters['upload_id']!;
+          final locationHeader = res.headers.value('location');
+          if (locationHeader == null) {
+            throw SomethingWentWrongError(
+              message: 'Missing Location header in response',
+              statusCode: res.statusCode,
+            );
+          }
+          final uploadId = Uri.parse(locationHeader).queryParameters['upload_id'];
+          if (uploadId == null) {
+            throw SomethingWentWrongError(
+              message: 'Missing upload_id in Location header',
+              statusCode: res.statusCode,
+            );
+          }
+          return uploadId;

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +254 to +258
await _client.req(GoogleDriveStartUploadEndpoint(request: file));

if (res.statusCode == 200) {
return Uri.parse(res.headers.value('location')!)
.queryParameters['upload_id']!;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle the upload session URI correctly

In the startUploadSession method, the 'Location' header contains the upload session URI required for subsequent upload requests. Instead of attempting to extract an upload_id from the query parameters, the full session URI should be stored and used.

Apply this diff to correct the code:

         if (res.statusCode == 200) {
-          return Uri.parse(res.headers.value('location')!)
-              .queryParameters['upload_id']!;
+          final sessionUri = res.headers.value('location');
+          if (sessionUri == null) {
+            throw SomethingWentWrongError(
+              message: 'Missing Location header in response',
+              statusCode: res.statusCode,
+            );
+          }
+          return sessionUri;
         }

Also, update the appendUploadSession method to accept uploadSessionUri instead of uploadId, and use it directly to upload the chunks.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 553 to 574
while (chunk < total) {
process =
_uploadQueue.firstWhere((element) => element.id == process.id);

if (process.status.isTerminated) {
showNotification('Upload to Google Drive Cancelled');
break;
} else if (process.status.isPaused) {
showNotification('Upload to Google Drive Paused');
break;
}

final endBytes = (chunk + ApiConfigs.uploadRequestByteSize) > total
? total
: chunk + ApiConfigs.uploadRequestByteSize;

final res = await _googleDriveService.appendUploadSession(
startByte: chunk,
endByte: endBytes,
uploadId: uploadSessionId,
path: process.path,
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Implement cancellation support during upload

In the _uploadInGoogleDrive method, if the upload process is paused or terminated, the ongoing network request to appendUploadSession should be canceled to prevent unnecessary data transfer and potential data corruption.

Consider passing a CancelToken to appendUploadSession and invoking cancelToken.cancel() when the process status is paused or terminated.

Apply this diff to pass the CancelToken:

           final res = await _googleDriveService.appendUploadSession(
             startByte: chunk,
             endByte: endBytes,
             uploadId: uploadSessionId,
             path: process.path,
+            cancelToken: cancelToken,
           );

Ensure you create a CancelToken before the loop and cancel it when needed:

+          final cancelToken = CancelToken();

And modify the condition to cancel the request:

           if (process.status.isTerminated || process.status.isPaused) {
+            cancelToken.cancel();
             showNotification('Upload to Google Drive Cancelled');
             break;
           }

Comment on lines 441 to 452
Future<void> pauseUploadProcess(String id) async {
await database.rawUpdate(
"UPDATE ${LocalDatabaseConstants.uploadQueueTable} SET status = ? WHERE id = ? AND (status = ? OR status = ?)",
[
MediaQueueProcessStatus.paused.value,
id,
MediaQueueProcessStatus.uploading.value,
MediaQueueProcessStatus.waiting.value,
],
);
await updateQueue(database);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ensure pausing the upload process cancels ongoing requests

The pauseUploadProcess method updates the status in the database but does not cancel any ongoing network requests. This might lead to the upload continuing despite being marked as paused.

Consider implementing cancellation logic by tracking CancelTokens for each upload process and invoking cancelToken.cancel() when pausing.

Comment on lines 609 to 611
showNotification('Failed to upload to Google Drive');

await updateDownloadProcessStatus(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect method call for updating upload process status

In the catch block of _uploadInGoogleDrive, the method updateDownloadProcessStatus is incorrectly called instead of updateUploadProcessStatus. This would result in incorrect status updates for the upload queue.

Apply this diff to correct the method call:

           showNotification('Failed to upload to Google Drive');

-          await updateDownloadProcessStatus(
+          await updateUploadProcessStatus(
             status: MediaQueueProcessStatus.failed,
             id: process.id,
           );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
showNotification('Failed to upload to Google Drive');
await updateDownloadProcessStatus(
showNotification('Failed to upload to Google Drive');
await updateUploadProcessStatus(

Comment on lines +41 to +47
void onPauseUploadProcess(String id) {
_mediaProcessRepo.pauseUploadProcess(id);
}

void onResumeUploadProcess(String id) {
_mediaProcessRepo.resumeUploadProcess(id);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for pause/resume operations.

While the implementation is correct, it lacks error handling. Consider catching and handling potential errors that might occur during pause/resume operations.

Here's a suggested implementation:

   void onPauseUploadProcess(String id) {
-    _mediaProcessRepo.pauseUploadProcess(id);
+    try {
+      _mediaProcessRepo.pauseUploadProcess(id);
+    } catch (e) {
+      state = state.copyWith(error: e);
+    }
   }

   void onResumeUploadProcess(String id) {
-    _mediaProcessRepo.resumeUploadProcess(id);
+    try {
+      _mediaProcessRepo.resumeUploadProcess(id);
+    } catch (e) {
+      state = state.copyWith(error: e);
+    }
   }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 363 to 428
Future<AppMedia> appendUploadSession({
required String path,
required int offset,
required String sessionId,
required String localRefId,
CancelToken? cancelToken,
void Function(int sent, int total)? onProgress,
}) async {
final localFile = File(path);

if (localFile.existsSync() == false) {
throw SomethingWentWrongError(
statusCode: 404,
message: 'File not found',
);
}

final res = await _dropboxAuthenticatedDio.req(
DropboxAppendUploadEndpoint(
offset: offset,
sessionId: sessionId,
cancellationToken: cancelToken,
onProgress: onProgress,
content: AppMediaContent(
stream: localFile.openRead(),
length: localFile.lengthSync(),
type: 'application/octet-stream',
),
),
);

if (res.statusCode == 200) {
final res = await _dropboxAuthenticatedDio.req(
DropboxFinishUploadEndpoint(
filePath:
"/${ProviderConstants.backupFolderName}/${localFile.path.split('/').last}",
appPropertyTemplateId:
_dropboxFileIdAppPropertyTemplateIdController.state!,
localRefId: localRefId,
offset: offset,
sessionId: sessionId,
cancellationToken: cancelToken,
onProgress: onProgress,
content: AppMediaContent(
stream: localFile.openRead(),
length: localFile.lengthSync(),
type: 'application/octet-stream',
),
),
);

if (res.statusCode == 200) {
return AppMedia.fromDropboxJson(json: res.data);
}

throw SomethingWentWrongError(
statusCode: res.statusCode,
message: res.statusMessage,
);
}

throw SomethingWentWrongError(
statusCode: res.statusCode,
message: res.statusMessage,
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider splitting append and finish operations.

The method currently handles both append and finish operations, which violates the Single Responsibility Principle. Consider splitting into separate methods for better clarity and maintainability:

  • appendUploadSession: Only handles appending data
  • finishUploadSession: Handles completing the upload

Example refactor:

 Future<void> appendUploadSession({
   required String path,
   required int offset,
   required String sessionId,
-  required String localRefId,
   CancelToken? cancelToken,
   void Function(int sent, int total)? onProgress,
 }) async {
   // ... file check ...
   final res = await _dropboxAuthenticatedDio.req(
     DropboxAppendUploadEndpoint(/* ... */)
   );
   if (res.statusCode != 200) {
     throw SomethingWentWrongError(/* ... */);
   }
 }

+Future<AppMedia> finishUploadSession({
+  required String path,
+  required int offset,
+  required String sessionId,
+  required String localRefId,
+  CancelToken? cancelToken,
+  void Function(int sent, int total)? onProgress,
+}) async {
+  // ... finish upload logic ...
+}

Committable suggestion skipped: line range outside the PR's diff.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
data/lib/services/google_drive_service.dart (1)

256-258: ⚠️ Potential issue

Add null safety checks when parsing headers

The code is vulnerable to null pointer exceptions when accessing the 'location' header and parsing the URI.

Apply this diff to enhance null safety:

-      return Uri.parse(res.headers.value('location')!)
-          .queryParameters['upload_id']!;
+      final locationHeader = res.headers.value('location');
+      if (locationHeader == null) {
+        throw SomethingWentWrongError(
+          message: 'Missing Location header in response',
+          statusCode: res.statusCode,
+        );
+      }
+      final uploadId = Uri.parse(locationHeader).queryParameters['upload_id'];
+      if (uploadId == null) {
+        throw SomethingWentWrongError(
+          message: 'Missing upload_id in Location header',
+          statusCode: res.statusCode,
+        );
+      }
+      return uploadId;
data/lib/apis/dropbox/dropbox_content_endpoints.dart (1)

281-281: ⚠️ Potential issue

Fix missing leading slash in the endpoint path

The path property is missing a leading slash. This could lead to incorrect API endpoint resolution.

Apply this diff to correct the path:

-  String get path => '/files/upload_session/finish';
+  String get path => '/files/upload_session/finish';
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e5551f and f6fc9ac.

📒 Files selected for processing (4)
  • data/lib/apis/dropbox/dropbox_content_endpoints.dart (2 hunks)
  • data/lib/repositories/media_process_repository.dart (21 hunks)
  • data/lib/services/dropbox_services.dart (5 hunks)
  • data/lib/services/google_drive_service.dart (7 hunks)
🧰 Additional context used
🪛 GitHub Actions: Analyze
data/lib/repositories/media_process_repository.dart

[info] 705-705: Missing a required trailing comma. Try adding a trailing comma.

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (13)
data/lib/services/google_drive_service.dart (3)

95-95: LGTM! Good separation of concerns

The query modification correctly excludes album files from media listing, maintaining a clean separation between media and album management.


220-220: LGTM! Consistent parameter naming

The parameter rename from contentType to type is applied consistently across all methods, improving code consistency.

Also applies to: 459-459, 485-485, 562-562, 636-636


267-315: LGTM! Robust implementation of appendUploadSession

The implementation includes:

  • Proper file existence validation
  • Correct byte range handling
  • Comprehensive error handling including resume status (308)
data/lib/apis/dropbox/dropbox_content_endpoints.dart (3)

150-150: LGTM! Property name change from contentType to type

The change aligns with the updated AppMediaContent model.


164-197: LGTM! Well-structured upload session initialization endpoint

The implementation follows the endpoint pattern consistently and includes all necessary configurations for starting an upload session.


199-242: LGTM! Well-implemented append operation endpoint

The implementation correctly handles session continuation with proper offset tracking and content streaming.

data/lib/services/dropbox_services.dart (4)

295-295: LGTM! Consistent property name updates

The changes from contentType to type are applied consistently across all occurrences.

Also applies to: 561-561, 586-586, 620-620


328-364: LGTM! Well-implemented upload session initialization

The method includes proper error handling and file existence validation.


366-403: LGTM! Well-structured append operation

The method follows consistent error handling patterns and properly manages upload session continuation.


405-450: LGTM! Well-implemented session finalization

The method properly handles session completion with appropriate error handling.

data/lib/repositories/media_process_repository.dart (3)

15-15: LGTM! Consistent integration of UniqueIdGenerator

The UniqueIdGenerator is properly injected and used consistently across the repository.

Also applies to: 31-31, 81-81, 246-246, 298-298, 388-388


141-141: LGTM! Database schema update for session management

The upload_session_id column is properly added to support resumable uploads.


463-490: LGTM! Well-implemented pause/resume functionality

The implementation properly handles upload process state transitions between paused and resumed states.

// If the upload session id is null and the chunk is not equal to 0 then start the upload session from the beginning
if (chunk != 0) {
await updateUploadProcessProgress(
id: process.id, chunk: 0, total: total);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing trailing comma

A trailing comma is required for consistent formatting.

Apply this diff to add the trailing comma:

-                id: process.id, chunk: 0, total: total);
+                id: process.id, chunk: 0, total: total,);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
id: process.id, chunk: 0, total: total);
id: process.id, chunk: 0, total: total,);
🧰 Tools
🪛 GitHub Actions: Analyze

[info] 705-705: Missing a required trailing comma. Try adding a trailing comma.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
data/lib/repositories/media_process_repository.dart (3)

463-473: 🛠️ Refactor suggestion

Implement request cancellation in pauseUploadProcess.

The pauseUploadProcess method only updates the status but doesn't cancel ongoing upload requests. This could lead to unnecessary network usage.


626-628: ⚠️ Potential issue

Fix incorrect method call in error handling.

The error handling block incorrectly calls updateDownloadProcessStatus instead of updateUploadProcessStatus.


705-705: ⚠️ Potential issue

Add missing trailing comma.

A trailing comma is required for consistent formatting.

🧰 Tools
🪛 GitHub Actions: Analyze

[info] 705-705: Missing a required trailing comma. Try adding a trailing comma.

🧹 Nitpick comments (3)
data/lib/repositories/media_process_repository.dart (3)

15-15: LGTM! Consider adding documentation for the new dependency.

The addition of UniqueIdGenerator as a dependency is a good practice for better testability and maintainability.

Consider adding a documentation comment explaining the purpose of _uniqueIdGenerator:

/// Generator for creating unique identifiers used in media processes
final UniqueIdGenerator _uniqueIdGenerator;

Also applies to: 81-81, 109-109


141-141: Consider adding an index for the upload_session_id column.

Since upload_session_id will be frequently queried during upload operations, adding an index could improve query performance.

CREATE INDEX idx_upload_session_id ON ${LocalDatabaseConstants.uploadQueueTable} (upload_session_id);

571-624: Extract common upload functionality into a shared method.

The upload implementations for Google Drive and Dropbox share similar logic for chunking, progress tracking, and notifications. Consider extracting these into a shared method to reduce code duplication.

Future<void> _handleChunkedUpload({
  required UploadMediaProcess process,
  required Future<void> Function(int startByte, int endByte) uploadChunk,
  required Future<void> Function(String message) showNotification,
  required AppMedia? response,
}) async {
  int chunk = process.chunk;
  final total = await File(process.path).length();

  while (chunk < total) {
    if (process.status.isTerminated || process.status.isPaused) {
      showNotification(process.status.isTerminated ? 'Upload Cancelled' : 'Upload Paused');
      break;
    }

    final end = (chunk + ApiConfigs.uploadRequestByteSize).clamp(0, total);
    await uploadChunk(chunk, end);
    chunk = end;

    await updateUploadProcessProgress(
      id: process.id,
      chunk: chunk,
      total: total,
    );

    await showNotification(
      '${chunk.formatBytes} / ${total.formatBytes} - ${(chunk / total * 100).round()}%',
    );
  }

  if (chunk >= total) {
    await updateUploadProcessStatus(
      status: MediaQueueProcessStatus.completed,
      response: response,
      id: process.id,
    );
    await showNotification('Upload completed successfully');
    await clearUploadProcessResponse(id: process.id);
  }
}

Also applies to: 664-749

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6fc9ac and f54b014.

📒 Files selected for processing (1)
  • data/lib/repositories/media_process_repository.dart (21 hunks)
🧰 Additional context used
🪛 GitHub Actions: Analyze
data/lib/repositories/media_process_repository.dart

[info] 705-705: Missing a required trailing comma. Try adding a trailing comma.

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
data/lib/repositories/media_process_repository.dart (2)

111-111: Consider adding error handling for database initialization.

The database initialization is called directly in the constructor without any error handling. If the initialization fails, it could leave the repository in an invalid state.

Consider wrapping the initialization in a try-catch block and handling potential database errors:

-    _initializeLocalDatabase();
+    try {
+      await _initializeLocalDatabase();
+    } catch (e) {
+      debugPrint('Failed to initialize database: $e');
+      // Consider throwing a custom exception or handling the error appropriately
+    }

557-624: Add cleanup for abandoned upload sessions.

When an upload is terminated or fails, the upload session is not cleaned up on Google Drive's side. This could lead to orphaned upload sessions.

Consider adding cleanup logic:

   if (process.status.isTerminated) {
+    try {
+      await _googleDriveService.deleteUploadSession(uploadSessionId);
+    } catch (e) {
+      debugPrint('Failed to cleanup upload session: $e');
+    }
     showNotification('Upload to Google Drive Cancelled');
     break;
   }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f54b014 and 7b44127.

📒 Files selected for processing (1)
  • data/lib/repositories/media_process_repository.dart (21 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (2)
data/lib/repositories/media_process_repository.dart (2)

591-596: Implement cancellation support during upload.

The appendUploadSession call should support cancellation to prevent unnecessary data transfer when the upload is paused or terminated.


Line range hint 124-164: Consider implementing database migration strategy.

The database schema has been updated with a new column upload_session_id, but there's no migration strategy for existing installations. This could cause issues for users upgrading from a previous version.

Consider implementing a version-based migration strategy. Also, verify if there are any existing installations that need migration:

Comment on lines 702 to 711
// If the upload session id is null and the chunk is not equal to 0 then start the upload session from the beginning
if (chunk != 0) {
await updateUploadProcessProgress(
id: process.id,
chunk: chunk,
chunk: 0,
total: total,
);
process = _uploadQueue.firstWhere((e) => e.id == process.id);
_uploadInDropbox(process);
return;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix potential infinite loop in Dropbox upload session restart.

The current implementation could lead to an infinite loop if the upload session fails to start repeatedly. There's no maximum retry limit or backoff strategy.

Consider adding a retry limit and exponential backoff:

   if (chunk != 0) {
+    static const maxRetries = 3;
+    int retryCount = 0;
+    while (retryCount < maxRetries) {
       await updateUploadProcessProgress(
         id: process.id,
         chunk: 0,
         total: total,
       );
       process = _uploadQueue.firstWhere((e) => e.id == process.id);
-      _uploadInDropbox(process);
-      return;
+      try {
+        await _uploadInDropbox(process);
+        return;
+      } catch (e) {
+        retryCount++;
+        if (retryCount >= maxRetries) throw e;
+        await Future.delayed(Duration(seconds: math.pow(2, retryCount).toInt()));
+      }
+    }
   }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +476 to +490
Future<void> resumeUploadProcess(String id) async {
final process = _uploadQueue.firstWhere(
(element) => element.id == id,
);
if (process.provider == MediaProvider.googleDrive) {
_uploadInGoogleDrive(process);
} else if (process.provider == MediaProvider.dropbox) {
_uploadInDropbox(process);
} else {
updateUploadProcessStatus(
status: MediaQueueProcessStatus.failed,
id: process.id,
);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Handle potential race condition in resumeUploadProcess.

The method retrieves the process and then starts the upload without checking if the status is still valid. Another operation could change the status between these steps.

Consider adding a status check and using a transaction:

   Future<void> resumeUploadProcess(String id) async {
+    bool updated = false;
+    await database.transaction((txn) async {
+      final result = await txn.rawUpdate(
+        "UPDATE ${LocalDatabaseConstants.uploadQueueTable} SET status = ? WHERE id = ? AND status = ?",
+        [MediaQueueProcessStatus.waiting.value, id, MediaQueueProcessStatus.paused.value],
+      );
+      updated = result > 0;
+    });
+
+    if (!updated) return;
+
     final process = _uploadQueue.firstWhere(
       (element) => element.id == id,
     );

Committable suggestion skipped: line range outside the PR's diff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant