Skip to content

Commit

Permalink
Implement resumable upload for google drive
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-pratik-k committed Jan 13, 2025
1 parent 6fd0c09 commit 4e5551f
Show file tree
Hide file tree
Showing 15 changed files with 681 additions and 105 deletions.
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);
}

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.

164 changes: 163 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,169 @@ class DropboxUploadEndpoint extends Endpoint {
}
],
}),
'Content-Type': content.contentType,
'Content-Type': content.type,
'Content-Length': content.length,
};

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

@override
CancelToken? get cancelToken => cancellationToken;

@override
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,
'Content-Length': content.length,
};

@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,
'Content-Length': content.length,
};

@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,
'Content-Length': content.length,
};

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);
}
5 changes: 3 additions & 2 deletions data/lib/models/media_content/media_content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ class AppMediaContent with _$AppMediaContent {
const factory AppMediaContent({
required Stream<List<int>> stream,
required int? length,
required String contentType,
String? range,
required String type,
}) = _AppMediaContent;

factory AppMediaContent.fromGoogleDrive(drive.Media media) => AppMediaContent(
stream: media.stream,
length: media.length,
contentType: media.contentType,
type: media.contentType,
);
}
Loading

0 comments on commit 4e5551f

Please sign in to comment.