diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index 1672b1e5a3..38af0ec18d 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/drive_detail/selected_item.dart'; import 'package:ardrive/entities/constants.dart'; import 'package:ardrive/entities/string_types.dart'; @@ -12,8 +13,6 @@ import 'package:moor/moor.dart'; import 'package:rxdart/rxdart.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../blocs.dart'; - part 'drive_detail_state.dart'; class DriveDetailCubit extends Cubit { diff --git a/lib/blocs/drives/drives_cubit.dart b/lib/blocs/drives/drives_cubit.dart index 35cea30977..e8eb94c1cf 100644 --- a/lib/blocs/drives/drives_cubit.dart +++ b/lib/blocs/drives/drives_cubit.dart @@ -1,13 +1,12 @@ import 'dart:async'; +import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/models/models.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:moor/moor.dart'; import 'package:rxdart/rxdart.dart'; -import '../blocs.dart'; - part 'drives_state.dart'; /// [DrivesCubit] includes logic for displaying the drives attached in the app. diff --git a/lib/blocs/folder_create/folder_create_cubit.dart b/lib/blocs/folder_create/folder_create_cubit.dart index b5e47278ff..79aeba6c87 100644 --- a/lib/blocs/folder_create/folder_create_cubit.dart +++ b/lib/blocs/folder_create/folder_create_cubit.dart @@ -97,7 +97,8 @@ class FolderCreateCubit extends Cubit { await _arweave.postTx(folderTx); folderEntity.txId = folderTx.id; await _driveDao.insertFolderRevision(folderEntity.toRevisionCompanion( - performedAction: RevisionAction.create)); + performedAction: RevisionAction.create, + )); }); } catch (err) { addError(err); diff --git a/lib/blocs/sync/sync_cubit.dart b/lib/blocs/sync/sync_cubit.dart index f0436900fe..fae4fcd949 100644 --- a/lib/blocs/sync/sync_cubit.dart +++ b/lib/blocs/sync/sync_cubit.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'package:ardrive/blocs/activity/activity_cubit.dart'; +import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/sync/ghost_folder.dart'; import 'package:ardrive/entities/entities.dart'; import 'package:ardrive/entities/string_types.dart'; @@ -17,8 +18,6 @@ import 'package:flutter/material.dart'; import 'package:meta/meta.dart'; import 'package:moor/moor.dart'; -import '../blocs.dart'; - part 'sync_state.dart'; const kRequiredTxConfirmationCount = 15; diff --git a/lib/blocs/upload/cost_estimate.dart b/lib/blocs/upload/cost_estimate.dart index 48b00d206c..1ec8f32256 100644 --- a/lib/blocs/upload/cost_estimate.dart +++ b/lib/blocs/upload/cost_estimate.dart @@ -1,6 +1,6 @@ -import 'package:ardrive/blocs/upload/bundle_upload_handle.dart'; -import 'package:ardrive/blocs/upload/file_upload_handle.dart'; -import 'package:ardrive/blocs/upload/upload_plan.dart'; +import 'package:ardrive/blocs/upload/models/upload_plan.dart'; +import 'package:ardrive/blocs/upload/upload_handles/bundle_upload_handle.dart'; +import 'package:ardrive/blocs/upload/upload_handles/file_v2_upload_handle.dart'; import 'package:ardrive/entities/entity.dart'; import 'package:ardrive/services/arweave/arweave.dart'; import 'package:ardrive/services/pst/pst.dart'; @@ -42,7 +42,7 @@ class CostEstimate { required PstService pstService, required Wallet wallet, }) async { - final _v2FileUploadHandles = uploadPlan.v2FileUploadHandles; + final _v2FileUploadHandles = uploadPlan.fileV2UploadHandles; final dataItemsCost = await estimateCostOfAllBundles( bundleUploadHandles: uploadPlan.bundleUploadHandles, arweaveService: arweaveService, @@ -128,7 +128,7 @@ class CostEstimate { } static Future estimateV2UploadsCost({ - required List fileUploadHandles, + required List fileUploadHandles, required ArweaveService arweaveService, }) async { var totalCost = BigInt.zero; @@ -141,7 +141,7 @@ class CostEstimate { } static Future estimateV2FileUploadCost({ - required FileUploadHandle fileUploadHandle, + required FileV2UploadHandle fileUploadHandle, required ArweaveService arweaveService, }) async { return await arweaveService.getPrice( diff --git a/lib/blocs/upload/models/folder_prepare_result.dart b/lib/blocs/upload/models/folder_prepare_result.dart new file mode 100644 index 0000000000..e1c7499f32 --- /dev/null +++ b/lib/blocs/upload/models/folder_prepare_result.dart @@ -0,0 +1,11 @@ +import 'package:ardrive/blocs/upload/models/web_file.dart'; +import 'package:ardrive/blocs/upload/models/web_folder.dart'; + +class FolderPrepareResult { + List files; + Map foldersByPath; + FolderPrepareResult({ + required this.files, + required this.foldersByPath, + }); +} diff --git a/lib/blocs/upload/models/io_file.dart b/lib/blocs/upload/models/io_file.dart new file mode 100644 index 0000000000..ef99c01b7a --- /dev/null +++ b/lib/blocs/upload/models/io_file.dart @@ -0,0 +1,50 @@ +import 'dart:typed_data'; + +// Use the crossfile package instead to make this suitable for use with mobile +import 'package:file_selector/file_selector.dart'; + +import 'upload_file.dart'; + +class IOFile extends UploadFile { + final XFile file; + @override + final DateTime lastModifiedDate; + @override + final int size; + + @override + final String parentFolderId; + + IOFile._create( + this.file, + this.lastModifiedDate, + this.size, + this.parentFolderId, + ) : super( + name: file.name, + path: file.path, + lastModifiedDate: lastModifiedDate, + size: size, + parentFolderId: parentFolderId, + ); + + static Future fromXFile(XFile file, String parentFolderId) async { + final fileLastModified = await file.lastModified(); + final fileSize = await file.length(); + + return IOFile._create( + file, + fileLastModified, + fileSize, + parentFolderId, + ); + } + + @override + Future readAsBytes() => file.readAsBytes(); + + @override + String getIdentifier() { + return path.isEmpty ? name : path; + } +} diff --git a/lib/blocs/upload/models/models.dart b/lib/blocs/upload/models/models.dart new file mode 100644 index 0000000000..9eff86df4a --- /dev/null +++ b/lib/blocs/upload/models/models.dart @@ -0,0 +1,5 @@ +export 'folder_prepare_result.dart'; +export 'upload_file.dart'; +export 'upload_plan.dart'; +export 'web_file.dart'; +export 'web_folder.dart'; diff --git a/lib/blocs/upload/models/upload_file.dart b/lib/blocs/upload/models/upload_file.dart new file mode 100644 index 0000000000..67ff677c8b --- /dev/null +++ b/lib/blocs/upload/models/upload_file.dart @@ -0,0 +1,20 @@ +import 'dart:typed_data'; + +abstract class UploadFile { + String name; + String path; + DateTime lastModifiedDate; + int size; + String parentFolderId; + Future readAsBytes(); + + String getIdentifier(); + + UploadFile({ + required this.name, + required this.path, + required this.lastModifiedDate, + required this.parentFolderId, + required this.size, + }); +} diff --git a/lib/blocs/upload/models/upload_plan.dart b/lib/blocs/upload/models/upload_plan.dart new file mode 100644 index 0000000000..44c42a2fb9 --- /dev/null +++ b/lib/blocs/upload/models/upload_plan.dart @@ -0,0 +1,68 @@ +import 'package:ardrive/blocs/upload/upload_handles/bundle_upload_handle.dart'; +import 'package:ardrive/blocs/upload/upload_handles/folder_data_item_upload_handle.dart'; +import 'package:ardrive/blocs/upload/upload_handles/upload_handle.dart'; +import 'package:ardrive/utils/bundles/next_fit_bundle_packer.dart'; + +import '../upload_handles/file_data_item_upload_handle.dart'; +import '../upload_handles/file_v2_upload_handle.dart'; + +final bundleSizeLimit = 503316480; +final maxBundleDataItemCount = 500; +final maxFilesPerBundle = maxBundleDataItemCount ~/ 2; + +class UploadPlan { + /// A map of [FileV2UploadHandle]s keyed by their respective file's id. + late Map fileV2UploadHandles; + + final List bundleUploadHandles = []; + + UploadPlan._create({ + required this.fileV2UploadHandles, + }); + + static Future create({ + required Map fileV2UploadHandles, + required Map fileDataItemUploadHandles, + required Map + folderDataItemUploadHandles, + }) async { + final bundle = UploadPlan._create( + fileV2UploadHandles: fileV2UploadHandles, + ); + if (fileDataItemUploadHandles.isNotEmpty || + folderDataItemUploadHandles.isNotEmpty) { + await bundle.createBundleHandlesFromDataItemHandles( + fileDataItemUploadHandles: fileDataItemUploadHandles, + folderDataItemUploadHandles: folderDataItemUploadHandles, + ); + } + return bundle; + } + + Future createBundleHandlesFromDataItemHandles({ + Map fileDataItemUploadHandles = const {}, + Map folderDataItemUploadHandles = + const {}, + }) async { + final bundleItems = await NextFitBundlePacker( + maxBundleSize: bundleSizeLimit, + maxDataItemCount: maxFilesPerBundle, + ).packItems([ + ...fileDataItemUploadHandles.values, + ...folderDataItemUploadHandles.values + ]); + for (var uploadHandles in bundleItems) { + final bundleToUpload = await BundleUploadHandle.create( + fileDataItemUploadHandles: List.from( + uploadHandles.whereType(), + ), + folderDataItemUploadHandles: List.from( + uploadHandles.whereType(), + ), + ); + bundleUploadHandles.add(bundleToUpload); + uploadHandles.clear(); + } + fileDataItemUploadHandles.clear(); + } +} diff --git a/lib/blocs/upload/models/web_file.dart b/lib/blocs/upload/models/web_file.dart new file mode 100644 index 0000000000..1f84c01262 --- /dev/null +++ b/lib/blocs/upload/models/web_file.dart @@ -0,0 +1,32 @@ +import 'dart:html'; +import 'dart:typed_data'; + +import 'package:ardrive/blocs/upload/models/upload_file.dart'; + +class WebFile extends UploadFile { + final File file; + @override + final String parentFolderId; + WebFile(this.file, this.parentFolderId) + : super( + name: file.name, + path: file.relativePath!, + lastModifiedDate: + DateTime.fromMillisecondsSinceEpoch(file.lastModified!), + size: file.size, + parentFolderId: parentFolderId, + ); + + @override + Future readAsBytes() async { + final reader = FileReader(); + reader.readAsArrayBuffer(file); + await reader.onLoad.first; + return reader.result as Uint8List; + } + + @override + String getIdentifier() { + return path.isEmpty ? name : path; + } +} diff --git a/lib/blocs/upload/models/web_folder.dart b/lib/blocs/upload/models/web_folder.dart new file mode 100644 index 0000000000..2234b1fb41 --- /dev/null +++ b/lib/blocs/upload/models/web_folder.dart @@ -0,0 +1,13 @@ +class WebFolder { + final String name; + final String parentFolderPath; + + String id; + late String parentFolderId; + late String path; + WebFolder({ + required this.name, + required this.id, + required this.parentFolderPath, + }); +} diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index 16a1a85a95..90805834c3 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -1,19 +1,18 @@ import 'dart:async'; import 'dart:math' as math; +import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/upload/cost_estimate.dart'; -import 'package:ardrive/blocs/upload/upload_plan.dart'; +import 'package:ardrive/blocs/upload/models/models.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/utils/upload_plan_utils.dart'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:file_selector/file_selector.dart'; import 'package:meta/meta.dart'; import 'package:pedantic/pedantic.dart'; import 'package:rxdart/rxdart.dart'; -import '../blocs.dart'; import 'enums/conflicting_files_actions.dart'; part 'upload_state.dart'; @@ -21,11 +20,11 @@ part 'upload_state.dart'; final privateFileSizeLimit = 104857600; final publicFileSizeLimit = 1.25 * math.pow(10, 9); final minimumPstTip = BigInt.from(10000000); +final filesNamesToExclude = ['.DS_Store']; class UploadCubit extends Cubit { final String driveId; final String folderId; - final List files; final ProfileCubit _profileCubit; final DriveDao _driveDao; @@ -33,9 +32,13 @@ class UploadCubit extends Cubit { final PstService _pst; final UploadPlanUtils _uploadPlanUtils; + late bool uploadFolders; late Drive _targetDrive; late FolderEntry _targetFolder; + List files = []; + Map foldersByPath = {}; + /// Map of conflicting file ids keyed by their file names. final Map conflictingFiles = {}; final List conflictingFolders = []; @@ -51,6 +54,7 @@ class UploadCubit extends Cubit { required ArweaveService arweave, required PstService pst, required UploadPlanUtils uploadPlanUtils, + this.uploadFolders = false, }) : _profileCubit = profileCubit, _driveDao = driveDao, _arweave = arweave, @@ -59,6 +63,7 @@ class UploadCubit extends Cubit { super(UploadPreparationInProgress()); Future startUploadPreparation() async { + files.removeWhere((file) => filesNamesToExclude.contains(file.name)); _targetDrive = await _driveDao.driveById(driveId: driveId).getSingle(); _targetFolder = await _driveDao .folderById(driveId: driveId, folderId: folderId) @@ -73,13 +78,19 @@ class UploadCubit extends Cubit { Future checkConflictingFolders() async { emit(UploadPreparationInProgress()); - + if (uploadFolders) { + final folderPrepareResult = + await generateFoldersAndAssignParentsForFiles(files); + files = folderPrepareResult.files; + foldersByPath = folderPrepareResult.foldersByPath; + } for (final file in files) { final fileName = file.name; + final existingFolderName = await _driveDao .foldersInFolderWithName( driveId: _targetDrive.id, - parentFolderId: _targetFolder.id, + parentFolderId: file.parentFolderId, name: fileName, ) .map((f) => f.name) @@ -112,14 +123,14 @@ class UploadCubit extends Cubit { final existingFileId = await _driveDao .filesInFolderWithName( driveId: _targetDrive.id, - parentFolderId: _targetFolder.id, + parentFolderId: file.parentFolderId, name: fileName, ) .map((f) => f.id) .getSingleOrNull(); if (existingFileId != null) { - conflictingFiles[fileName] = existingFileId; + conflictingFiles[file.getIdentifier()] = existingFileId; } } @@ -135,6 +146,68 @@ class UploadCubit extends Cubit { } } + /// Generate Folders and assign parentFolderIds + + Future generateFoldersAndAssignParentsForFiles( + List files, + ) async { + final folders = UploadPlanUtils.generateFoldersForFiles( + files as List, + ); + final foldersToSkip = []; + for (var folder in folders.values) { + //If The folders map contains the immediate ancestor of the current folder + //we use the id of that folder, otherwise use targetFolder as root + + folder.parentFolderId = folders.containsKey(folder.parentFolderPath) + ? folders[folder.parentFolderPath]!.id + : _targetFolder.id; + final existingFolderId = await _driveDao + .foldersInFolderWithName( + driveId: driveId, + name: folder.name, + parentFolderId: folder.parentFolderId, + ) + .map((f) => f.id) + .getSingleOrNull(); + final existingFileId = await _driveDao + .filesInFolderWithName( + driveId: driveId, + name: folder.name, + parentFolderId: folders[folder.parentFolderPath] != null + ? folders[folder.parentFolderPath]!.id + : _targetFolder.id, + ) + .map((f) => f.id) + .getSingleOrNull(); + if (existingFolderId != null) { + folder.id = existingFolderId; + foldersToSkip.add(folder); + } + if (existingFileId != null) { + conflictingFolders.add(folder.name); + } + folder.path = folder.parentFolderPath.isNotEmpty + ? '${_targetFolder.path}/${folder.parentFolderPath}/${folder.name}' + : '${_targetFolder.path}/${folder.name}'; + } + final filesToUpload = []; + files.forEach((file) { + // Splits the file path, gets rid of the file name and rejoins the strings + // to get parent folder path. + // eg: Test/A/B/C/file.txt becomes Test/A/B/C + final fileFolder = (file.path.split('/')..removeLast()).join('/'); + filesToUpload.add( + WebFile( + file.file, + folders[fileFolder]?.id ?? _targetFolder.id, + ), + ); + }); + folders.removeWhere((key, value) => foldersToSkip.contains(value)); + return FolderPrepareResult(files: filesToUpload, foldersByPath: folders); + } + /// If `conflictingFileAction` is null, means that had no conflict. Future prepareUploadPlanAndCostEstimates({ ConflictingFileActions? conflictingFileAction, @@ -160,7 +233,7 @@ class UploadCubit extends Cubit { final tooLargeFiles = [ for (final file in files) - if (await file.length() > sizeLimit) file.name + if (file.size > sizeLimit) file.name ]; if (tooLargeFiles.isNotEmpty) { @@ -171,13 +244,14 @@ class UploadCubit extends Cubit { return; } - final uploadPlan = await _uploadPlanUtils.xfilesToUploadPlan( - folderEntry: _targetFolder, + final uploadPlan = await _uploadPlanUtils.filesToUploadPlan( + targetFolder: _targetFolder, targetDrive: _targetDrive, files: files, cipherKey: profile.cipherKey, wallet: profile.wallet, conflictingFiles: conflictingFiles, + foldersByPath: foldersByPath, ); final costEstimate = await CostEstimate.create( @@ -242,7 +316,7 @@ class UploadCubit extends Cubit { } // Upload V2 Files - for (final uploadHandle in uploadPlan.v2FileUploadHandles.values) { + for (final uploadHandle in uploadPlan.fileV2UploadHandles.values) { await uploadHandle.prepareAndSignTransactions( arweaveService: _arweave, wallet: profile.wallet, @@ -258,18 +332,18 @@ class UploadCubit extends Cubit { } uploadHandle.dispose(); } - unawaited(_profileCubit.refreshBalance()); emit(UploadComplete()); } void _removeFilesWithFileNameConflicts() { - files.removeWhere((element) => conflictingFiles.containsKey(element.name)); + files.removeWhere((file) => conflictingFiles + .containsKey(file.path.isEmpty ? file.name : file.path)); } void _removeFilesWithFolderNameConflicts() { - files.removeWhere((element) => conflictingFolders.contains(element.name)); + files.removeWhere((file) => conflictingFolders.contains(file.name)); } @override diff --git a/lib/blocs/upload/bundle_upload_handle.dart b/lib/blocs/upload/upload_handles/bundle_upload_handle.dart similarity index 62% rename from lib/blocs/upload/bundle_upload_handle.dart rename to lib/blocs/upload/upload_handles/bundle_upload_handle.dart index 35c9efd3b7..5b23ebf56b 100644 --- a/lib/blocs/upload/bundle_upload_handle.dart +++ b/lib/blocs/upload/upload_handles/bundle_upload_handle.dart @@ -1,32 +1,35 @@ -import 'package:ardrive/blocs/upload/data_item_upload_handle.dart'; -import 'package:ardrive/blocs/upload/upload_handle.dart'; +import 'package:ardrive/blocs/upload/upload_handles/file_data_item_upload_handle.dart'; +import 'package:ardrive/blocs/upload/upload_handles/folder_data_item_upload_handle.dart'; +import 'package:ardrive/blocs/upload/upload_handles/upload_handle.dart'; import 'package:ardrive/entities/entities.dart'; -import 'package:ardrive/entities/file_entity.dart'; import 'package:ardrive/models/daos/daos.dart'; import 'package:ardrive/services/services.dart'; import 'package:arweave/arweave.dart'; import 'package:moor/moor.dart'; class BundleUploadHandle implements UploadHandle { - final List dataItemUploadHandles; + final List fileDataItemUploadHandles; + final List folderDataItemUploadHandles; late Transaction bundleTx; late Iterable fileEntities; BundleUploadHandle._create({ - required this.dataItemUploadHandles, + this.fileDataItemUploadHandles = const [], + this.folderDataItemUploadHandles = const [], this.size = 0, }) { - fileEntities = dataItemUploadHandles.map((item) => item.entity); + fileEntities = fileDataItemUploadHandles.map((item) => item.entity); } static Future create({ - required List dataItemUploadHandles, + List fileDataItemUploadHandles = const [], + List folderDataItemUploadHandles = const [], }) async { final bundle = BundleUploadHandle._create( - dataItemUploadHandles: dataItemUploadHandles, + fileDataItemUploadHandles: fileDataItemUploadHandles, + folderDataItemUploadHandles: folderDataItemUploadHandles, ); - bundle.size = await bundle.computeBundleSize(); return bundle; } @@ -46,7 +49,12 @@ class BundleUploadHandle implements UploadHandle { required PstService pstService, required Wallet wallet, }) async { - final bundle = await DataBundle.fromHandles(handles: dataItemUploadHandles); + final bundle = await DataBundle.fromHandles( + handles: List.castFrom( + fileDataItemUploadHandles) + + List.castFrom( + folderDataItemUploadHandles), + ); // Create bundle tx bundleTx = await arweaveService.prepareDataBundleTxFromBlob( bundle.blob, @@ -60,8 +68,10 @@ class BundleUploadHandle implements UploadHandle { ..setTarget(await pstService.getWeightedPstHolder()) ..setQuantity(bundleTip); await bundleTx.sign(wallet); - - dataItemUploadHandles.forEach((file) async { + folderDataItemUploadHandles.forEach((folder) async { + await folder.writeFolderToDatabase(driveDao: driveDao); + }); + fileDataItemUploadHandles.forEach((file) async { await file.writeFileEntityToDatabase( bundledInTxId: bundleTx.id, driveDao: driveDao); }); @@ -84,9 +94,12 @@ class BundleUploadHandle implements UploadHandle { Future computeBundleSize() async { final fileSizes = []; - for (var item in dataItemUploadHandles) { + for (var item in fileDataItemUploadHandles) { fileSizes.add(await item.estimateDataItemSizes()); } + for (var item in folderDataItemUploadHandles) { + fileSizes.add(item.size); + } var size = 0; // Add data item binary size size += fileSizes.reduce((value, element) => value + element); diff --git a/lib/blocs/upload/data_item_upload_handle.dart b/lib/blocs/upload/upload_handles/file_data_item_upload_handle.dart similarity index 87% rename from lib/blocs/upload/data_item_upload_handle.dart rename to lib/blocs/upload/upload_handles/file_data_item_upload_handle.dart index cc187b665e..63eeac4aa2 100644 --- a/lib/blocs/upload/data_item_upload_handle.dart +++ b/lib/blocs/upload/upload_handles/file_data_item_upload_handle.dart @@ -1,6 +1,7 @@ import 'dart:convert'; -import 'package:ardrive/blocs/upload/upload_handle.dart'; +import 'package:ardrive/blocs/upload/models/upload_file.dart'; +import 'package:ardrive/blocs/upload/upload_handles/upload_handle.dart'; import 'package:ardrive/entities/entities.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; @@ -8,13 +9,15 @@ import 'package:ardrive/utils/bundles/fake_tags.dart'; import 'package:arweave/arweave.dart'; import 'package:arweave/utils.dart'; import 'package:cryptography/cryptography.dart'; -import 'package:file_selector/file_selector.dart'; import 'package:moor/moor.dart'; import 'package:package_info_plus/package_info_plus.dart'; -class DataItemUploadHandle implements UploadHandle, DataItemHandle { +// Number of data items returned by this handle +const fileDataItemEntityCount = 2; + +class FileDataItemUploadHandle implements UploadHandle, DataItemHandle { final FileEntity entity; - final XFile file; + final UploadFile file; final String path; final SecretKey? driveKey; final SecretKey? fileKey; @@ -39,7 +42,7 @@ class DataItemUploadHandle implements UploadHandle, DataItemHandle { ArweaveService arweave; Wallet wallet; - DataItemUploadHandle({ + FileDataItemUploadHandle({ required this.entity, required this.path, required this.file, @@ -146,9 +149,12 @@ class DataItemUploadHandle implements UploadHandle, DataItemHandle { } @override - Future> createDataItemsFromFileHandle() async { + Future> getDataItems() async { final dataItems = await prepareAndSignDataItems(); - // Remove file data references return dataItems; } + + // Returning a static count here to save memory and avoid any unneccessary data duplication + @override + int get dataItemCount => fileDataItemEntityCount; } diff --git a/lib/blocs/upload/file_upload_handle.dart b/lib/blocs/upload/upload_handles/file_v2_upload_handle.dart similarity index 93% rename from lib/blocs/upload/file_upload_handle.dart rename to lib/blocs/upload/upload_handles/file_v2_upload_handle.dart index a1af04d596..93c624f86c 100644 --- a/lib/blocs/upload/file_upload_handle.dart +++ b/lib/blocs/upload/upload_handles/file_v2_upload_handle.dart @@ -1,18 +1,18 @@ import 'dart:convert'; -import 'package:ardrive/blocs/upload/upload_handle.dart'; +import 'package:ardrive/blocs/upload/models/upload_file.dart'; +import 'package:ardrive/blocs/upload/upload_handles/upload_handle.dart'; import 'package:ardrive/entities/entities.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; import 'package:arweave/arweave.dart'; import 'package:cryptography/cryptography.dart'; -import 'package:file_selector/file_selector.dart'; import 'package:moor/moor.dart'; import 'package:package_info_plus/package_info_plus.dart'; -class FileUploadHandle implements UploadHandle { +class FileV2UploadHandle implements UploadHandle { final FileEntity entity; - final XFile file; + final UploadFile file; final String path; final SecretKey? driveKey; final SecretKey? fileKey; @@ -34,7 +34,7 @@ class FileUploadHandle implements UploadHandle { late Transaction entityTx; late Transaction dataTx; - FileUploadHandle({ + FileV2UploadHandle({ required this.entity, required this.path, required this.file, diff --git a/lib/blocs/upload/upload_handles/folder_data_item_upload_handle.dart b/lib/blocs/upload/upload_handles/folder_data_item_upload_handle.dart new file mode 100644 index 0000000000..b0ff656a9a --- /dev/null +++ b/lib/blocs/upload/upload_handles/folder_data_item_upload_handle.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; + +import 'package:ardrive/blocs/upload/models/web_folder.dart'; +import 'package:ardrive/blocs/upload/upload_handles/upload_handle.dart'; +import 'package:ardrive/entities/folder_entity.dart'; +import 'package:ardrive/entities/string_types.dart'; +import 'package:ardrive/models/models.dart'; +import 'package:ardrive/services/services.dart'; +import 'package:arweave/arweave.dart'; +import 'package:cryptography/cryptography.dart'; + +// Number of data items returned by this handle +const folderDataItemEntityCount = 1; + +class FolderDataItemUploadHandle implements UploadHandle, DataItemHandle { + final WebFolder folder; + final DriveID targetDriveId; + final SecretKey? driveKey; + + /// The size of the file before it was encoded/encrypted for upload. + @override + int get size => jsonEncode( + FolderEntity( + id: folder.id, + driveId: targetDriveId, + parentFolderId: folder.parentFolderId, + name: folder.name, + ).toJson(), + ).codeUnits.length; + + /// The size of the file that has been uploaded, not accounting for the file encoding/encryption overhead. + @override + int get uploadedSize => (size * uploadProgress).round(); + + bool get isPrivate => driveKey != null; + + @override + double uploadProgress = 0; + + late DataItem folderEntityTx; + late FolderEntity folderEntity; + + ArweaveService arweave; + Wallet wallet; + + FolderDataItemUploadHandle({ + required this.folder, + required this.arweave, + required this.wallet, + required this.targetDriveId, + this.driveKey, + }); + + Future prepareAndSignFolderDataItem() async { + folderEntity = FolderEntity( + id: folder.id, + driveId: targetDriveId, + parentFolderId: folder.parentFolderId, + name: folder.name, + ); + + folderEntityTx = await arweave.prepareEntityDataItem( + folderEntity, + wallet, + driveKey, + ); + await folderEntityTx.sign(wallet); + } + + Future writeFolderToDatabase({ + required DriveDao driveDao, + }) async { + await driveDao.transaction(() async { + await driveDao.createFolder( + driveId: targetDriveId, + parentFolderId: folder.parentFolderId, + folderName: folder.name, + path: folder.path, + folderId: folder.id, + ); + + folderEntity.txId = folderEntityTx.id; + + await driveDao.insertFolderRevision( + folderEntity.toRevisionCompanion( + performedAction: RevisionAction.create, + ), + ); + }); + } + + // Returning a static count here to save memory and avoid any unneccessary data duplication + @override + int get dataItemCount => folderDataItemEntityCount; + + @override + Future> getDataItems() async { + await prepareAndSignFolderDataItem(); + return [folderEntityTx]; + } +} diff --git a/lib/blocs/upload/upload_handles/handles.dart b/lib/blocs/upload/upload_handles/handles.dart new file mode 100644 index 0000000000..cbdf3e3ad6 --- /dev/null +++ b/lib/blocs/upload/upload_handles/handles.dart @@ -0,0 +1,5 @@ +export 'bundle_upload_handle.dart'; +export 'file_data_item_upload_handle.dart'; +export 'file_v2_upload_handle.dart'; +export 'folder_data_item_upload_handle.dart'; +export 'upload_handle.dart'; diff --git a/lib/blocs/upload/upload_handle.dart b/lib/blocs/upload/upload_handles/upload_handle.dart similarity index 100% rename from lib/blocs/upload/upload_handle.dart rename to lib/blocs/upload/upload_handles/upload_handle.dart diff --git a/lib/blocs/upload/upload_plan.dart b/lib/blocs/upload/upload_plan.dart deleted file mode 100644 index f6ba366c8c..0000000000 --- a/lib/blocs/upload/upload_plan.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:ardrive/blocs/upload/bundle_upload_handle.dart'; -import 'package:ardrive/utils/bundles/next_fit_bundle_packer.dart'; - -import 'data_item_upload_handle.dart'; -import 'file_upload_handle.dart'; - -final bundleSizeLimit = 503316480; -final maxBundleDataItemCount = 500; -final maxFilesPerBundle = maxBundleDataItemCount ~/ 2; - -class UploadPlan { - /// A map of [FileUploadHandle]s keyed by their respective file's id. - late Map v2FileUploadHandles; - - final List bundleUploadHandles = []; - - UploadPlan._create({ - required this.v2FileUploadHandles, - }); - - static Future create({ - required Map v2FileUploadHandles, - required Map dataItemUploadHandles, - }) async { - final bundle = UploadPlan._create( - v2FileUploadHandles: v2FileUploadHandles, - ); - await bundle.createBundleHandlesFromDataItemHandles(dataItemUploadHandles); - return bundle; - } - - Future createBundleHandlesFromDataItemHandles( - Map dataItemUploadHandles) async { - // NOTE: Using maxFilesPerBundle since FileUploadHandles have 2 data items - final bundleItems = await NextFitBundlePacker( - maxBundleSize: bundleSizeLimit, - maxDataItemCount: maxFilesPerBundle, - ).packItems(dataItemUploadHandles.values.toList()); - for (var uploadHandles in bundleItems) { - final bundleToUpload = await BundleUploadHandle.create( - dataItemUploadHandles: List.from(uploadHandles), - ); - bundleUploadHandles.add(bundleToUpload); - uploadHandles.clear(); - } - dataItemUploadHandles.clear(); - } -} diff --git a/lib/components/app_drawer/app_drawer.dart b/lib/components/app_drawer/app_drawer.dart index 588bf7e1ad..6cfcb96140 100644 --- a/lib/components/app_drawer/app_drawer.dart +++ b/lib/components/app_drawer/app_drawer.dart @@ -1,15 +1,14 @@ import 'package:ardrive/blocs/blocs.dart'; +import 'package:ardrive/components/app_drawer/drive_list_tile.dart'; +import 'package:ardrive/components/components.dart'; import 'package:ardrive/misc/resources.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../utils/app_localizations_wrapper.dart'; -import '../components.dart'; -import 'drive_list_tile.dart'; - class AppDrawer extends StatelessWidget { const AppDrawer({ Key? key, @@ -210,15 +209,32 @@ class AppDrawer extends StatelessWidget { PopupMenuDivider(), PopupMenuItem( enabled: state.hasWritePermissions, - value: (context) => promptToUploadFile( + value: (context) => promptToUpload( context, driveId: state.currentDrive.id, folderId: state.folderInView.folder.id, + isFolderUpload: false, ), child: ListTile( enabled: state.hasWritePermissions, - title: - Text(appLocalizationsOf(context).uploadFiles), + title: Text( + appLocalizationsOf(context).uploadFiles, + ), + ), + ), + PopupMenuItem( + enabled: state.hasWritePermissions, + value: (context) => promptToUpload( + context, + driveId: state.currentDrive.id, + folderId: state.folderInView.folder.id, + isFolderUpload: true, + ), + child: ListTile( + enabled: state.hasWritePermissions, + title: Text( + appLocalizationsOf(context).uploadFolder, + ), ), ), PopupMenuDivider(), diff --git a/lib/components/app_drawer/drive_list_tile.dart b/lib/components/app_drawer/drive_list_tile.dart index 5e0cf848ec..ecbefba99d 100644 --- a/lib/components/app_drawer/drive_list_tile.dart +++ b/lib/components/app_drawer/drive_list_tile.dart @@ -1,9 +1,8 @@ import 'package:ardrive/models/models.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; -import '../../../utils/app_localizations_wrapper.dart'; - class DriveListTile extends StatelessWidget { final Drive drive; final bool selected; diff --git a/lib/components/create_manifest_form.dart b/lib/components/create_manifest_form.dart index 5bd05bcffc..a2c63d061e 100644 --- a/lib/components/create_manifest_form.dart +++ b/lib/components/create_manifest_form.dart @@ -7,14 +7,14 @@ import 'package:ardrive/misc/misc.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; -import 'package:filesize/filesize.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; +import 'package:ardrive/utils/filesize.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:reactive_forms/reactive_forms.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components.dart'; Future promptToCreateManifest(BuildContext context, diff --git a/lib/components/csv_export_dialog.dart b/lib/components/csv_export_dialog.dart index d94054085f..882312d45e 100644 --- a/lib/components/csv_export_dialog.dart +++ b/lib/components/csv_export_dialog.dart @@ -5,12 +5,12 @@ import 'package:ardrive/blocs/data_export/data_export_cubit.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pedantic/pedantic.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components.dart'; Future promptToExportCSVData({ diff --git a/lib/components/drive_attach_form.dart b/lib/components/drive_attach_form.dart index 230912de3b..810378fa5a 100644 --- a/lib/components/drive_attach_form.dart +++ b/lib/components/drive_attach_form.dart @@ -5,6 +5,7 @@ import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/user_interaction_wrapper.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:cryptography/cryptography.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,7 +13,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:meta/meta.dart'; import 'package:reactive_forms/reactive_forms.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components.dart'; Future attachDrive({ diff --git a/lib/components/drive_create_form.dart b/lib/components/drive_create_form.dart index 0a323d8160..e688393119 100644 --- a/lib/components/drive_create_form.dart +++ b/lib/components/drive_create_form.dart @@ -4,11 +4,11 @@ import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/congestion_warning_wrapper.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:reactive_forms/reactive_forms.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components.dart'; Future promptToCreateDrive(BuildContext context) => diff --git a/lib/components/drive_rename_form.dart b/lib/components/drive_rename_form.dart index a630e75d1d..4b7faf531e 100644 --- a/lib/components/drive_rename_form.dart +++ b/lib/components/drive_rename_form.dart @@ -5,11 +5,11 @@ import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/congestion_warning_wrapper.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:reactive_forms/reactive_forms.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components.dart'; Future promptToRenameDrive( diff --git a/lib/components/drive_share_dialog.dart b/lib/components/drive_share_dialog.dart index 79376aad3b..245f85d19b 100644 --- a/lib/components/drive_share_dialog.dart +++ b/lib/components/drive_share_dialog.dart @@ -1,11 +1,11 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components.dart'; Future promptToShareDrive({ diff --git a/lib/components/error_dialog.dart b/lib/components/error_dialog.dart index e668a415ca..5d29e27221 100644 --- a/lib/components/error_dialog.dart +++ b/lib/components/error_dialog.dart @@ -1,6 +1,6 @@ +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components.dart'; Future showErrorDialog({ diff --git a/lib/components/file_download_dialog.dart b/lib/components/file_download_dialog.dart index d5f9167271..a1adc88c14 100644 --- a/lib/components/file_download_dialog.dart +++ b/lib/components/file_download_dialog.dart @@ -5,14 +5,14 @@ import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; +import 'package:ardrive/utils/filesize.dart'; import 'package:cryptography/cryptography.dart'; import 'package:file_selector/file_selector.dart'; -import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pedantic/pedantic.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components.dart'; Future promptToDownloadProfileFile({ diff --git a/lib/components/file_share_dialog.dart b/lib/components/file_share_dialog.dart index 8da522d336..28c5f4f227 100644 --- a/lib/components/file_share_dialog.dart +++ b/lib/components/file_share_dialog.dart @@ -1,11 +1,11 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components.dart'; Future promptToShareFile({ diff --git a/lib/components/folder_create_form.dart b/lib/components/folder_create_form.dart index e45ce805e5..d6ce04dd88 100644 --- a/lib/components/folder_create_form.dart +++ b/lib/components/folder_create_form.dart @@ -4,11 +4,11 @@ import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/congestion_warning_wrapper.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:reactive_forms/reactive_forms.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components.dart'; Future promptToCreateFolder( diff --git a/lib/components/fs_entry_move_form.dart b/lib/components/fs_entry_move_form.dart index b0eb30c772..140ba79a53 100644 --- a/lib/components/fs_entry_move_form.dart +++ b/lib/components/fs_entry_move_form.dart @@ -3,10 +3,10 @@ import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/congestion_warning_wrapper.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components.dart'; Future promptToMoveFolder( diff --git a/lib/components/fs_entry_rename_form.dart b/lib/components/fs_entry_rename_form.dart index 2a0e92b051..9246eae503 100644 --- a/lib/components/fs_entry_rename_form.dart +++ b/lib/components/fs_entry_rename_form.dart @@ -4,11 +4,11 @@ import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/congestion_warning_wrapper.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:reactive_forms/reactive_forms.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components.dart'; Future promptToRenameFolder( diff --git a/lib/components/ghost_fixer_form.dart b/lib/components/ghost_fixer_form.dart index c2a3e3e0d7..3f2062eeb6 100644 --- a/lib/components/ghost_fixer_form.dart +++ b/lib/components/ghost_fixer_form.dart @@ -4,12 +4,12 @@ import 'package:ardrive/l11n/l11n.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:reactive_forms/reactive_forms.dart'; import 'package:responsive_builder/responsive_builder.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components.dart'; Future promptToReCreateFolder(BuildContext context, diff --git a/lib/components/profile_overlay.dart b/lib/components/profile_overlay.dart index fbf76374dd..4a57aed703 100644 --- a/lib/components/profile_overlay.dart +++ b/lib/components/profile_overlay.dart @@ -1,12 +1,11 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:arweave/utils.dart' as utils; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../utils/app_localizations_wrapper.dart'; - class ProfileOverlay extends StatelessWidget { @override Widget build(BuildContext context) => Column( diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index 45061ff258..ca6fe921d8 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -1,50 +1,70 @@ +import 'dart:html'; + import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/upload/enums/conflicting_files_actions.dart'; +import 'package:ardrive/blocs/upload/models/web_file.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/congestion_warning_wrapper.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; +import 'package:ardrive/utils/filesize.dart'; import 'package:ardrive/utils/upload_plan_utils.dart'; -import 'package:file_selector/file_selector.dart'; -import 'package:filesize/filesize.dart'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components.dart'; -Future promptToUploadFile( +Future promptToUpload( BuildContext context, { required String driveId, required String folderId, + required bool isFolderUpload, }) async { - final selectedFiles = await openFiles(); - if (selectedFiles.isEmpty) { - return; - } - await showCongestionDependentModalDialog( - context, - () => showDialog( - context: context, - builder: (_) => BlocProvider( - create: (context) => UploadCubit( - uploadPlanUtils: UploadPlanUtils( + final uploadInput = FileUploadInputElement() + ..setAttribute( + isFolderUpload ? 'webkitdirectory' : 'webkitEntries', 'multiple'); + uploadInput.click(); +// Create and click upload input element + + uploadInput.onChange.listen((e) async { + // read file content as dataURL + final files = uploadInput.files; + if (files == null) { + return; + } + final selectedFiles = files.map((file) { + return WebFile(file, folderId); + }).toList(); + if (selectedFiles.isEmpty) { + return; + } + await showCongestionDependentModalDialog( + context, + () => showDialog( + context: context, + builder: (_) => BlocProvider( + create: (context) => UploadCubit( + uploadPlanUtils: UploadPlanUtils( + arweave: context.read(), + driveDao: context.read(), + ), + driveId: driveId, + folderId: folderId, + files: selectedFiles, + profileCubit: context.read(), arweave: context.read(), + pst: context.read(), driveDao: context.read(), - ), - driveId: driveId, - folderId: folderId, - files: selectedFiles, - profileCubit: context.read(), - arweave: context.read(), - pst: context.read(), - driveDao: context.read(), - )..startUploadPreparation(), - child: UploadForm(), + uploadFolders: isFolderUpload, + )..startUploadPreparation(), + child: UploadForm(), + ), + barrierDismissible: false, ), - barrierDismissible: false, - ), - ); + ); + }); } class UploadForm extends StatelessWidget { @@ -83,10 +103,13 @@ class UploadForm extends StatelessWidget { Text(appLocalizationsOf(context).conflictingFiles), const SizedBox(height: 8), ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 320), - child: SingleChildScrollView( - child: - Text(state.conflictingFileNames.join(', ')))), + constraints: const BoxConstraints(maxHeight: 320), + child: SingleChildScrollView( + child: Text( + state.conflictingFileNames.join(', \n'), + ), + ), + ), ], ), ), @@ -129,10 +152,13 @@ class UploadForm extends StatelessWidget { Text(appLocalizationsOf(context).conflictingFiles), const SizedBox(height: 8), ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 320), - child: SingleChildScrollView( - child: - Text(state.conflictingFileNames.join(', ')))), + constraints: const BoxConstraints(maxHeight: 320), + child: SingleChildScrollView( + child: Text( + state.conflictingFileNames.join(', \n'), + ), + ), + ), ], ), ), @@ -234,7 +260,7 @@ class UploadForm extends StatelessWidget { .map((e) => e.numberOfFiles) .reduce((value, element) => value += element) : 0; - final numberOfV2Files = state.uploadPlan.v2FileUploadHandles.length; + final numberOfV2Files = state.uploadPlan.fileV2UploadHandles.length; return AppDialog( title: appLocalizationsOf(context) .uploadNFiles(numberOfFilesInBundles + numberOfV2Files), @@ -251,7 +277,7 @@ class UploadForm extends StatelessWidget { shrinkWrap: true, children: [ for (final file in state - .uploadPlan.v2FileUploadHandles.values) ...{ + .uploadPlan.fileV2UploadHandles.values) ...{ ListTile( contentPadding: EdgeInsets.zero, title: Text(file.entity.name!), @@ -352,7 +378,7 @@ class UploadForm extends StatelessWidget { .map((e) => e.numberOfFiles) .reduce((value, element) => value += element) : 0; - final numberOfV2Files = state.uploadPlan.v2FileUploadHandles.length; + final numberOfV2Files = state.uploadPlan.fileV2UploadHandles.length; return AppDialog( dismissable: false, title: appLocalizationsOf(context) @@ -366,7 +392,7 @@ class UploadForm extends StatelessWidget { shrinkWrap: true, children: [ for (final file - in state.uploadPlan.v2FileUploadHandles.values) ...{ + in state.uploadPlan.fileV2UploadHandles.values) ...{ ListTile( contentPadding: EdgeInsets.zero, title: Text(file.entity.name!), diff --git a/lib/components/wallet_switch_dialog.dart b/lib/components/wallet_switch_dialog.dart index 9bad0422f4..4e2470d520 100644 --- a/lib/components/wallet_switch_dialog.dart +++ b/lib/components/wallet_switch_dialog.dart @@ -1,11 +1,10 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/components/app_dialog.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/html/html_util.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../utils/app_localizations_wrapper.dart'; - class WalletSwitchDialog extends StatelessWidget { final bool fromAuthPage; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2c27e97f42..2973682028 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -291,6 +291,10 @@ "@fileSize": { "description": "The size of a file" }, + "size": "Size", + "@size": { + "description": "The size of a file, folder or drive" + }, "lastUpdated": "Last updated", "@lastUpdated": { "description": "The timestamp of the transaction" @@ -525,6 +529,10 @@ "@uploadFiles": { "description": "Upload files to the Weave" }, + "uploadFolder": "Upload folder", + "@uploadFolder": { + "description": "Upload a folder and every subfolder alongside files inside them" + }, "newDrive": "New drive", "@newDrive": { "description": "Create a new drive entity" diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 02de5f1fc3..f45c4ae70f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -525,6 +525,10 @@ "@uploadFiles": { "description": "Upload files to the Weave" }, + "uploadFolder": "Cargar carpeta", + "@uploadFolder": { + "description": "Upload a folder and every subfolder alongside files inside them" + }, "newDrive": "Nueva unidad", "@newDrive": { "description": "Create a new drive entity" diff --git a/lib/models/daos/drive_dao/drive_dao.dart b/lib/models/daos/drive_dao/drive_dao.dart index 32488d19ce..e919a918b5 100644 --- a/lib/models/daos/drive_dao/drive_dao.dart +++ b/lib/models/daos/drive_dao/drive_dao.dart @@ -311,23 +311,22 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { /// Create a new folder entry. /// Returns the id of the created folder. - Future createFolder({ - required String driveId, - String? parentFolderId, + Future createFolder({ + required DriveID driveId, + FolderID? parentFolderId, + FolderID? folderId, required String folderName, required String path, }) async { - final id = _uuid.v4(); - - await into(folderEntries).insert( - FolderEntriesCompanion.insert( - id: id, - driveId: driveId, - parentFolderId: Value(parentFolderId), - name: folderName, - path: path, - ), + final id = folderId ?? _uuid.v4(); + final folderEntriesCompanion = FolderEntriesCompanion.insert( + id: id, + driveId: driveId, + parentFolderId: Value(parentFolderId), + name: folderName, + path: path, ); + await into(folderEntries).insert(folderEntriesCompanion); return id; } diff --git a/lib/models/daos/drive_dao/folder_node.dart b/lib/models/daos/drive_dao/folder_node.dart index 1196d828df..509b0171e4 100644 --- a/lib/models/daos/drive_dao/folder_node.dart +++ b/lib/models/daos/drive_dao/folder_node.dart @@ -42,6 +42,18 @@ class FolderNode { return totalFiles; } + int computeFolderSize() { + var totalSize = files.isNotEmpty + ? files.values.map((file) => file.size).reduce( + (value, fileSize) => value + fileSize, + ) + : 0; + for (var subfolder in subfolders) { + totalSize += subfolder.computeFolderSize(); + } + return totalSize; + } + // TODO: maxDepth slider in story ticket PE-1069 List getRecursiveFiles(/*{maxDepth = 2000000}*/) { // if (maxDepth == -1) { diff --git a/lib/models/daos/profile_dao.dart b/lib/models/daos/profile_dao.dart index 9ee965ef9f..8b69245dd7 100644 --- a/lib/models/daos/profile_dao.dart +++ b/lib/models/daos/profile_dao.dart @@ -1,14 +1,13 @@ import 'dart:convert'; import 'package:ardrive/entities/profileTypes.dart'; +import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/arconnect/arconnect_wallet.dart'; import 'package:ardrive/services/services.dart'; import 'package:arweave/arweave.dart'; import 'package:cryptography/cryptography.dart'; import 'package:moor/moor.dart'; -import '../database/database.dart'; - part 'profile_dao.g.dart'; const keyByteLength = 256 ~/ 8; diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 3e2e0c2dc0..6b5ea92df7 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -1,6 +1,6 @@ +import 'package:ardrive/models/daos/daos.dart'; import 'package:moor/moor.dart'; -import '../daos/daos.dart'; import 'unsupported.dart' if (dart.library.html) 'web.dart' if (dart.library.io) 'ffi.dart'; diff --git a/lib/pages/app_router_delegate.dart b/lib/pages/app_router_delegate.dart index f40e8e9bb7..2f09cc8750 100644 --- a/lib/pages/app_router_delegate.dart +++ b/lib/pages/app_router_delegate.dart @@ -1,3 +1,4 @@ +import 'package:ardrive/app_shell.dart'; import 'package:ardrive/blocs/activity/activity_cubit.dart'; import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/components/components.dart'; @@ -5,13 +6,11 @@ import 'package:ardrive/entities/constants.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/pages.dart'; import 'package:ardrive/services/services.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:cryptography/cryptography.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../utils/app_localizations_wrapper.dart'; -import '../app_shell.dart'; - class AppRouterDelegate extends RouterDelegate with ChangeNotifier, PopNavigatorRouterDelegateMixin { bool signingIn = false; diff --git a/lib/pages/congestion_warning_wrapper.dart b/lib/pages/congestion_warning_wrapper.dart index defe6627aa..69df53435f 100644 --- a/lib/pages/congestion_warning_wrapper.dart +++ b/lib/pages/congestion_warning_wrapper.dart @@ -3,11 +3,10 @@ import 'package:ardrive/misc/misc.dart'; import 'package:ardrive/pages/user_interaction_wrapper.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../utils/app_localizations_wrapper.dart'; - Future showCongestionDependentModalDialog( BuildContext context, Function() showAppDialog) async { final warnAboutCongestion = diff --git a/lib/pages/drive_detail/components/custom_paginated_data_table.dart b/lib/pages/drive_detail/components/custom_paginated_data_table.dart index 523488a2a3..b848eac42b 100644 --- a/lib/pages/drive_detail/components/custom_paginated_data_table.dart +++ b/lib/pages/drive_detail/components/custom_paginated_data_table.dart @@ -1,11 +1,10 @@ import 'dart:math' as math; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:intersperse/src/intersperse_extensions.dart'; -import '../../../utils/app_localizations_wrapper.dart'; - class CustomPaginatedDataTable extends StatefulWidget { CustomPaginatedDataTable({ Key? key, diff --git a/lib/pages/drive_detail/components/drive_file_drop_zone.dart b/lib/pages/drive_detail/components/drive_file_drop_zone.dart index 47191c0c5e..06a97d43cc 100644 --- a/lib/pages/drive_detail/components/drive_file_drop_zone.dart +++ b/lib/pages/drive_detail/components/drive_file_drop_zone.dart @@ -1,16 +1,16 @@ import 'package:ardrive/blocs/blocs.dart'; +import 'package:ardrive/blocs/upload/models/io_file.dart'; import 'package:ardrive/components/upload_form.dart'; import 'package:ardrive/models/daos/drive_dao/drive_dao.dart'; +import 'package:ardrive/pages/congestion_warning_wrapper.dart'; import 'package:ardrive/services/services.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/upload_plan_utils.dart'; import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dropzone/flutter_dropzone.dart'; -import '../../../utils/app_localizations_wrapper.dart'; -import '../../congestion_warning_wrapper.dart'; - class DriveFileDropZone extends StatefulWidget { final String driveId; final String folderId; @@ -84,7 +84,7 @@ class _DriveFileDropZoneState extends State { lastModified: fileLastModified, length: fileLength, ); - final selectedFiles = [fileToUpload]; + final selectedFiles = [await IOFile.fromXFile(fileToUpload, folderId)]; try { //This is the only way to know whether the dropped file is a folder await fileToUpload.readAsBytes(); diff --git a/lib/pages/drive_detail/components/fs_entry_side_sheet.dart b/lib/pages/drive_detail/components/fs_entry_side_sheet.dart index 43f7da25cb..51d2090332 100644 --- a/lib/pages/drive_detail/components/fs_entry_side_sheet.dart +++ b/lib/pages/drive_detail/components/fs_entry_side_sheet.dart @@ -104,6 +104,18 @@ class FsEntrySideSheet extends StatelessWidget { ), ), ]), + DataRow(cells: [ + DataCell(Text(appLocalizationsOf(context).size)), + DataCell( + Align( + alignment: Alignment.centerRight, + child: Text( + filesize((state).rootFolderTree.computeFolderSize()), + textAlign: TextAlign.end, + ), + ), + ), + ]), DataRow(cells: [ DataCell(Text(appLocalizationsOf(context).driveID)), DataCell( @@ -144,6 +156,18 @@ class FsEntrySideSheet extends StatelessWidget { ), ), ]), + DataRow(cells: [ + DataCell(Text(appLocalizationsOf(context).size)), + DataCell( + Align( + alignment: Alignment.centerRight, + child: Text( + filesize(state.entry.computeFolderSize()), + textAlign: TextAlign.end, + ), + ), + ), + ]), DataRow(cells: [ DataCell(Text(appLocalizationsOf(context).folderID)), DataCell( diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 4779356b6e..b1b55c81b8 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -13,8 +13,9 @@ import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/congestion_warning_wrapper.dart'; import 'package:ardrive/pages/drive_detail/components/drive_file_drop_zone.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; +import 'package:ardrive/utils/filesize.dart'; import 'package:ardrive/utils/num_to_string_parsers.dart'; -import 'package:filesize/filesize.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -26,7 +27,6 @@ import 'package:responsive_builder/responsive_builder.dart'; import 'package:timeago/timeago.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'components/custom_paginated_data_table.dart'; part 'components/drive_detail_actions_row.dart'; diff --git a/lib/pages/no_drives/no_drives_page.dart b/lib/pages/no_drives/no_drives_page.dart index c89cdaa7aa..1e1c85bd08 100644 --- a/lib/pages/no_drives/no_drives_page.dart +++ b/lib/pages/no_drives/no_drives_page.dart @@ -1,7 +1,6 @@ +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; -import '../../../utils/app_localizations_wrapper.dart'; - /// A page letting the user know that they have no personal or attached drives /// with a call to action for them to add new ones. class NoDrivesPage extends StatelessWidget { diff --git a/lib/pages/profile_auth/components/profile_auth_add_screen.dart b/lib/pages/profile_auth/components/profile_auth_add_screen.dart index 555cc65a79..5ebf9e804a 100644 --- a/lib/pages/profile_auth/components/profile_auth_add_screen.dart +++ b/lib/pages/profile_auth/components/profile_auth_add_screen.dart @@ -1,12 +1,12 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/l11n/l11n.dart'; import 'package:ardrive/misc/misc.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:reactive_forms/reactive_forms.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'profile_auth_shell.dart'; class ProfileAuthAddScreen extends StatelessWidget { diff --git a/lib/pages/profile_auth/components/profile_auth_fail_screen.dart b/lib/pages/profile_auth/components/profile_auth_fail_screen.dart index 387e6f43df..aad842bd9d 100644 --- a/lib/pages/profile_auth/components/profile_auth_fail_screen.dart +++ b/lib/pages/profile_auth/components/profile_auth_fail_screen.dart @@ -1,10 +1,10 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/misc/misc.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/html/html_util.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'profile_auth_shell.dart'; class ProfileAuthFailScreen extends StatelessWidget { diff --git a/lib/pages/profile_auth/components/profile_auth_loading_screen.dart b/lib/pages/profile_auth/components/profile_auth_loading_screen.dart index ec07f8fb28..33b80ad7dd 100644 --- a/lib/pages/profile_auth/components/profile_auth_loading_screen.dart +++ b/lib/pages/profile_auth/components/profile_auth_loading_screen.dart @@ -1,7 +1,7 @@ import 'package:ardrive/misc/misc.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'profile_auth_shell.dart'; class ProfileAuthLoadingScreen extends StatelessWidget { diff --git a/lib/pages/profile_auth/components/profile_auth_onboarding_screen.dart b/lib/pages/profile_auth/components/profile_auth_onboarding_screen.dart index 0eccc475be..2d67282e70 100644 --- a/lib/pages/profile_auth/components/profile_auth_onboarding_screen.dart +++ b/lib/pages/profile_auth/components/profile_auth_onboarding_screen.dart @@ -1,9 +1,9 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/misc/misc.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'profile_auth_shell.dart'; class ProfileAuthOnboarding extends StatefulWidget { diff --git a/lib/pages/profile_auth/components/profile_auth_prompt_wallet_screen.dart b/lib/pages/profile_auth/components/profile_auth_prompt_wallet_screen.dart index e28ac47bad..734bfa092f 100644 --- a/lib/pages/profile_auth/components/profile_auth_prompt_wallet_screen.dart +++ b/lib/pages/profile_auth/components/profile_auth_prompt_wallet_screen.dart @@ -1,11 +1,11 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/misc/misc.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:file_selector/file_selector.dart' as file_selector; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'profile_auth_shell.dart'; class ProfileAuthPromptWalletScreen extends StatelessWidget { diff --git a/lib/pages/profile_auth/components/profile_auth_unlock_screen.dart b/lib/pages/profile_auth/components/profile_auth_unlock_screen.dart index af8a5f688c..0dab5e269b 100644 --- a/lib/pages/profile_auth/components/profile_auth_unlock_screen.dart +++ b/lib/pages/profile_auth/components/profile_auth_unlock_screen.dart @@ -3,11 +3,11 @@ import 'package:ardrive/l11n/l11n.dart'; import 'package:ardrive/misc/misc.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/arweave/arweave.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:reactive_forms/reactive_forms.dart'; -import '../../../utils/app_localizations_wrapper.dart'; import 'profile_auth_fail_screen.dart'; import 'profile_auth_shell.dart'; diff --git a/lib/pages/screen_not_supported/screen_not_supported_page.dart b/lib/pages/screen_not_supported/screen_not_supported_page.dart index f50663587a..cbfb66bc22 100644 --- a/lib/pages/screen_not_supported/screen_not_supported_page.dart +++ b/lib/pages/screen_not_supported/screen_not_supported_page.dart @@ -1,9 +1,8 @@ import 'package:ardrive/misc/misc.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../utils/app_localizations_wrapper.dart'; - class ScreenNotSupportedPage extends StatelessWidget { @override Widget build(BuildContext context) => Material( diff --git a/lib/pages/shared_file/shared_file_page.dart b/lib/pages/shared_file/shared_file_page.dart index 6e310d07a8..627f2b1999 100644 --- a/lib/pages/shared_file/shared_file_page.dart +++ b/lib/pages/shared_file/shared_file_page.dart @@ -2,13 +2,12 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/components/components.dart'; import 'package:ardrive/misc/misc.dart'; import 'package:ardrive/theme/theme.dart'; -import 'package:filesize/filesize.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; +import 'package:ardrive/utils/filesize.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../../utils/app_localizations_wrapper.dart'; - /// [SharedFilePage] displays details of a shared file and controls for downloading and previewing it /// from a parent [SharedFileCubit]. class SharedFilePage extends StatelessWidget { diff --git a/lib/services/arweave/arweave_service.dart b/lib/services/arweave/arweave_service.dart index cb51bbca2d..db28f6ec75 100644 --- a/lib/services/arweave/arweave_service.dart +++ b/lib/services/arweave/arweave_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:ardrive/entities/entities.dart'; +import 'package:ardrive/services/services.dart'; import 'package:artemis/artemis.dart'; import 'package:arweave/arweave.dart'; import 'package:cryptography/cryptography.dart'; @@ -10,8 +11,6 @@ import 'package:moor/moor.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:pedantic/pedantic.dart'; -import '../services.dart'; - const byteCountPerChunk = 262144; // 256 KiB class ArweaveService { @@ -548,7 +547,9 @@ class ArweaveService { /// Creates and signs a [Transaction] representing the provided [DataBundle]. Future prepareDataBundleTx( - DataBundle bundle, Wallet wallet) async { + DataBundle bundle, + Wallet wallet, + ) async { final packageInfo = await PackageInfo.fromPlatform(); final bundleTx = await client.transactions.prepare( diff --git a/lib/services/config/config_service.dart b/lib/services/config/config_service.dart index 01cbc69e4b..e3db5bce51 100644 --- a/lib/services/config/config_service.dart +++ b/lib/services/config/config_service.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:cooky/cooky.dart' as cookie; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -13,7 +14,12 @@ class ConfigService { final environment = kReleaseMode ? 'prod' : 'dev'; final configContent = await rootBundle.loadString('assets/config/$environment.json'); - _config = AppConfig.fromJson(json.decode(configContent)); + + final gatewayCookie = cookie.get('arweaveGatewayUrl'); + + _config = AppConfig.fromJson(gatewayCookie != null + ? {'defaultArweaveGatewayUrl': gatewayCookie} + : json.decode(configContent)); } return _config!; diff --git a/lib/services/crypto/entities.dart b/lib/services/crypto/entities.dart index 62cb7d66c6..39f82e45f7 100644 --- a/lib/services/crypto/entities.dart +++ b/lib/services/crypto/entities.dart @@ -2,12 +2,11 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:ardrive/entities/entities.dart'; +import 'package:ardrive/services/services.dart'; import 'package:arweave/arweave.dart'; import 'package:arweave/utils.dart' as utils; import 'package:cryptography/cryptography.dart' hide Cipher; -import '../services.dart'; - final aesGcm = AesGcm.with256bits(); /// Decrypts the provided transaction details and data into JSON using the provided key. diff --git a/lib/utils/filesize.dart b/lib/utils/filesize.dart new file mode 100644 index 0000000000..08f3dec40d --- /dev/null +++ b/lib/utils/filesize.dart @@ -0,0 +1,64 @@ +import 'dart:math'; + +/// A method returns a human readable string representing a file _size +String filesize(dynamic size, [int round = 2]) { + /** + * [size] in bytes, can be passed as number or as string + * + * the optional parameter [round] specifies the number + * of digits after comma/point (default is 2) + */ + final divider = pow(2, 10); + int _size; + try { + _size = int.parse(size.toString()); + } catch (e) { + throw ArgumentError('Can not parse the size parameter: $e'); + } + + if (_size < divider) { + return '$_size B'; + } + + if (_size < pow(divider, 2) && _size % divider == 0) { + return '${(_size / divider).toStringAsFixed(0)} KiB'; + } + + if (_size < pow(divider, 2)) { + return '${(_size / divider).toStringAsFixed(round)} KiB'; + } + + if (_size < pow(divider, 3) && _size % pow(divider, 2) == 0) { + return '${(_size / pow(divider, 2)).toStringAsFixed(0)} MiB'; + } + + if (_size < pow(divider, 3)) { + return '${(_size / pow(divider, 2)).toStringAsFixed(round)} MiB'; + } + + if (_size < pow(divider, 4) && _size % pow(divider, 3) == 0) { + return '${(_size / pow(divider, 3)).toStringAsFixed(0)} GiB'; + } + + if (_size < pow(divider, 4)) { + return '${(_size / pow(divider, 3)).toStringAsFixed(round)} GiB'; + } + + if (_size < pow(divider, 5) && _size % pow(divider, 4) == 0) { + final num r = _size / pow(divider, 4); + return '${r.toStringAsFixed(0)} TiB'; + } + + if (_size < pow(divider, 5)) { + final num r = _size / pow(divider, 4); + return '${r.toStringAsFixed(round)} TiB'; + } + + if (_size < pow(divider, 6) && _size % pow(divider, 5) == 0) { + final num r = _size / pow(divider, 5); + return '${r.toStringAsFixed(0)} PiB'; + } else { + final num r = _size / pow(divider, 5); + return '${r.toStringAsFixed(round)} PiB'; + } +} diff --git a/lib/utils/upload_plan_utils.dart b/lib/utils/upload_plan_utils.dart index 16ca5ff024..b8f283a43f 100644 --- a/lib/utils/upload_plan_utils.dart +++ b/lib/utils/upload_plan_utils.dart @@ -1,65 +1,63 @@ -import 'package:ardrive/blocs/upload/upload_plan.dart'; -import 'package:ardrive/models/daos/daos.dart'; -import 'package:ardrive/models/drive.dart'; -import 'package:ardrive/services/arweave/arweave.dart'; +import 'package:ardrive/blocs/upload/models/models.dart'; +import 'package:ardrive/blocs/upload/upload_handles/handles.dart'; +import 'package:ardrive/entities/entities.dart'; +import 'package:ardrive/models/models.dart'; +import 'package:ardrive/services/services.dart'; import 'package:arweave/arweave.dart'; import 'package:cryptography/cryptography.dart'; -import 'package:file_selector/file_selector.dart'; import 'package:mime/mime.dart'; import 'package:uuid/uuid.dart'; -import '../blocs/upload/data_item_upload_handle.dart'; -import '../blocs/upload/file_upload_handle.dart'; -import '../entities/file_entity.dart'; -import '../models/database/database.dart'; -import '../models/enums.dart'; -import '../services/crypto/keys.dart'; - class UploadPlanUtils { - UploadPlanUtils({required this.arweave, required this.driveDao}); + UploadPlanUtils({ + required this.arweave, + required this.driveDao, + }); final ArweaveService arweave; final DriveDao driveDao; final _uuid = Uuid(); - Future xfilesToUploadPlan( - {required List files, - required SecretKey cipherKey, - required Wallet wallet, - required Map conflictingFiles, - required Drive targetDrive, - required FolderEntry folderEntry}) async { - final _dataItemUploadHandles = {}; - final _v2FileUploadHandles = {}; + Future filesToUploadPlan({ + required List files, + required SecretKey cipherKey, + required Wallet wallet, + required Map conflictingFiles, + required Drive targetDrive, + required FolderEntry targetFolder, + Map foldersByPath = const {}, + }) async { + final _fileDataItemUploadHandles = {}; + final _fileV2UploadHandles = {}; + final _folderDataItemUploadHandles = {}; + final private = targetDrive.isPrivate; + final driveKey = + private ? await driveDao.getDriveKey(targetDrive.id, cipherKey) : null; for (var file in files) { final fileName = file.name; - final filePath = '${folderEntry.path}/$fileName'; - final fileSize = await file.length(); + final filePath = '${targetFolder.path}/${file.path}'; + final fileSize = file.size; final fileEntity = FileEntity( driveId: targetDrive.id, name: fileName, size: fileSize, - lastModifiedDate: await file.lastModified(), - parentFolderId: folderEntry.id, + lastModifiedDate: file.lastModifiedDate, + parentFolderId: file.parentFolderId, dataContentType: lookupMimeType(fileName) ?? 'application/octet-stream', ); // If this file conflicts with one that already exists in the target folder reuse the id of the conflicting file. - fileEntity.id = conflictingFiles[fileName] ?? _uuid.v4(); + fileEntity.id = conflictingFiles[file.getIdentifier()] ?? _uuid.v4(); - final private = targetDrive.isPrivate; - final driveKey = private - ? await driveDao.getDriveKey(targetDrive.id, cipherKey) - : null; final fileKey = private ? await deriveFileKey(driveKey!, fileEntity.id!) : null; - final revisionAction = !conflictingFiles.containsKey(file.name) - ? RevisionAction.create - : RevisionAction.uploadNewVersion; + final revisionAction = conflictingFiles.containsKey(file.getIdentifier()) + ? RevisionAction.uploadNewVersion + : RevisionAction.create; if (fileSize < bundleSizeLimit) { - _dataItemUploadHandles[fileEntity.id!] = DataItemUploadHandle( + _fileDataItemUploadHandles[fileEntity.id!] = FileDataItemUploadHandle( entity: fileEntity, path: filePath, file: file, @@ -70,7 +68,7 @@ class UploadPlanUtils { revisionAction: revisionAction, ); } else { - _v2FileUploadHandles[fileEntity.id!] = FileUploadHandle( + _fileV2UploadHandles[fileEntity.id!] = FileV2UploadHandle( entity: fileEntity, path: filePath, file: file, @@ -80,9 +78,55 @@ class UploadPlanUtils { ); } } + foldersByPath.forEach((key, folder) async { + _folderDataItemUploadHandles.putIfAbsent( + folder.id, + () => FolderDataItemUploadHandle( + folder: folder, + arweave: arweave, + wallet: wallet, + targetDriveId: targetDrive.id, + driveKey: driveKey, + ), + ); + }); + return UploadPlan.create( - v2FileUploadHandles: _v2FileUploadHandles, - dataItemUploadHandles: _dataItemUploadHandles, + fileV2UploadHandles: _fileV2UploadHandles, + fileDataItemUploadHandles: _fileDataItemUploadHandles, + folderDataItemUploadHandles: _folderDataItemUploadHandles, ); } + + ///Returns a sorted list of folders (root folder first) from a list of files + ///with paths + static Map generateFoldersForFiles(List files) { + final foldersByPath = {}; + + // Generate folders + for (var file in files) { + final path = file.file.relativePath!; + final folderPath = path.split('/'); + folderPath.removeLast(); + for (var i = 0; i < folderPath.length; i++) { + final currentFolder = folderPath.getRange(0, i + 1).join('/'); + if (foldersByPath[currentFolder] == null) { + final parentFolderPath = folderPath.getRange(0, i).join('/'); + foldersByPath.putIfAbsent( + currentFolder, + () => WebFolder( + name: folderPath[i], + id: Uuid().v4(), + parentFolderPath: parentFolderPath, + ), + ); + } + } + } + + final sortedFolders = foldersByPath.entries.toList() + ..sort( + (a, b) => a.key.split('/').length.compareTo(b.key.split('/').length)); + return Map.fromEntries(sortedFolders); + } } diff --git a/pubspec.lock b/pubspec.lock index 9ee48b672f..4d4f038cd8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -47,11 +47,11 @@ packages: dependency: "direct main" description: path: "." - ref: "v3.0.1" - resolved-ref: e74024d267687f40edd4139b15c54aea427596e0 + ref: "v3.1.0" + resolved-ref: ddfcad2436545d261686d5d33e6eed18e1ac7f93 url: "https://github.com/ardriveapp/arweave-dart" source: git - version: "3.0.0" + version: "3.1.0" async: dependency: transitive description: @@ -199,6 +199,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + cooky: + dependency: "direct main" + description: + name: cooky + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" coverage: dependency: transitive description: @@ -311,13 +318,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.8.1+2" - filesize: - dependency: "direct main" - description: - name: filesize - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" fixnum: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 034dad5fa6..37aa742fd4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: ardrive description: Secure, permanent storage -publish_to: 'none' +publish_to: "none" # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -13,10 +13,10 @@ publish_to: 'none' # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.13.0 +version: 1.14.0 environment: - sdk: '>=2.13.0 <3.0.0' + sdk: ">=2.13.0 <3.0.0" dependencies: flutter: @@ -27,14 +27,13 @@ dependencies: arweave: git: url: https://github.com/ardriveapp/arweave-dart - ref: v3.0.1 + ref: v3.1.0 cryptography: ^2.0.1 flutter_bloc: ^8.0.1 flutter_portal: ^0.4.0 file_selector: ^0.8.2 file_selector_web: ^0.8.1 file_selector_macos: ^0.0.4 - filesize: ^2.0.1 google_fonts: ^2.1.0 intersperse: ^2.0.0 intl: ^0.17.0 @@ -56,6 +55,7 @@ dependencies: collection: ^1.15.0-nullsafety.4 csv: 5.0.0 stash_memory: 4.0.1 + cooky: ^2.0.0 dev_dependencies: flutter_test: diff --git a/test/blocs/upload_cubit_test.dart b/test/blocs/upload_cubit_test.dart index 893b57533c..6daccc9088 100644 --- a/test/blocs/upload_cubit_test.dart +++ b/test/blocs/upload_cubit_test.dart @@ -2,7 +2,9 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:ardrive/blocs/blocs.dart'; -import 'package:ardrive/blocs/upload/upload_plan.dart'; +import 'package:ardrive/blocs/upload/models/io_file.dart'; +import 'package:ardrive/blocs/upload/models/upload_file.dart'; +import 'package:ardrive/blocs/upload/models/upload_plan.dart'; import 'package:ardrive/models/daos/drive_dao/drive_dao.dart'; import 'package:ardrive/models/database/database.dart'; import 'package:arweave/arweave.dart'; @@ -36,9 +38,9 @@ void main() { const tEmptyNestedFolderCount = 5; late Database db; - late List tAllConflictingFiles; - late List tSomeConflictingFiles; - late List tNoConflictingFiles; + late List tAllConflictingFiles; + late List tSomeConflictingFiles; + late List tNoConflictingFiles; final tWallet = getTestWallet(); String? tWalletAddress; @@ -74,19 +76,24 @@ void main() { // We need a real file path because in the UploadCubit we needs the size of the file // to know if the file is `tooLargeFiles`. - final _tRealPathFile = XFile('assets/config/dev.json'); + final _tRealPathFile = + await IOFile.fromXFile(XFile('assets/config/dev.json'), tRootFolderId); // The `addTestFilesToDb` will generate files with this path and name, so it // will be a confliting file. - final tConflictingFile = XFile(tRootFolderId + '1'); + final tConflictingFile = + await IOFile.fromXFile(XFile(tRootFolderId + '1'), tRootFolderId); // Contains only conflicting files. - tAllConflictingFiles = [tConflictingFile]; + tAllConflictingFiles = [tConflictingFile]; /// This list contains conflicting and non conflicting files. - tSomeConflictingFiles = [tConflictingFile, XFile('dumb_test_path')]; + tSomeConflictingFiles = [ + tConflictingFile, + await IOFile.fromXFile(XFile('dumb_test_path'), tRootFolderId) + ]; - tNoConflictingFiles = [_tRealPathFile]; + tNoConflictingFiles = [_tRealPathFile]; mockArweave = MockArweaveService(); mockPst = MockPstService(); @@ -107,7 +114,7 @@ void main() { ); }); - UploadCubit getUploadCubitInstanceWith(List files) { + UploadCubit getUploadCubitInstanceWith(List files) { return UploadCubit( uploadPlanUtils: mockUploadPlanUtils, driveId: tDriveId, @@ -119,15 +126,20 @@ void main() { pst: mockPst); } - void setDumbUploadPlan() => when(() => mockUploadPlanUtils.xfilesToUploadPlan( - files: any(named: 'files'), - cipherKey: any(named: 'cipherKey'), - wallet: any(named: 'wallet'), - conflictingFiles: any(named: 'conflictingFiles'), - targetDrive: any(named: 'targetDrive'), - folderEntry: any( - named: 'folderEntry'))).thenAnswer((invocation) => Future.value( - UploadPlan.create(v2FileUploadHandles: {}, dataItemUploadHandles: {}))); + void setDumbUploadPlan() => when(() => mockUploadPlanUtils.filesToUploadPlan( + files: any(named: 'files'), + cipherKey: any(named: 'cipherKey'), + wallet: any(named: 'wallet'), + conflictingFiles: any(named: 'conflictingFiles'), + targetDrive: any(named: 'targetDrive'), + targetFolder: any(named: 'folderEntry'))) + .thenAnswer((invocation) => Future.value( + UploadPlan.create( + fileV2UploadHandles: {}, + fileDataItemUploadHandles: {}, + folderDataItemUploadHandles: {}, + ), + )); group('check if there are some conflicting file', () { setUp(() { @@ -205,8 +217,8 @@ void main() { }); group('prepare upload plan and costs estimates', () { - late XFile tTooLargeFile; - late List tTooLargeFiles; + late UploadFile tTooLargeFile; + late List tTooLargeFiles; setUp(() { when(() => mockProfileCubit!.state).thenReturn( @@ -225,15 +237,21 @@ void main() { .thenAnswer((invocation) => Future.value(BigInt.zero)); when(() => mockArweave.getArUsdConversionRate()) .thenAnswer((invocation) => Future.value(10)); - when(() => mockUploadPlanUtils.xfilesToUploadPlan( - files: any(named: 'files'), - cipherKey: any(named: 'cipherKey'), - wallet: any(named: 'wallet'), - conflictingFiles: any(named: 'conflictingFiles'), - targetDrive: any(named: 'targetDrive'), - folderEntry: any(named: 'folderEntry'))) - .thenAnswer((invocation) => Future.value(UploadPlan.create( - v2FileUploadHandles: {}, dataItemUploadHandles: {}))); + when(() => mockUploadPlanUtils.filesToUploadPlan( + files: any(named: 'files'), + cipherKey: any(named: 'cipherKey'), + wallet: any(named: 'wallet'), + conflictingFiles: any(named: 'conflictingFiles'), + targetDrive: any(named: 'targetDrive'), + targetFolder: any(named: 'folderEntry'))).thenAnswer( + (invocation) => Future.value( + UploadPlan.create( + fileV2UploadHandles: {}, + fileDataItemUploadHandles: {}, + folderDataItemUploadHandles: {}, + ), + ), + ); when(() => mockProfileCubit!.isCurrentProfileArConnect()) .thenAnswer((i) => Future.value(true)); }); @@ -281,7 +299,10 @@ void main() { setUp: () async { final tFile = File('some_file.txt'); tFile.writeAsBytesSync(Uint8List(publicFileSizeLimit.toInt() + 1)); - tTooLargeFile = XFile(tFile.path); + tTooLargeFile = await IOFile.fromXFile( + XFile(tFile.path), + tRootFolderId, + ); tTooLargeFiles = [tTooLargeFile]; }, build: () { diff --git a/test/utils/filesize_test.dart b/test/utils/filesize_test.dart new file mode 100644 index 0000000000..2573f5ae1c --- /dev/null +++ b/test/utils/filesize_test.dart @@ -0,0 +1,51 @@ +import 'package:ardrive/utils/filesize.dart'; +import 'package:test/test.dart'; + +void main() { + group( + 'filesize returns the correct string interpretation for', + () { + test('10 as int', () { + expect(filesize(10), '10 B'); + }); + + test('10 as string', () { + expect(filesize('10'), '10 B'); + }); + + test('1024 as int', () { + expect(filesize(1024), '1 KiB'); + }); + + test('1024 as string', () { + expect(filesize('1024'), '1 KiB'); + }); + + test('1M as int', () { + expect(filesize(1024 * 1024), '1 MiB'); + }); + + test('1G as int', () { + expect(filesize(1024 * 1024 * 1024), '1 GiB'); + }); + + test('1T as int', () { + expect(filesize(1024 * 1024 * 1024 * 1024), '1 TiB'); + }); + + test('1P as int', () { + expect(filesize(1024 * 1024 * 1024 * 1024 * 1024), '1 PiB'); + }); + + test('Invalid Value', () { + late ArgumentError exception; + try { + filesize('abc'); + } on ArgumentError catch (e) { + exception = e; + } + expect(exception, isArgumentError); + }); + }, + ); +} diff --git a/test/utils/num_to_string_parsers_test.dart b/test/utils/num_to_string_parsers_test.dart deleted file mode 100644 index 4dead525b0..0000000000 --- a/test/utils/num_to_string_parsers_test.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:ardrive/utils/num_to_string_parsers.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('num to string parser tests', () { - // The method is now using internationalization. Different ouputs are expected for different languages - Skip; - test( - 'fileAndFolderCountsToString returns the correct results ' - 'for 0 files and 0 folders', () { - expect( - fileAndFolderCountsToString(folderCount: 0, fileCount: 0), - equals('0 folders, 0 files'), - ); - }); - - test( - 'fileAndFolderCountsToString returns the correct results ' - 'for 0 files and 1 folder', () { - expect( - fileAndFolderCountsToString(folderCount: 1, fileCount: 0), - equals('1 folder, 0 files'), - ); - }); - test( - 'fileAndFolderCountsToString returns the correct results ' - 'for 1 file and 0 folders', () { - expect( - fileAndFolderCountsToString(folderCount: 0, fileCount: 1), - equals('0 folders, 1 file'), - ); - }); - - test( - 'fileAndFolderCountsToString returns the correct results ' - 'for 1 file and 1 folder', () { - expect( - fileAndFolderCountsToString(folderCount: 1, fileCount: 1), - equals('1 folder, 1 file'), - ); - }); - test( - 'fileAndFolderCountsToString returns the correct results ' - 'for 2 files and 1 folder', () { - expect( - fileAndFolderCountsToString(folderCount: 1, fileCount: 2), - equals('1 folder, 2 files'), - ); - }); - - test( - 'fileAndFolderCountsToString returns the correct results ' - 'for 1 file and 2 folders', () { - expect( - fileAndFolderCountsToString(folderCount: 2, fileCount: 1), - equals('2 folders, 1 file'), - ); - }); - test( - 'fileAndFolderCountsToString returns the correct results ' - 'for 2 files and 2 folders', () { - expect( - fileAndFolderCountsToString(folderCount: 2, fileCount: 2), - equals('2 folders, 2 files'), - ); - }); - }); -}