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
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion app/lib/ui/flow/media_transfer/components/transfer_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ class UploadProcessItem extends StatelessWidget {
final UploadMediaProcess process;
final void Function() onCancelTap;
final void Function() onRemoveTap;
final void Function() onPausedTap;
final void Function() onResumeTap;

const UploadProcessItem({
super.key,
required this.process,
required this.onCancelTap,
required this.onRemoveTap,
required this.onPausedTap,
required this.onResumeTap,
});

@override
Expand Down Expand Up @@ -77,7 +81,27 @@ class UploadProcessItem extends StatelessWidget {
],
),
),
if (process.status.isRunning || process.status.isWaiting)
if (process.status.isPaused)
ActionButton(
onPressed: onResumeTap,
icon: Icon(
CupertinoIcons.play,
color: context.colorScheme.textPrimary,
size: 20,
),
),
if (process.status.isRunning)
ActionButton(
onPressed: onPausedTap,
icon: Icon(
CupertinoIcons.pause,
color: context.colorScheme.textPrimary,
size: 20,
),
),
if (process.status.isRunning ||
process.status.isWaiting ||
process.status.isPaused)
ActionButton(
onPressed: onCancelTap,
icon: Icon(
Expand All @@ -86,6 +110,15 @@ class UploadProcessItem extends StatelessWidget {
size: 20,
),
),
if (process.status.isTerminated || process.status.isFailed)
ActionButton(
onPressed: onResumeTap,
icon: Icon(
CupertinoIcons.refresh,
color: context.colorScheme.textSecondary,
size: 20,
),
),
if (process.status.isTerminated ||
process.status.isFailed ||
process.status.isCompleted)
Expand All @@ -105,6 +138,8 @@ class UploadProcessItem extends StatelessWidget {
String _getUploadMessage(BuildContext context) {
if (process.status.isWaiting) {
return context.l10n.upload_status_waiting;
} else if (process.status.isPaused) {
return "Upload paused";
} else if (process.status.isFailed) {
return context.l10n.upload_status_failed;
} else if (process.status.isCompleted) {
Expand Down
6 changes: 6 additions & 0 deletions app/lib/ui/flow/media_transfer/media_transfer_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ class _MediaTransferScreenState extends ConsumerState<MediaTransferScreen> {
itemBuilder: (context, index) => UploadProcessItem(
key: ValueKey(uploadProcesses[index].id),
process: uploadProcesses[index],
onResumeTap: () {
notifier.onResumeUploadProcess(uploadProcesses[index].id);
},
onPausedTap: () {
notifier.onPauseUploadProcess(uploadProcesses[index].id);
},
onRemoveTap: () {
notifier.onRemoveUploadProcess(uploadProcesses[index].id);
},
Expand Down
8 changes: 8 additions & 0 deletions app/lib/ui/flow/media_transfer/media_transfer_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ class MediaTransferStateNotifier extends StateNotifier<MediaTransferState> {
_mediaProcessRepo.removeItemFromUploadQueue(id);
}

void onPauseUploadProcess(String id) {
_mediaProcessRepo.pauseUploadProcess(id);
}

void onResumeUploadProcess(String id) {
_mediaProcessRepo.resumeUploadProcess(id);
}
Comment on lines +41 to +47
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.


void onRemoveDownloadProcess(String id) {
_mediaProcessRepo.removeItemFromDownloadQueue(id);
}
Expand Down
2 changes: 1 addition & 1 deletion data/.flutter-plugins-dependencies

Large diffs are not rendered by default.

161 changes: 160 additions & 1 deletion data/lib/apis/dropbox/dropbox_content_endpoints.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class DropboxUploadEndpoint extends Endpoint {
}
],
}),
'Content-Type': content.contentType,
'Content-Type': content.type,
'Content-Length': content.length,
};

Expand All @@ -161,6 +161,165 @@ class DropboxUploadEndpoint extends Endpoint {
void Function(int p1, int p2)? get onSendProgress => onProgress;
}

class DropboxStartUploadEndpoint extends Endpoint {
final AppMediaContent content;
final void Function(int chunk, int length)? onProgress;
final CancelToken? cancellationToken;

const DropboxStartUploadEndpoint({
this.cancellationToken,
this.onProgress,
required this.content,
});

@override
String get baseUrl => BaseURL.dropboxContentV2;

@override
HttpMethod get method => HttpMethod.post;

@override
String get path => '/files/upload_session/start';

@override
Map<String, dynamic> get headers => {
'Content-Type': content.type,
};

@override
Object? get data => content.stream;

@override
CancelToken? get cancelToken => cancellationToken;

@override
void Function(int p1, int p2)? get onSendProgress => onProgress;
}

class DropboxAppendUploadEndpoint extends Endpoint {
final String sessionId;
final int offset;
final AppMediaContent content;
final void Function(int chunk, int length)? onProgress;
final CancelToken? cancellationToken;

const DropboxAppendUploadEndpoint({
required this.sessionId,
required this.offset,
this.cancellationToken,
this.onProgress,
required this.content,
});

@override
String get baseUrl => BaseURL.dropboxContentV2;

@override
HttpMethod get method => HttpMethod.post;

@override
String get path => '/files/upload_session/append_v2';

@override
Map<String, dynamic> get headers => {
'Dropbox-API-Arg': jsonEncode({
'cursor': {
'session_id': sessionId,
'offset': offset,
},
}),
'Content-Type': content.type,
};

@override
Object? get data => content.stream;

@override
CancelToken? get cancelToken => cancellationToken;

@override
void Function(int p1, int p2)? get onSendProgress => onProgress;
}

class DropboxFinishUploadEndpoint extends Endpoint {
final String? appPropertyTemplateId;
final String filePath;
final String? localRefId;
final String mode;
final bool autoRename;
final bool mute;
final bool strictConflict;

final String sessionId;
final int offset;
final AppMediaContent content;
final void Function(int chunk, int length)? onProgress;
final CancelToken? cancellationToken;

const DropboxFinishUploadEndpoint({
this.appPropertyTemplateId,
required this.filePath,
this.mode = 'add',
this.autoRename = true,
this.mute = false,
this.localRefId,
this.strictConflict = false,
this.cancellationToken,
this.onProgress,
required this.content,
required this.sessionId,
required this.offset,
});

@override
String get baseUrl => BaseURL.dropboxContentV2;

@override
HttpMethod get method => HttpMethod.post;

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

@override
Map<String, dynamic> get headers => {
'Dropbox-API-Arg': jsonEncode({
"commit": {
"autorename": autoRename,
"mode": mode,
"mute": mute,
"path": filePath,
"strict_conflict": strictConflict,
if (appPropertyTemplateId != null && localRefId != null)
'property_groups': [
{
"fields": [
{
"name": ProviderConstants.localRefIdKey,
"value": localRefId ?? '',
},
],
"template_id": appPropertyTemplateId,
}
],
},
"cursor": {
"offset": offset,
"session_id": sessionId,
},
}),
'Content-Type': content.type,
};

@override
Object? get data => content.stream;

@override
CancelToken? get cancelToken => cancellationToken;

@override
void Function(int p1, int p2)? get onSendProgress => onProgress;
}

class DropboxDownloadEndpoint extends DownloadEndpoint {
final String filePath;
final String? storagePath;
Expand Down
79 changes: 77 additions & 2 deletions data/lib/apis/google_drive/google_drive_endpoint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class GoogleDriveUploadEndpoint extends Endpoint {

@override
Map<String, dynamic> get headers => {
'Content-Type': content.contentType,
'Content-Type': content.type,
'Content-Length': content.length.toString(),
};

Expand Down Expand Up @@ -84,6 +84,81 @@ class GoogleDriveUploadEndpoint extends Endpoint {
void Function(int p1, int p2)? get onSendProgress => onProgress;
}

class GoogleDriveStartUploadEndpoint extends Endpoint {
final drive.File request;
final CancelToken? cancellationToken;

const GoogleDriveStartUploadEndpoint({
required this.request,
this.cancellationToken,
});

@override
String get baseUrl => BaseURL.googleDriveUploadV3;

@override
CancelToken? get cancelToken => cancellationToken;

@override
HttpMethod get method => HttpMethod.post;

@override
Object? get data => request.toJson();

@override
String get path => '/files';

@override
Map<String, dynamic>? get queryParameters => {
'uploadType': 'resumable',
};
}

class GoogleDriveAppendUploadEndpoint extends Endpoint {
final String uploadId;
final AppMediaContent content;
final CancelToken? cancellationToken;
final void Function(int chunk, int length)? onProgress;

const GoogleDriveAppendUploadEndpoint({
required this.uploadId,
required this.content,
this.cancellationToken,
this.onProgress,
});

@override
String get baseUrl => BaseURL.googleDriveUploadV3;

@override
CancelToken? get cancelToken => cancellationToken;

@override
HttpMethod get method => HttpMethod.put;

@override
Map<String, dynamic> get headers => {
'Content-Type': content.type,
'Content-Length': content.length.toString(),
'Content-Range': content.range,
};

@override
Object? get data => content.stream;

@override
String get path => '/files';

@override
Map<String, dynamic>? get queryParameters => {
'upload_id': uploadId,
'uploadType': 'resumable',
};

@override
void Function(int p1, int p2)? get onSendProgress => onProgress;
}

class GoogleDriveContentUpdateEndpoint extends Endpoint {
final AppMediaContent content;
final String id;
Expand All @@ -108,7 +183,7 @@ class GoogleDriveContentUpdateEndpoint extends Endpoint {

@override
Map<String, dynamic> get headers => {
'Content-Type': content.contentType,
'Content-Type': content.type,
'Content-Length': content.length.toString(),
};

Expand Down
8 changes: 8 additions & 0 deletions data/lib/domain/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,11 @@ class LocalDatabaseConstants {
class FeatureFlag {
static final googleDriveSupport = true;
}

class ApiConfigs {
/// The size of the byte to be uploaded from the server in one request.
static final uploadRequestByteSize = 262144;

/// The duration to wait before updating the progress of the process.
static final processProgressUpdateDuration = Duration(milliseconds: 300);
}
Loading
Loading