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

Merged
merged 14 commits into from
Jan 22, 2025
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";
cp-pratik-k marked this conversation as resolved.
Show resolved Hide resolved
} 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);
}
cp-pratik-k marked this conversation as resolved.
Show resolved Hide resolved

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';
cp-pratik-k marked this conversation as resolved.
Show resolved Hide resolved

@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
Loading