Skip to content

Commit

Permalink
Merge pull request #1822 from ardriveapp/dev
Browse files Browse the repository at this point in the history
PE-6750: Release ArDrive v2.51.0
  • Loading branch information
thiagocarvalhodev authored Aug 26, 2024
2 parents ebb6663 + b826be3 commit f00bd95
Show file tree
Hide file tree
Showing 11 changed files with 245 additions and 64 deletions.
2 changes: 2 additions & 0 deletions android/fastlane/metadata/android/en-US/changelogs/145.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Show Retry Failed Uploads modal for conflicts with failed uploads.
- Stop all uploads immediately upon receiving any ‘underfunded’ response from Turbo.
4 changes: 3 additions & 1 deletion lib/blocs/upload/enums/conflicting_files_actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
/// `Skip` Will ignore the files and don't upload them.
///
/// `Replace` will upload the conflicting file and replace the existent.
enum UploadActions { skip, replace }
///
/// `SkipSuccessfulUploads` will skip the files that were successfully uploaded.
enum UploadActions { skip, skipSuccessfulUploads, replace }
73 changes: 66 additions & 7 deletions lib/blocs/upload/upload_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ class UploadCubit extends Cubit<UploadState> {
/// Map of conflicting file ids keyed by their file names.
final Map<String, String> conflictingFiles = {};
final List<String> conflictingFolders = [];
List<String> failedFiles = [];

UploadCubit({
required this.driveId,
Expand Down Expand Up @@ -312,32 +313,81 @@ class UploadCubit extends Cubit<UploadState> {
checkConflicts();
}

Future<void> checkConflictingFiles() async {
Future<void> checkConflictingFiles({
bool checkFailedFiles = true,
}) async {
emit(UploadPreparationInProgress());

_removeFilesWithFolderNameConflicts();

for (final file in files) {
final fileName = file.ioFile.name;
final existingFileId = await _driveDao
final existingFileIds = await _driveDao
.filesInFolderWithName(
driveId: _targetDrive.id,
parentFolderId: file.parentFolderId,
name: fileName,
)
.map((f) => f.id)
.getSingleOrNull();
.get();

if (existingFileIds.isNotEmpty) {
final existingFileId = existingFileIds.first;

if (existingFileId != null) {
logger.d('Found conflicting file. Existing file id: $existingFileId');
conflictingFiles[file.getIdentifier()] = existingFileId;
}
}

if (conflictingFiles.isNotEmpty) {
if (checkFailedFiles) {
failedFiles.clear();

conflictingFiles.forEach((key, value) {
logger.d('Checking if file $key has failed');
});

for (final fileNameKey in conflictingFiles.keys) {
final fileId = conflictingFiles[fileNameKey];

final fileRevision = await _driveDao
.latestFileRevisionByFileId(
driveId: driveId,
fileId: fileId!,
)
.getSingleOrNull();

final status = _driveDao.select(_driveDao.networkTransactions)
..where((tbl) => tbl.id.equals(fileRevision!.dataTxId));

final transaction = await status.getSingleOrNull();

logger.d('Transaction status: ${transaction?.status}');

if (transaction?.status == TransactionStatus.failed) {
failedFiles.add(fileNameKey);
}
}

logger.d('Failed files: $failedFiles');

if (failedFiles.isNotEmpty) {
emit(
UploadConflictWithFailedFiles(
areAllFilesConflicting: conflictingFiles.length == files.length,
conflictingFileNames: conflictingFiles.keys.toList(),
conflictingFileNamesForFailedFiles: failedFiles,
),
);
return;
}
}

emit(
UploadFileConflict(
areAllFilesConflicting: conflictingFiles.length == files.length,
conflictingFileNames: conflictingFiles.keys.toList(),
conflictingFileNamesForFailedFiles: const [],
),
);
} else {
Expand Down Expand Up @@ -369,19 +419,20 @@ class UploadCubit extends Cubit<UploadState> {
)
.map((f) => f.id)
.getSingleOrNull();
final existingFileId = await _driveDao
final existingFileIds = await _driveDao
.filesInFolderWithName(
driveId: driveId,
name: folder.name,
parentFolderId: folder.parentFolderId,
)
.map((f) => f.id)
.getSingleOrNull();
.get();

if (existingFolderId != null) {
folder.id = existingFolderId;
foldersToSkip.add(folder);
}
if (existingFileId != null) {
if (existingFileIds.isNotEmpty) {
conflictingFolders.add(folder.name);
}
}
Expand Down Expand Up @@ -419,6 +470,8 @@ class UploadCubit extends Cubit<UploadState> {

if (uploadAction == UploadActions.skip) {
_removeFilesWithFileNameConflicts();
} else if (uploadAction == UploadActions.skipSuccessfulUploads) {
_removeSuccessfullyUploadedFiles();
}

logger.d(
Expand Down Expand Up @@ -939,6 +992,12 @@ class UploadCubit extends Cubit<UploadState> {
);
}

void _removeSuccessfullyUploadedFiles() {
files.removeWhere(
(file) => !failedFiles.contains(file.getIdentifier()),
);
}

void _removeFilesWithFolderNameConflicts() {
files.removeWhere((file) => conflictingFolders.contains(file.ioFile.name));
}
Expand Down
12 changes: 12 additions & 0 deletions lib/blocs/upload/upload_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,33 @@ class UploadSigningInProgress extends UploadState {

class UploadFileConflict extends UploadState {
final List<String> conflictingFileNames;
final List<String> conflictingFileNamesForFailedFiles;

final bool areAllFilesConflicting;

UploadFileConflict({
required this.conflictingFileNames,
required this.areAllFilesConflicting,
required this.conflictingFileNamesForFailedFiles,
});

@override
List<Object> get props => [conflictingFileNames];
}

class UploadConflictWithFailedFiles extends UploadFileConflict {
UploadConflictWithFailedFiles({
required super.conflictingFileNames,
required super.areAllFilesConflicting,
super.conflictingFileNamesForFailedFiles = const <String>[],
});
}

class UploadFolderNameConflict extends UploadFileConflict {
UploadFolderNameConflict({
required super.conflictingFileNames,
required super.areAllFilesConflicting,
super.conflictingFileNamesForFailedFiles = const <String>[],
});
}

Expand Down
138 changes: 93 additions & 45 deletions lib/components/upload_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -288,63 +288,111 @@ class _UploadFormState extends State<UploadForm> {
),
],
);
} else if (state is UploadFileConflict) {
} else if (state is UploadConflictWithFailedFiles) {
return ArDriveStandardModal(
title: appLocalizationsOf(context)
.duplicateFiles(state.conflictingFileNames.length),
content: SizedBox(
width: kMediumDialogWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appLocalizationsOf(context)
.filesWithTheSameNameAlreadyExists(
state.conflictingFileNames.length,
title: 'Retry Failed Uploads?',
content: SizedBox(
width: kMediumDialogWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'There are ${state.conflictingFileNamesForFailedFiles.length} file(s) marked with a red dot, indicating they failed to upload. Would you like to retry uploading these files by replacing the failed versions? This action will only affect the failed uploads and will not alter any successfully uploaded files. Alternatively, you can choose to skip these files and proceed with the others.',
style: ArDriveTypography.body.buttonNormalRegular(),
),
const SizedBox(height: 16),
Text(
'Conflicting files',
style: ArDriveTypography.body.buttonNormalRegular(),
),
const SizedBox(height: 8),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 320),
child: SingleChildScrollView(
child: Text(
state.conflictingFileNamesForFailedFiles
.join(', \n'),
style: ArDriveTypography.body.buttonNormalRegular(),
),
style: ArDriveTypography.body.buttonNormalRegular(),
),
const SizedBox(height: 16),
Text(
appLocalizationsOf(context).conflictingFiles,
style: ArDriveTypography.body.buttonNormalRegular(),
),
],
),
),
actions: [
ModalAction(
action: () => context
.read<UploadCubit>()
.checkConflictingFiles(checkFailedFiles: false),
title: appLocalizationsOf(context).skipEmphasized,
),
ModalAction(
action: () => context
.read<UploadCubit>()
.prepareUploadPlanAndCostEstimates(
uploadAction: UploadActions.skipSuccessfulUploads),
title: 'Replace failed uploads',
),
],
);
} else if (state is UploadFileConflict) {
return ArDriveStandardModal(
title: appLocalizationsOf(context)
.duplicateFiles(state.conflictingFileNames.length),
content: SizedBox(
width: kMediumDialogWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appLocalizationsOf(context)
.filesWithTheSameNameAlreadyExists(
state.conflictingFileNames.length,
),
const SizedBox(height: 8),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 320),
child: SingleChildScrollView(
child: Text(
state.conflictingFileNames.join(', \n'),
style:
ArDriveTypography.body.buttonNormalRegular(),
),
style: ArDriveTypography.body.buttonNormalRegular(),
),
const SizedBox(height: 16),
Text(
appLocalizationsOf(context).conflictingFiles,
style: ArDriveTypography.body.buttonNormalRegular(),
),
const SizedBox(height: 8),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 320),
child: SingleChildScrollView(
child: Text(
state.conflictingFileNames.join(', \n'),
style: ArDriveTypography.body.buttonNormalRegular(),
),
),
],
),
),
actions: [
if (!state.areAllFilesConflicting)
ModalAction(
action: () => context
.read<UploadCubit>()
.prepareUploadPlanAndCostEstimates(
uploadAction: UploadActions.skip),
title: appLocalizationsOf(context).skipEmphasized,
),
ModalAction(
action: () => Navigator.of(context).pop(false),
title: appLocalizationsOf(context).cancelEmphasized,
),
],
),
),
actions: [
if (!state.areAllFilesConflicting)
ModalAction(
action: () => context
.read<UploadCubit>()
.prepareUploadPlanAndCostEstimates(
uploadAction: UploadActions.replace),
title: appLocalizationsOf(context).replaceEmphasized,
uploadAction: UploadActions.skip),
title: appLocalizationsOf(context).skipEmphasized,
),
]);
ModalAction(
action: () => Navigator.of(context).pop(false),
title: appLocalizationsOf(context).cancelEmphasized,
),
ModalAction(
action: () => context
.read<UploadCubit>()
.prepareUploadPlanAndCostEstimates(
uploadAction: UploadActions.replace),
title: appLocalizationsOf(context).replaceEmphasized,
),
],
);
} else if (state is UploadFileTooLarge) {
return ArDriveStandardModal(
title: appLocalizationsOf(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ query FirstDriveEntityWithIdOwner($driveId: String!, $after: String) {
tags: [
{ name: "Drive-Id", values: [$driveId] }
{ name: "Entity-Type", values: ["drive"] }
{ name: "Drive-Privacy", values: ["public", "private"]}
]
) {
edges {
Expand Down
12 changes: 12 additions & 0 deletions packages/ardrive_uploader/lib/src/exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ class UnknownNetworkException implements NetworkException {
Object? error;
}

class UnderFundException implements ArDriveUploaderExceptions {
UnderFundException({
required this.message,
this.error,
});

@override
final String message;
@override
Object? error;
}

class ThumbnailUploadException implements UploadStrategyException {
ThumbnailUploadException({
required this.message,
Expand Down
4 changes: 3 additions & 1 deletion packages/ardrive_uploader/lib/src/turbo_upload_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class TurboUploadService {
}, onError: (error) {
onSendProgressTimer?.cancel();
_cancelTokens.remove(cancelToken);

throw error;
});
} catch (e) {
Expand Down Expand Up @@ -212,7 +213,8 @@ class TurboUploadService {
logger.i('[$dataItemId] DataItem confirmed!');
return response;
case 'UNDERFUNDED':
throw UploadCanceledException('Upload canceled. Underfunded.');
throw UnderFundException(
message: 'Upload canceled. Underfunded.', error: response.data);
case 'ASSEMBLING':
case 'VALIDATING':
case 'FINALIZING':
Expand Down
Loading

0 comments on commit f00bd95

Please sign in to comment.