diff --git a/lib/authentication/ardrive_auth.dart b/lib/authentication/ardrive_auth.dart index 2cdd6a8e71..4c58f6cfcc 100644 --- a/lib/authentication/ardrive_auth.dart +++ b/lib/authentication/ardrive_auth.dart @@ -289,8 +289,9 @@ class ArDriveAuthImpl implements ArDriveAuth { final privateDrive = await _arweave.getLatestDriveEntityWithId( firstDrivePrivateDriveTxId, - checkDriveKey, - profileQueryMaxRetries, + driveKey: checkDriveKey, + driveOwner: await wallet.getAddress(), + maxRetries: profileQueryMaxRetries, ); return privateDrive != null; diff --git a/lib/blocs/create_manifest/create_manifest_cubit.dart b/lib/blocs/create_manifest/create_manifest_cubit.dart index 3d32536faa..215e804f5f 100644 --- a/lib/blocs/create_manifest/create_manifest_cubit.dart +++ b/lib/blocs/create_manifest/create_manifest_cubit.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'package:ardrive/blocs/blocs.dart'; +import 'package:ardrive/core/arfs/repository/file_repository.dart'; +import 'package:ardrive/core/arfs/repository/folder_repository.dart'; import 'package:ardrive/entities/entities.dart'; import 'package:ardrive/entities/manifest_data.dart'; import 'package:ardrive/models/models.dart'; @@ -31,6 +33,10 @@ class CreateManifestCubit extends Cubit { final TurboUploadService _turboUploadService; final DriveDao _driveDao; final PstService _pst; + + final FolderRepository _folderRepository; + final FileRepository _fileRepository; + bool _hasPendingFiles = false; StreamSubscription? _selectedFolderSubscription; @@ -43,12 +49,16 @@ class CreateManifestCubit extends Cubit { required DriveDao driveDao, required PstService pst, required bool hasPendingFiles, + required FileRepository fileRepository, + required FolderRepository folderRepository, }) : _profileCubit = profileCubit, _arweave = arweave, _turboUploadService = turboUploadService, _driveDao = driveDao, _pst = pst, _hasPendingFiles = hasPendingFiles, + _fileRepository = fileRepository, + _folderRepository = folderRepository, super(CreateManifestInitial()) { if (drive.isPrivate) { // Extra guardrail to prevent private drives from creating manifests @@ -215,10 +225,14 @@ class CreateManifestCubit extends Cubit { try { final parentFolder = (state as CreateManifestPreparingManifest).parentFolder; + final folderNode = rootFolderNode.searchForFolder(parentFolder.id) ?? await _driveDao.getFolderTree(drive.id, parentFolder.id); - final arweaveManifest = ManifestData.fromFolderNode( + + final arweaveManifest = await ManifestData.fromFolderNode( folderNode: folderNode, + fileRepository: _fileRepository, + folderRepository: _folderRepository, ); final profile = _profileCubit.state as ProfileLoggedIn; @@ -253,10 +267,7 @@ class CreateManifestCubit extends Cubit { addManifestToDatabase() => _driveDao.transaction( () async { - await _driveDao.writeFileEntity( - manifestFileEntity, - '${parentFolder.path}/$manifestName', - ); + await _driveDao.writeFileEntity(manifestFileEntity); await _driveDao.insertFileRevision( manifestFileEntity.toRevisionCompanion( performedAction: existingManifestFileId == null diff --git a/lib/blocs/drive_attach/drive_attach_cubit.dart b/lib/blocs/drive_attach/drive_attach_cubit.dart index 6687884520..f5b7204094 100644 --- a/lib/blocs/drive_attach/drive_attach_cubit.dart +++ b/lib/blocs/drive_attach/drive_attach_cubit.dart @@ -125,7 +125,7 @@ class DriveAttachCubit extends Cubit { final driveEntity = await _arweave.getLatestDriveEntityWithId( driveId, - _driveKey, + driveKey: _driveKey, ); if (driveEntity == null) { @@ -188,7 +188,8 @@ class DriveAttachCubit extends Cubit { } } - final drive = await _arweave.getLatestDriveEntityWithId(driveId, _driveKey); + final drive = + await _arweave.getLatestDriveEntityWithId(driveId, driveKey: _driveKey); if (drive == null) { return false; @@ -208,7 +209,8 @@ class DriveAttachCubit extends Cubit { _driveKey = await getDriveKey(promptedDriveKey); - final drive = await _arweave.getLatestDriveEntityWithId(driveId, _driveKey); + final drive = + await _arweave.getLatestDriveEntityWithId(driveId, driveKey: _driveKey); if (drive == null) { return 'Invalid drive key'; diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index 0159e51851..d343f53e8f 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/blocs.dart'; +import 'package:ardrive/blocs/drive_detail/utils/breadcrumb_builder.dart'; import 'package:ardrive/core/activity_tracker.dart'; -import 'package:ardrive/entities/constants.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/pages.dart'; import 'package:ardrive/services/services.dart'; @@ -29,6 +29,7 @@ class DriveDetailCubit extends Cubit { final ConfigService _configService; final ArDriveAuth _auth; final ActivityTracker _activityTracker; + final BreadcrumbBuilder _breadcrumbBuilder; StreamSubscription? _folderSubscription; final _defaultAvailableRowsPerPage = [25, 50, 75, 100]; @@ -52,11 +53,13 @@ class DriveDetailCubit extends Cubit { required ConfigService configService, required ActivityTracker activityTracker, required ArDriveAuth auth, + required BreadcrumbBuilder breadcrumbBuilder, }) : _profileCubit = profileCubit, _activityTracker = activityTracker, _driveDao = driveDao, _auth = auth, _configService = configService, + _breadcrumbBuilder = breadcrumbBuilder, super(DriveDetailLoadInProgress()) { if (driveId.isEmpty) { return; @@ -70,11 +73,15 @@ class DriveDetailCubit extends Cubit { .getSingleOrNull(); // Open the root folder if the deep-linked folder could not be found. - openFolder(path: folder?.path ?? rootPath); + openFolder(folderId: folder?.id); // The empty string here is required to open the root folder }); } else { - openFolder(path: rootPath); + Future.microtask(() async { + final drive = + await _driveDao.driveById(driveId: driveId).getSingleOrNull(); + openFolder(folderId: drive?.rootFolderId); + }); } } @@ -85,7 +92,7 @@ class DriveDetailCubit extends Cubit { } void openFolder({ - required String path, + String? folderId, DriveOrder contentOrderBy = DriveOrder.name, OrderingMode contentOrderingMode = OrderingMode.asc, }) async { @@ -111,9 +118,15 @@ class DriveDetailCubit extends Cubit { } try { - await _driveDao.getFolderTree(driveId, value.rootFolderId); + await _driveDao + .folderById( + driveId: driveId, + folderId: folderId ?? value.rootFolderId, + ) + .getSingle(); } catch (e) { - logger.d('Folder with id ${value.rootFolderId} not found'); + logger + .d('Folder with id ${folderId ?? value.rootFolderId} not found'); emit(DriveInitialLoading()); return; @@ -125,9 +138,9 @@ class DriveDetailCubit extends Cubit { _driveDao.driveById(driveId: driveId).watchSingle(), _driveDao.watchFolderContents( driveId, - folderPath: path, orderBy: contentOrderBy, orderingMode: contentOrderingMode, + folderId: folderId, ), _profileCubit.stream.startWith(ProfileCheckingAvailability()), (drive, folderContents, _) async { @@ -141,15 +154,10 @@ class DriveDetailCubit extends Cubit { final profile = _profileCubit.state; - var availableRowsPerPage = _defaultAvailableRowsPerPage; - - availableRowsPerPage = calculateRowsPerPage( + final availableRowsPerPage = calculateRowsPerPage( folderContents.files.length + folderContents.subfolders.length, ); - final rootFolderNode = - await _driveDao.getFolderTree(driveId, drive.rootFolderId); - if (_selectedItem != null && _refreshSelectedItem) { if (_selectedItem is FileDataTableItem) { final index = folderContents.files.indexWhere( @@ -193,6 +201,13 @@ class DriveDetailCubit extends Cubit { isOwner: isDriveOwner(_auth, drive.ownerAddress), ); + final List pathSegments = + await _breadcrumbBuilder.buildForFolder( + folderId: folderContents.folder.id, + rootFolderId: drive.rootFolderId, + driveId: driveId, + ); + if (state != null) { emit( state.copyWith( @@ -207,12 +222,17 @@ class DriveDetailCubit extends Cubit { availableRowsPerPage: availableRowsPerPage, currentFolderContents: currentFolderContents, isShowingHiddenFiles: _showHiddenFiles, + pathSegments: pathSegments, + driveIsEmpty: folderContents.files.isEmpty && + folderContents.subfolders.isEmpty, ), ); } else { final columnsVisibility = await getTableColumnVisibility(); + emit( DriveDetailLoadSuccess( + pathSegments: pathSegments, selectedItem: _selectedItem, currentDrive: drive, hasWritePermissions: profile is ProfileLoggedIn && @@ -222,7 +242,8 @@ class DriveDetailCubit extends Cubit { contentOrderingMode: contentOrderingMode, rowsPerPage: availableRowsPerPage.first, availableRowsPerPage: availableRowsPerPage, - driveIsEmpty: rootFolderNode.isEmpty(), + driveIsEmpty: folderContents.files.isEmpty && + folderContents.subfolders.isEmpty, multiselect: false, currentFolderContents: currentFolderContents, columnVisibility: columnsVisibility, @@ -393,7 +414,7 @@ class DriveDetailCubit extends Cubit { }) { final state = this.state as DriveDetailLoadSuccess; openFolder( - path: state.folderInView.folder.path, + folderId: state.folderInView.folder.id, contentOrderBy: contentOrderBy, contentOrderingMode: contentOrderingMode, ); diff --git a/lib/blocs/drive_detail/drive_detail_state.dart b/lib/blocs/drive_detail/drive_detail_state.dart index 09817b9e2c..044da15802 100644 --- a/lib/blocs/drive_detail/drive_detail_state.dart +++ b/lib/blocs/drive_detail/drive_detail_state.dart @@ -17,6 +17,8 @@ class DriveDetailLoadSuccess extends DriveDetailState { final FolderWithContents folderInView; + final List pathSegments; + final DriveOrder contentOrderBy; final OrderingMode contentOrderingMode; @@ -59,6 +61,7 @@ class DriveDetailLoadSuccess extends DriveDetailState { required this.columnVisibility, this.forceRebuildKey, required this.isShowingHiddenFiles, + required this.pathSegments, }); DriveDetailLoadSuccess copyWith({ @@ -79,6 +82,7 @@ class DriveDetailLoadSuccess extends DriveDetailState { List? currentFolderContents, Key? forceRebuildKey, bool? isShowingHiddenFiles, + List? pathSegments, }) => DriveDetailLoadSuccess( columnVisibility: columnVisibility, @@ -102,6 +106,7 @@ class DriveDetailLoadSuccess extends DriveDetailState { currentFolderContents: currentFolderContents ?? this.currentFolderContents, isShowingHiddenFiles: isShowingHiddenFiles ?? this.isShowingHiddenFiles, + pathSegments: pathSegments ?? this.pathSegments, ); @override diff --git a/lib/blocs/drive_detail/utils/breadcrumb_builder.dart b/lib/blocs/drive_detail/utils/breadcrumb_builder.dart new file mode 100644 index 0000000000..b32c255e9c --- /dev/null +++ b/lib/blocs/drive_detail/utils/breadcrumb_builder.dart @@ -0,0 +1,39 @@ +import 'package:ardrive/core/arfs/repository/folder_repository.dart'; +import 'package:ardrive/pages/drive_detail/drive_detail_page.dart'; +import 'package:ardrive/utils/logger.dart'; + +class BreadcrumbBuilder { + final FolderRepository _folderRepository; + + BreadcrumbBuilder(this._folderRepository); + + Future> buildForFolder({ + required String folderId, + required String rootFolderId, + required String driveId, + }) async { + List breadcrumbs = []; + String? currentFolderId = folderId; + + while (currentFolderId != null && currentFolderId != rootFolderId) { + final folderRevision = await _folderRepository + .getLatestFolderRevisionInfo(driveId, currentFolderId); + if (folderRevision == null) { + logger.e('FolderRevision not found for folderId: $currentFolderId'); + throw Exception( + 'FolderRevision not found for folderId: $currentFolderId'); + } + + breadcrumbs.insert( + 0, + BreadCrumbRowInfo( + text: folderRevision.name, + targetId: folderRevision.folderId, + ), + ); + currentFolderId = folderRevision.parentFolderId; + } + + return breadcrumbs; + } +} diff --git a/lib/blocs/folder_create/folder_create_cubit.dart b/lib/blocs/folder_create/folder_create_cubit.dart index 9f4e37be56..eda99a7a29 100644 --- a/lib/blocs/folder_create/folder_create_cubit.dart +++ b/lib/blocs/folder_create/folder_create_cubit.dart @@ -68,7 +68,6 @@ class FolderCreateCubit extends Cubit { driveId: targetFolder.driveId, parentFolderId: targetFolder.id, folderName: folderName, - path: '${targetFolder.path}/$folderName', ); final folderEntity = FolderEntity( diff --git a/lib/blocs/fs_entry_move/fs_entry_move_bloc.dart b/lib/blocs/fs_entry_move/fs_entry_move_bloc.dart index 47ed4ef70b..b25f43dd48 100644 --- a/lib/blocs/fs_entry_move/fs_entry_move_bloc.dart +++ b/lib/blocs/fs_entry_move/fs_entry_move_bloc.dart @@ -27,7 +27,6 @@ class FsEntryMoveBloc extends Bloc { final TurboUploadService _turboUploadService; final DriveDao _driveDao; final ProfileCubit _profileCubit; - final SyncCubit _syncCubit; final ArDriveCrypto _crypto; final DriveDetailCubit _driveDetailCubit; @@ -48,7 +47,6 @@ class FsEntryMoveBloc extends Bloc { _driveDao = driveDao, _profileCubit = profileCubit, _driveDetailCubit = driveDetailCubit, - _syncCubit = syncCubit, _crypto = crypto, super(const FsEntryMoveLoadInProgress()) { if (_selectedItems.isEmpty) { @@ -223,9 +221,7 @@ class FsEntryMoveBloc extends Bloc { .fileById(driveId: driveId, fileId: fileToMove.id) .getSingle(); file = file.copyWith( - parentFolderId: parentFolder.id, - path: '${parentFolder.path}/${file.name}', - lastUpdated: DateTime.now()); + parentFolderId: parentFolder.id, lastUpdated: DateTime.now()); final fileKey = driveKey != null ? await _crypto.deriveFileKey(driveKey, file.id) : null; @@ -254,7 +250,6 @@ class FsEntryMoveBloc extends Bloc { .getSingle(); folder = folder.copyWith( parentFolderId: Value(parentFolder.id), - path: '${parentFolder.path}/${folder.name}', lastUpdated: DateTime.now(), ); @@ -296,7 +291,5 @@ class FsEntryMoveBloc extends Bloc { ); await _arweave.postTx(moveTx); } - - await _syncCubit.generateFsEntryPaths(driveId, folderMap, {}); } } diff --git a/lib/blocs/fs_entry_rename/fs_entry_rename_cubit.dart b/lib/blocs/fs_entry_rename/fs_entry_rename_cubit.dart index e22594bfcc..b1487678bb 100644 --- a/lib/blocs/fs_entry_rename/fs_entry_rename_cubit.dart +++ b/lib/blocs/fs_entry_rename/fs_entry_rename_cubit.dart @@ -2,7 +2,6 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; -import 'package:ardrive/sync/domain/cubit/sync_cubit.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; import 'package:ardrive/utils/logger.dart'; import 'package:ardrive_io/ardrive_io.dart'; @@ -24,7 +23,6 @@ class FsEntryRenameCubit extends Cubit { final TurboUploadService _turboUploadService; final DriveDao _driveDao; final ProfileCubit _profileCubit; - final SyncCubit _syncCubit; final ArDriveCrypto _crypto; bool get _isRenamingFolder => folderId != null; @@ -39,13 +37,11 @@ class FsEntryRenameCubit extends Cubit { required TurboUploadService turboUploadService, required DriveDao driveDao, required ProfileCubit profileCubit, - required SyncCubit syncCubit, required ArDriveCrypto crypto, }) : _arweave = arweave, _turboUploadService = turboUploadService, _driveDao = driveDao, _profileCubit = profileCubit, - _syncCubit = syncCubit, _crypto = crypto, assert(folderId != null || fileId != null), super(FsEntryRenameInitializing(isRenamingFolder: folderId != null)) { @@ -121,9 +117,6 @@ class FsEntryRenameCubit extends Cubit { performedAction: RevisionAction.rename)); }); - final folderMap = {folder.id: folder.toCompanion(false)}; - await _syncCubit.generateFsEntryPaths(driveId, folderMap, {}); - emit(const FolderEntryRenameSuccess()); } else { var file = await _driveDao diff --git a/lib/blocs/ghost_fixer/ghost_fixer_cubit.dart b/lib/blocs/ghost_fixer/ghost_fixer_cubit.dart index 7bf312ded3..f87dadd29d 100644 --- a/lib/blocs/ghost_fixer/ghost_fixer_cubit.dart +++ b/lib/blocs/ghost_fixer/ghost_fixer_cubit.dart @@ -4,7 +4,6 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/pages.dart'; import 'package:ardrive/services/services.dart'; -import 'package:ardrive/sync/domain/cubit/sync_cubit.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; import 'package:ardrive/utils/logger.dart'; import 'package:equatable/equatable.dart'; @@ -20,7 +19,6 @@ class GhostFixerCubit extends Cubit { final ArweaveService _arweave; final TurboUploadService _turboUploadService; final DriveDao _driveDao; - final SyncCubit _syncCubit; StreamSubscription? _selectedFolderSubscription; @@ -30,12 +28,10 @@ class GhostFixerCubit extends Cubit { required ArweaveService arweave, required TurboUploadService turboUploadService, required DriveDao driveDao, - required SyncCubit syncCubit, }) : _profileCubit = profileCubit, _arweave = arweave, _turboUploadService = turboUploadService, _driveDao = driveDao, - _syncCubit = syncCubit, super(GhostFixerInitial()) { _driveDao .driveById(driveId: ghostFolder.driveId) @@ -129,11 +125,12 @@ class GhostFixerCubit extends Cubit { driveId: ghostFolder.driveId, name: folderName, parentFolderId: parentFolder.id, - path: '${parentFolder.path}/$folderName', isGhost: false, lastUpdated: ghostFolder.lastUpdated, dateCreated: ghostFolder.dateCreated, isHidden: ghostFolder.isHidden, + // TODO: path is not used in the app, so it's not necessary to set it + path: '', ); final folderEntity = folder.asEntity(); @@ -164,8 +161,6 @@ class GhostFixerCubit extends Cubit { await _driveDao.insertFolderRevision(folderEntity.toRevisionCompanion( performedAction: RevisionAction.create)); - final folderMap = {folder.id: folder.toCompanion(false)}; - await _syncCubit.generateFsEntryPaths(folder.driveId, folderMap, {}); }); emit(GhostFixerSuccess()); } catch (err) { diff --git a/lib/blocs/pin_file/pin_file_bloc.dart b/lib/blocs/pin_file/pin_file_bloc.dart index 4eea5e3a12..95e6ba5875 100644 --- a/lib/blocs/pin_file/pin_file_bloc.dart +++ b/lib/blocs/pin_file/pin_file_bloc.dart @@ -311,10 +311,6 @@ class PinFileBloc extends Bloc { ? await _crypto.deriveFileKey(driveKey, newFileEntity.id!) : null; - final parentFolder = await _driveDao - .folderById(driveId: _driveId, folderId: _parentFolderId) - .getSingle(); - final isAPublicPin = fileKey == null; if (_turboUploadService.useTurboUpload) { @@ -368,10 +364,7 @@ class PinFileBloc extends Bloc { newFileEntity.txId = fileDataItem.id; } - final parentFolderPath = parentFolder.path; - final filePath = '$parentFolderPath/${newFileEntity.name}'; - - await _driveDao.writeFileEntity(newFileEntity, filePath); + await _driveDao.writeFileEntity(newFileEntity); await _driveDao.insertFileRevision(newFileEntity.toRevisionCompanion( // FIXME: this is gonna change when we allow to ovewrite an existing file performedAction: RevisionAction.create, diff --git a/lib/blocs/profile_add/profile_add_cubit.dart b/lib/blocs/profile_add/profile_add_cubit.dart index ec32a4e236..0a70cd3cfc 100644 --- a/lib/blocs/profile_add/profile_add_cubit.dart +++ b/lib/blocs/profile_add/profile_add_cubit.dart @@ -184,8 +184,9 @@ class ProfileAddCubit extends Cubit { try { privateDrive = await _arweave.getLatestDriveEntityWithId( checkDriveId, - checkDriveKey, - profileQueryMaxRetries, + driveOwner: await _wallet.getAddress(), + driveKey: checkDriveKey, + maxRetries: profileQueryMaxRetries, ); } catch (e) { emit(ProfileAddFailure()); diff --git a/lib/blocs/prompt_to_snapshot/prompt_to_snapshot_bloc.dart b/lib/blocs/prompt_to_snapshot/prompt_to_snapshot_bloc.dart index 8a619366fb..a1567fc590 100644 --- a/lib/blocs/prompt_to_snapshot/prompt_to_snapshot_bloc.dart +++ b/lib/blocs/prompt_to_snapshot/prompt_to_snapshot_bloc.dart @@ -96,9 +96,6 @@ class PromptToSnapshotBloc Emitter emit, ) async { if (_isSyncRunning) { - logger.d( - '[PROMPT TO SNAPSHOT] The sync is running, so we won\'t prompt to snapshot', - ); _debouncer.cancel(); return; } @@ -133,15 +130,8 @@ class PromptToSnapshotBloc return; } - logger.d('[PROMPT TO SNAPSHOT] Selected drive ${event.driveId}'); - final shouldAskAgain = await _shouldAskToSnapshotAgain(); - logger.d( - '[PROMPT TO SNAPSHOT] Will attempt to prompt for drive ${event.driveId}' - ' in ${_durationBeforePrompting.inSeconds}s', - ); - await _debouncer.run(() async { final stateIsIdle = state is PromptToSnapshotIdle; final wouldDriveBenefitFromSnapshot = event.driveId != null && @@ -159,16 +149,6 @@ class PromptToSnapshotBloc logger.d( '[PROMPT TO SNAPSHOT] Prompting to snapshot for ${event.driveId}'); emit(PromptToSnapshotPrompting(driveId: event.driveId!)); - } else { - logger.d( - '[PROMPT TO SNAPSHOT] Didn\'t prompt for ${event.driveId}.' - ' isSyncRunning: $_isSyncRunning' - ' shoudAskAgain: $shouldAskAgain' - ' wouldDriveBenefitFromSnapshot: $wouldDriveBenefitFromSnapshot' - ' hasWritePermissions: $hasWritePermissions' - ' isBlocClosed: $isClosed' - ' stateIsIdle: $stateIsIdle - ${state.runtimeType}', - ); } }).catchError((e) { logger.d('[PROMPT TO SNAPSHOT] Debuncer cancelled for ${event.driveId}'); @@ -274,12 +254,6 @@ abstract class CountOfTxsSyncedWithGql { final count = _getForDrive(driveId); final wouldBenefit = count >= numberOfTxsBeforeSnapshot; - logger.d( - '[PROMPT TO SNAPSHOT] Would drive $driveId' - ' ($count / $numberOfTxsBeforeSnapshot TXs) benefit from a snapshot:' - ' $wouldBenefit', - ); - return wouldBenefit; } } diff --git a/lib/blocs/upload/models/web_folder.dart b/lib/blocs/upload/models/web_folder.dart index 2234b1fb41..f1b00c63f8 100644 --- a/lib/blocs/upload/models/web_folder.dart +++ b/lib/blocs/upload/models/web_folder.dart @@ -4,7 +4,7 @@ class WebFolder { String id; late String parentFolderId; - late String path; + WebFolder({ required this.name, required this.id, diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index 44979bff1c..ba56b66764 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -373,9 +373,6 @@ class UploadCubit extends Cubit { if (existingFileId != null) { conflictingFolders.add(folder.name); } - folder.path = folder.parentFolderPath.isNotEmpty - ? '${_targetFolder.path}/${folder.parentFolderPath}/${folder.name}' - : '${_targetFolder.path}/${folder.name}'; } final filesToUpload = []; for (var file in files) { @@ -568,7 +565,6 @@ class UploadCubit extends Cubit { UploadFolder( lastModifiedDate: DateTime.now(), name: folder.name, - path: folder.path, ), )); } @@ -858,8 +854,7 @@ class UploadCubit extends Cubit { entity.txId = fileMetadata.metadataTxId!; _driveDao.transaction(() async { - final filePath = '${_targetFolder.path}/${metadata.name}'; - await _driveDao.writeFileEntity(entity, filePath); + await _driveDao.writeFileEntity(entity); await _driveDao.insertFileRevision( entity.toRevisionCompanion( performedAction: revisionAction, @@ -888,18 +883,11 @@ class UploadCubit extends Cubit { entity.txId = metadata.metadataTxId!; - final folderPath = foldersByPath.values - .firstWhere((element) => - element.name == metadata.name && - element.parentFolderId == metadata.parentFolderId) - .path; - await _driveDao.transaction(() async { await _driveDao.createFolder( driveId: _targetDrive.id, parentFolderId: metadata.parentFolderId, folderName: metadata.name, - path: folderPath, folderId: metadata.id, ); await _driveDao.insertFolderRevision( @@ -1033,7 +1021,6 @@ class UploadCubit extends Cubit { class UploadFolder extends IOFolder { UploadFolder({ required this.name, - required this.path, required this.lastModifiedDate, }); @@ -1059,8 +1046,8 @@ class UploadFolder extends IOFolder { final String name; @override - // TODO: implement path - final String path; + // We dont need to use the path for the upload + final String path = ''; @override List get props => [name, path, lastModifiedDate]; diff --git a/lib/blocs/upload/upload_handles/file_data_item_upload_handle.dart b/lib/blocs/upload/upload_handles/file_data_item_upload_handle.dart index 38dc87f7ad..09378060a1 100644 --- a/lib/blocs/upload/upload_handles/file_data_item_upload_handle.dart +++ b/lib/blocs/upload/upload_handles/file_data_item_upload_handle.dart @@ -20,7 +20,6 @@ const fileDataItemEntityCount = 2; class FileDataItemUploadHandle implements UploadHandle, DataItemHandle { final FileEntity entity; final UploadFile file; - final String path; final SecretKey? driveKey; final SecretKey? fileKey; final String revisionAction; @@ -47,7 +46,6 @@ class FileDataItemUploadHandle implements UploadHandle, DataItemHandle { FileDataItemUploadHandle({ required this.entity, - required this.path, required this.file, required this.revisionAction, required this.arweave, @@ -64,7 +62,7 @@ class FileDataItemUploadHandle implements UploadHandle, DataItemHandle { }) async { entity.bundledIn = bundledInTxId; await driveDao.transaction(() async { - await driveDao.writeFileEntity(entity, path); + await driveDao.writeFileEntity(entity); await driveDao.insertFileRevision( entity.toRevisionCompanion(performedAction: revisionAction), ); diff --git a/lib/blocs/upload/upload_handles/file_v2_upload_handle.dart b/lib/blocs/upload/upload_handles/file_v2_upload_handle.dart index 86c872f678..6afe44954a 100644 --- a/lib/blocs/upload/upload_handles/file_v2_upload_handle.dart +++ b/lib/blocs/upload/upload_handles/file_v2_upload_handle.dart @@ -16,7 +16,6 @@ import 'package:pst/pst.dart'; class FileV2UploadHandle implements UploadHandle { final FileEntity entity; final UploadFile file; - final String path; final SecretKey? driveKey; final SecretKey? fileKey; final String revisionAction; @@ -40,7 +39,6 @@ class FileV2UploadHandle implements UploadHandle { FileV2UploadHandle({ required this.entity, - required this.path, required this.file, required this.revisionAction, required this.crypto, @@ -52,7 +50,7 @@ class FileV2UploadHandle implements UploadHandle { Future writeFileEntityToDatabase({required DriveDao driveDao}) async { if (hasError) return; await driveDao.transaction(() async { - await driveDao.writeFileEntity(entity, path); + await driveDao.writeFileEntity(entity); await driveDao.insertFileRevision( entity.toRevisionCompanion(performedAction: revisionAction), ); 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 index 2ff760e569..3a2f98507b 100644 --- a/lib/blocs/upload/upload_handles/folder_data_item_upload_handle.dart +++ b/lib/blocs/upload/upload_handles/folder_data_item_upload_handle.dart @@ -79,7 +79,6 @@ class FolderDataItemUploadHandle implements UploadHandle, DataItemHandle { driveId: targetDriveId, parentFolderId: folder.parentFolderId, folderName: folder.name, - path: folder.path, folderId: folder.id, ); diff --git a/lib/components/create_manifest_form.dart b/lib/components/create_manifest_form.dart index 61f329518e..14f541328b 100644 --- a/lib/components/create_manifest_form.dart +++ b/lib/components/create_manifest_form.dart @@ -1,6 +1,8 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/create_manifest/create_manifest_cubit.dart'; import 'package:ardrive/blocs/feedback_survey/feedback_survey_cubit.dart'; +import 'package:ardrive/core/arfs/repository/file_repository.dart'; +import 'package:ardrive/core/arfs/repository/folder_repository.dart'; import 'package:ardrive/misc/misc.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/drive_detail/components/hover_widget.dart'; @@ -38,6 +40,8 @@ Future promptToCreateManifest( turboUploadService: context.read(), driveDao: context.read(), pst: context.read(), + fileRepository: context.read(), + folderRepository: context.read(), ), child: const CreateManifestForm(), ), diff --git a/lib/components/fs_entry_rename_form.dart b/lib/components/fs_entry_rename_form.dart index eb59b94256..86277210b9 100644 --- a/lib/components/fs_entry_rename_form.dart +++ b/lib/components/fs_entry_rename_form.dart @@ -3,7 +3,6 @@ import 'package:ardrive/components/progress_dialog.dart'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; -import 'package:ardrive/sync/domain/cubit/sync_cubit.dart'; import 'package:ardrive/theme/theme.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; @@ -35,7 +34,6 @@ void promptToRenameModal( turboUploadService: context.read(), driveDao: context.read(), profileCubit: context.read(), - syncCubit: context.read(), ), ), BlocProvider.value( diff --git a/lib/components/ghost_fixer_form.dart b/lib/components/ghost_fixer_form.dart index b6898b6068..013a9dbfdd 100644 --- a/lib/components/ghost_fixer_form.dart +++ b/lib/components/ghost_fixer_form.dart @@ -4,7 +4,6 @@ import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/drive_detail/components/hover_widget.dart'; import 'package:ardrive/pages/pages.dart'; import 'package:ardrive/services/services.dart'; -import 'package:ardrive/sync/domain/cubit/sync_cubit.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/show_general_dialog.dart'; @@ -24,12 +23,12 @@ Future promptToReCreateFolder(BuildContext context, context, content: BlocProvider( create: (context) => GhostFixerCubit( - ghostFolder: ghostFolder, - profileCubit: context.read(), - arweave: context.read(), - turboUploadService: context.read(), - driveDao: context.read(), - syncCubit: context.read()), + ghostFolder: ghostFolder, + profileCubit: context.read(), + arweave: context.read(), + turboUploadService: context.read(), + driveDao: context.read(), + ), child: GhostFixerForm( driveDetailCubit: driveDetailCubit, ), diff --git a/lib/components/side_bar.dart b/lib/components/side_bar.dart index d506c4a6cb..1b10fed7f6 100644 --- a/lib/components/side_bar.dart +++ b/lib/components/side_bar.dart @@ -303,7 +303,7 @@ class _AppSideBarState extends State { onTap: () { if (state.selectedDriveId == d.id) { // opens the root folder - context.read().openFolder(path: ''); + context.read().openFolder(); return; } context.read().selectDrive(d.id); diff --git a/lib/core/arfs/repository/file_repository.dart b/lib/core/arfs/repository/file_repository.dart new file mode 100644 index 0000000000..9ca87664c4 --- /dev/null +++ b/lib/core/arfs/repository/file_repository.dart @@ -0,0 +1,35 @@ +import 'package:ardrive/core/arfs/repository/folder_repository.dart'; +import 'package:ardrive/models/daos/drive_dao/drive_dao.dart'; + +abstract class FileRepository { + Future getFilePath(String driveId, String fileId); + + factory FileRepository( + DriveDao driveDao, FolderRepository folderRepository) => + _FileRepository( + driveDao, + folderRepository, + ); +} + +class _FileRepository implements FileRepository { + final DriveDao _driveDao; + final FolderRepository _folderRepository; + + _FileRepository(this._driveDao, this._folderRepository); + + @override + Future getFilePath(String driveId, String fileId) async { + final file = await _driveDao + .latestFileRevisionByFileId(driveId: driveId, fileId: fileId) + .getSingleOrNull(); + if (file == null) { + return ''; + } + + final folderPath = + await _folderRepository.getFolderPath(driveId, file.parentFolderId); + final filePath = '$folderPath/${file.name}'; + return filePath; + } +} diff --git a/lib/core/arfs/repository/folder_repository.dart b/lib/core/arfs/repository/folder_repository.dart new file mode 100644 index 0000000000..38ad35c5b1 --- /dev/null +++ b/lib/core/arfs/repository/folder_repository.dart @@ -0,0 +1,57 @@ +import 'package:ardrive/models/daos/drive_dao/drive_dao.dart'; +import 'package:ardrive/models/database/database.dart'; + +abstract class FolderRepository { + Future getLatestFolderRevisionInfo( + String driveId, String folderId); + Future getFolderPath(String driveId, String folderId); + + factory FolderRepository(DriveDao driveDao) => _FolderRepository(driveDao); +} + +class _FolderRepository implements FolderRepository { + final DriveDao _driveDao; + + _FolderRepository(this._driveDao); + + @override + Future getLatestFolderRevisionInfo( + String driveId, String folderId) async { + return await _driveDao + .latestFolderRevisionByFolderId(driveId: driveId, folderId: folderId) + .getSingleOrNull(); + } + + @override + Future getFolderPath(String driveId, String folderId) async { + // Initialize an empty list to hold each folder's name as we traverse up the hierarchy + final List pathComponents = []; + + // Current folder ID that we will be checking + String? currentFolderId = folderId; + + while (currentFolderId != null) { + // Retrieve the folder by its ID + final folder = await _driveDao + .latestFolderRevisionByFolderId( + driveId: driveId, folderId: currentFolderId) + .getSingleOrNull(); + + // If the folder is null (not found), break out of the loop to avoid an infinite loop + if (folder == null) { + break; + } + + // Prepend the folder's name to the path components list + // Assuming 'name' is the property where the folder's name is stored + pathComponents.insert(0, folder.name); + + // Move up to the parent folder for the next iteration + currentFolderId = folder.parentFolderId; + } + + // Join all path components with '/' to create the full path + // This will correctly handle the scenario when pathComponents is empty + return pathComponents.join('/'); + } +} diff --git a/lib/entities/manifest_data.dart b/lib/entities/manifest_data.dart index e9dbf0ef2f..f4f6b28a94 100644 --- a/lib/entities/manifest_data.dart +++ b/lib/entities/manifest_data.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'package:ardrive/core/arfs/repository/file_repository.dart'; +import 'package:ardrive/core/arfs/repository/folder_repository.dart'; import 'package:ardrive/entities/entities.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; @@ -73,9 +75,11 @@ class ManifestData { return manifestDataItem; } - static ManifestData fromFolderNode({ + static Future fromFolderNode({ required FolderNode folderNode, - }) { + required FolderRepository folderRepository, + required FileRepository fileRepository, + }) async { final fileList = folderNode .getRecursiveFiles() // We will not include any existing manifests in the new manifest @@ -96,16 +100,22 @@ class ManifestData { return fileList.first; }(); - final rootFolderPath = folderNode.folder.path; + final rootFolderPath = await folderRepository.getFolderPath( + folderNode.folder.driveId, + folderNode.folder.id, + ); + + final indexPath = + await fileRepository.getFilePath(indexFile.driveId, indexFile.id); + final index = ManifestIndex( - prepareManifestPath( - filePath: indexFile.path, rootFolderPath: rootFolderPath), + prepareManifestPath(filePath: indexPath, rootFolderPath: rootFolderPath), ); final paths = { for (final file in fileList) prepareManifestPath( - filePath: file.path, + filePath: await fileRepository.getFilePath(file.driveId, file.id), rootFolderPath: rootFolderPath, ): ManifestPath(file.dataTxId, fileId: file.id) }; diff --git a/lib/main.dart b/lib/main.dart index 7b803fe68a..b57bb318dc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,8 @@ import 'package:ardrive/blocs/upload/limits.dart'; import 'package:ardrive/blocs/upload/upload_file_checker.dart'; import 'package:ardrive/components/keyboard_handler.dart'; import 'package:ardrive/core/activity_tracker.dart'; +import 'package:ardrive/core/arfs/repository/file_repository.dart'; +import 'package:ardrive/core/arfs/repository/folder_repository.dart'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/core/upload/cost_calculator.dart'; import 'package:ardrive/core/upload/uploader.dart'; @@ -418,5 +420,16 @@ class AppState extends State { batchProcessor: BatchProcessor(), ), ), + RepositoryProvider( + create: (_) => FolderRepository( + _.read(), + ), + ), + RepositoryProvider( + create: (_) => FileRepository( + _.read(), + _.read(), + ), + ), ]; } diff --git a/lib/models/daos/drive_dao/drive_dao.dart b/lib/models/daos/drive_dao/drive_dao.dart index e31101f8e4..8a96c6fa1f 100644 --- a/lib/models/daos/drive_dao/drive_dao.dart +++ b/lib/models/daos/drive_dao/drive_dao.dart @@ -195,8 +195,9 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { id: rootFolderId, driveId: driveId, name: name, - path: rootPath, isHidden: const Value(false), + // TODO: path is not used in the app, so it's not necessary to set it + path: '', ), ); }); @@ -353,63 +354,52 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { Stream watchFolderContents( String driveId, { String? folderId, - String? folderPath, DriveOrder orderBy = DriveOrder.name, OrderingMode orderingMode = OrderingMode.asc, }) { - assert(folderId != null || folderPath != null); - final folderStream = (folderId != null - ? folderById(driveId: driveId, folderId: folderId) - : folderWithPath(driveId: driveId, path: folderPath!)) - .watchSingleOrNull(); - - final subfolderQuery = (folderId != null - ? foldersInFolder( - driveId: driveId, - parentFolderId: folderId, - order: (folderEntries) { - return enumToFolderOrderByClause( - folderEntries, - orderBy, - orderingMode, - ); - }, - ) - : foldersInFolderAtPath( - driveId: driveId, - path: folderPath!, - order: (folderEntries) { - return enumToFolderOrderByClause( - folderEntries, - orderBy, - orderingMode, - ); - }, - )); - - final filesQuery = folderId != null - ? filesInFolderWithLicenseAndRevisionTransactions( - driveId: driveId, - parentFolderId: folderId, - order: (fileEntries, _, __, ___) { - return enumToFileOrderByClause( - fileEntries, - orderBy, - orderingMode, - ); - }, - ) - : filesInFolderAtPathWithLicenseAndRevisionTransactions( - driveId: driveId, - path: folderPath!, - order: (fileEntries, _, __, ___) { - return enumToFileOrderByClause( - fileEntries, - orderBy, - orderingMode, - ); - }, - ); + if (folderId == null) { + return driveById(driveId: driveId).watchSingleOrNull().switchMap((drive) { + if (drive == null) { + throw Exception('Drive with id $driveId not found'); + } + + return folderById(driveId: driveId, folderId: drive.rootFolderId) + .watchSingleOrNull() + .switchMap((folder) { + return watchFolderContents(driveId, + folderId: folder!.id, + orderBy: orderBy, + orderingMode: orderingMode); + }); + }); + } + + final folderStream = + folderById(driveId: driveId, folderId: folderId).watchSingleOrNull(); + + final subfolderQuery = foldersInFolder( + driveId: driveId, + parentFolderId: folderId, + order: (folderEntries) { + return enumToFolderOrderByClause( + folderEntries, + orderBy, + orderingMode, + ); + }, + ); + + final filesQuery = filesInFolderWithLicenseAndRevisionTransactions( + driveId: driveId, + parentFolderId: folderId, + order: (fileEntries, _, __, ___) { + return enumToFileOrderByClause( + fileEntries, + orderBy, + orderingMode, + ); + }, + ); return Rx.combineLatest3( folderStream.where((folder) => folder != null).map((folder) => folder!), @@ -447,7 +437,6 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { FolderID? parentFolderId, FolderID? folderId, required String folderName, - required String path, }) async { final id = folderId ?? _uuid.v4(); final folderEntriesCompanion = FolderEntriesCompanion.insert( @@ -455,8 +444,9 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { driveId: driveId, parentFolderId: Value(parentFolderId), name: folderName, - path: path, isHidden: const Value(false), + // TODO: path is not used in the app, so it's not necessary to set it + path: '', ); await into(folderEntries).insert(folderEntriesCompanion); @@ -510,20 +500,20 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { Future writeFileEntity( FileEntity entity, - String path, ) { final companion = FileEntriesCompanion.insert( id: entity.id!, driveId: entity.driveId!, parentFolderId: entity.parentFolderId!, name: entity.name!, - path: path, dataTxId: entity.dataTxId!, size: entity.size!, lastModifiedDate: entity.lastModifiedDate ?? DateTime.now(), dataContentType: Value(entity.dataContentType), pinnedDataOwnerAddress: Value(entity.pinnedDataOwnerAddress), isHidden: Value(entity.isHidden ?? false), + // TODO: path is not used in the app, so it's not necessary to set it + path: '', ); return into(fileEntries).insert( diff --git a/lib/models/file_revision.dart b/lib/models/file_revision.dart index 7c9051825f..0f64df08d5 100644 --- a/lib/models/file_revision.dart +++ b/lib/models/file_revision.dart @@ -15,7 +15,6 @@ extension FileRevisionsCompanionExtensions on FileRevisionsCompanion { dataTxId: dataTxId.value, licenseTxId: Value(licenseTxId.value), size: size.value, - path: rootPath, lastUpdated: dateCreated, lastModifiedDate: lastModifiedDate.value, dataContentType: dataContentType, @@ -24,6 +23,8 @@ extension FileRevisionsCompanionExtensions on FileRevisionsCompanion { customJsonMetadata: customJsonMetadata, pinnedDataOwnerAddress: pinnedDataOwnerAddress, isHidden: isHidden, + // TODO: path is not used in the app, so it's not necessary to set it + path: '', ); /// Returns a list of [NetworkTransactionsCompanion] representing the metadata and data transactions diff --git a/lib/models/folder_revision.dart b/lib/models/folder_revision.dart index 1dd93439ae..353ec1a6ee 100644 --- a/lib/models/folder_revision.dart +++ b/lib/models/folder_revision.dart @@ -17,11 +17,12 @@ extension FolderRevisionCompanionExtensions on FolderRevisionsCompanion { driveId: driveId.value, parentFolderId: parentFolderId, name: name.value, - path: rootPath, lastUpdated: dateCreated, customGQLTags: customGQLTags, customJsonMetadata: customJsonMetadata, isHidden: isHidden, + // TODO: path is not used in the app, so it's not necessary to set it + path: '', ); /// Returns a [NetworkTransactionsCompanion] representing the metadata transaction diff --git a/lib/models/queries/drive_queries.drift b/lib/models/queries/drive_queries.drift index bf9f67d129..36377cb13a 100644 --- a/lib/models/queries/drive_queries.drift +++ b/lib/models/queries/drive_queries.drift @@ -31,25 +31,12 @@ latestDriveRevisionsByDriveIdWithTransactions AS DriveRevisionWithTransaction: folderById: SELECT * FROM folder_entries WHERE driveId = :driveId AND id = :folderId; -folderWithPath: - SELECT * FROM folder_entries - WHERE driveId = :driveId AND path = :path - LIMIT 1; foldersInFolder ($order = ''): SELECT * FROM folder_entries WHERE driveId = :driveId AND parentFolderId = :parentFolderId ORDER BY $order; -foldersInFolderAtPath ($order = ''): - SELECT * FROM folder_entries - WHERE parentFolderId IN ( - SELECT id FROM folder_entries - WHERE driveId = :driveId AND path = :path - LIMIT 1 - ) - ORDER BY $order; - ghostFolders: SELECT * FROM folder_entries WHERE isGhost = TRUE; @@ -92,14 +79,6 @@ filesInFolder ($order = ''): filesInFolderWithName: SELECT * FROM file_entries WHERE driveId = :driveId AND parentFolderId = :parentFolderId AND name = :name; -filesInFolderAtPath ($order = ''): - SELECT * FROM file_entries - WHERE parentFolderId IN ( - SELECT id FROM folder_entries - WHERE driveId = :driveId AND path = :path - LIMIT 1 - ) - ORDER BY $order; filesInFolderWithLicenseAndRevisionTransactions ($order = '') AS FileWithLicenseAndLatestRevisionTransactions: SELECT file_entries.*, license.**, metadataTx.**, dataTx.** FROM file_entries @@ -120,30 +99,6 @@ filesInFolderWithLicenseAndRevisionTransactions ($order = '') AS FileWithLicense LIMIT 1) WHERE file_entries.driveId = :driveId AND file_entries.parentFolderId = :parentFolderId ORDER BY $order; -filesInFolderAtPathWithLicenseAndRevisionTransactions ($order = '') AS FileWithLicenseAndLatestRevisionTransactions: - SELECT file_entries.*, license.**, metadataTx.**, dataTx.** FROM file_entries - LEFT JOIN licenses AS license ON license.licenseTxId = ( - SELECT licenseTxId FROM file_revisions AS rev - WHERE rev.driveId = :driveId AND rev.fileId = file_entries.id - ORDER BY rev.dateCreated DESC - LIMIT 1) - JOIN network_transactions AS metadataTx ON metadataTx.id = ( - SELECT metadataTxId FROM file_revisions AS rev - WHERE driveId = :driveId AND fileId = file_entries.id - ORDER BY rev.dateCreated DESC - LIMIT 1) - JOIN network_transactions AS dataTx ON dataTx.id = ( - SELECT dataTxId FROM file_revisions AS rev - WHERE driveId = :driveId AND fileId = file_entries.id - ORDER BY rev.dateCreated DESC - LIMIT 1) - WHERE parentFolderId IN ( - SELECT id FROM folder_entries - WHERE folder_entries.driveId = :driveId AND folder_entries.path = :path - LIMIT 1 - ) - ORDER BY $order; - filesInDriveWithRevisionTransactions ($order = '') AS FileWithLatestRevisionTransactions: SELECT file_entries.*, metadataTx.**, dataTx.** FROM file_entries JOIN network_transactions AS metadataTx ON metadataTx.id = ( diff --git a/lib/models/tables/folder_entries.drift b/lib/models/tables/folder_entries.drift index f44c5d95a6..63e6be8e41 100644 --- a/lib/models/tables/folder_entries.drift +++ b/lib/models/tables/folder_entries.drift @@ -5,7 +5,7 @@ CREATE TABLE folder_entries ( name TEXT NOT NULL, parentFolderId TEXT, path TEXT NOT NULL, - + dateCreated DATETIME NOT NULL DEFAULT (strftime('%s','now')), lastUpdated DATETIME NOT NULL DEFAULT (strftime('%s','now')), isGhost BOOLEAN NOT NULL DEFAULT FALSE, diff --git a/lib/pages/app_router_delegate.dart b/lib/pages/app_router_delegate.dart index 4ce762df2b..c100cf00be 100644 --- a/lib/pages/app_router_delegate.dart +++ b/lib/pages/app_router_delegate.dart @@ -3,11 +3,13 @@ import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/authentication/login/views/login_page.dart'; import 'package:ardrive/blocs/activity/activity_cubit.dart'; import 'package:ardrive/blocs/blocs.dart'; +import 'package:ardrive/blocs/drive_detail/utils/breadcrumb_builder.dart'; import 'package:ardrive/blocs/feedback_survey/feedback_survey_cubit.dart'; import 'package:ardrive/blocs/prompt_to_snapshot/prompt_to_snapshot_bloc.dart'; import 'package:ardrive/components/components.dart'; import 'package:ardrive/components/feedback_survey.dart'; import 'package:ardrive/core/activity_tracker.dart'; +import 'package:ardrive/core/arfs/repository/folder_repository.dart'; import 'package:ardrive/dev_tools/app_dev_tools.dart'; import 'package:ardrive/entities/constants.dart'; import 'package:ardrive/models/models.dart'; @@ -194,6 +196,9 @@ class AppRouterDelegate extends RouterDelegate driveDao: context.read(), configService: context.read(), auth: context.read(), + breadcrumbBuilder: BreadcrumbBuilder( + context.read(), + ), ), child: MultiBlocListener( listeners: [ diff --git a/lib/pages/drive_detail/components/drive_detail_breadcrumb_row.dart b/lib/pages/drive_detail/components/drive_detail_breadcrumb_row.dart index 2cd1206a8d..657f91d271 100644 --- a/lib/pages/drive_detail/components/drive_detail_breadcrumb_row.dart +++ b/lib/pages/drive_detail/components/drive_detail_breadcrumb_row.dart @@ -1,14 +1,26 @@ part of '../drive_detail_page.dart'; +class BreadCrumbRowInfo { + final String text; + final String targetId; + + BreadCrumbRowInfo({ + required this.text, + required this.targetId, + }); +} + class DriveDetailBreadcrumbRow extends StatelessWidget { - final List _pathSegments; + final List _pathSegments; final String driveName; + final String rootFolderId; - DriveDetailBreadcrumbRow({ + const DriveDetailBreadcrumbRow({ Key? key, - required String path, + required List path, required this.driveName, - }) : _pathSegments = path.split('/').where((s) => s != '').toList(), + required this.rootFolderId, + }) : _pathSegments = path, super(key: key); @override @@ -48,11 +60,12 @@ class DriveDetailBreadcrumbRow extends StatelessWidget { Widget buildSegment(int index) { return GestureDetector( onTap: () { - final path = _pathSegments.sublist(0, index + 1).join('/'); - context.read().openFolder(path: '/$path'); + context.read().openFolder( + folderId: _pathSegments[index].targetId, + ); }, child: HoverText( - text: _pathSegments[index], + text: _pathSegments[index].text, style: segmentStyle(index), ), ); @@ -97,7 +110,7 @@ class DriveDetailBreadcrumbRow extends StatelessWidget { GestureDetector( onTap: () => context .read() - .openFolder(path: entities.rootPath), + .openFolder(folderId: rootFolderId), child: HoverText( text: driveName, style: segmentStyle(_pathSegments.length).copyWith( @@ -139,11 +152,11 @@ class DriveDetailBreadcrumbRow extends StatelessWidget { return [ ArDriveDropdownItem( onClick: () => context.read().openFolder( - path: '/${path.sublist(0, s.key + 1).join('/')}', + folderId: s.value.targetId, ), content: _buildDropdownItemContent( context, - s.value, + s.value.text, false, ), ), @@ -153,7 +166,7 @@ class DriveDetailBreadcrumbRow extends StatelessWidget { 0, ArDriveDropdownItem( onClick: () => context.read().openFolder( - path: entities.rootPath, + folderId: rootFolderId, ), content: _buildDropdownItemContent( context, @@ -179,17 +192,8 @@ class DriveDetailBreadcrumbRow extends StatelessWidget { String text, bool isDrive, ) { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - text, - style: ArDriveTypography.body.captionBold( - color: ArDriveTheme.of(context).themeData.colors.themeFgDefault, - ), - ), - ), + return ArDriveDropdownItemTile( + name: text, ); } } diff --git a/lib/pages/drive_detail/components/drive_detail_data_list.dart b/lib/pages/drive_detail/components/drive_detail_data_list.dart index b70493c8a0..3674afdaaa 100644 --- a/lib/pages/drive_detail/components/drive_detail_data_list.dart +++ b/lib/pages/drive_detail/components/drive_detail_data_list.dart @@ -27,7 +27,6 @@ abstract class ArDriveDataTableItem extends IndexedItem { final String? fileStatusFromTransactions; final String id; final String driveId; - final String path; final bool isOwner; final bool isHidden; @@ -41,7 +40,6 @@ abstract class ArDriveDataTableItem extends IndexedItem { this.licenseType, required this.contentType, this.fileStatusFromTransactions, - required this.path, required int index, required this.isOwner, this.isHidden = false, @@ -56,7 +54,6 @@ class DriveDataItem extends ArDriveDataTableItem { required super.lastUpdated, required super.dateCreated, super.contentType = 'drive', - super.path = '', required super.index, required super.isOwner, super.isHidden, @@ -77,7 +74,6 @@ class FolderDataTableItem extends ArDriveDataTableItem { required super.lastUpdated, required super.dateCreated, required super.contentType, - required super.path, super.fileStatusFromTransactions, super.isHidden, required super.index, @@ -108,7 +104,6 @@ class FileDataTableItem extends ArDriveDataTableItem { required super.size, required super.dateCreated, required super.contentType, - required super.path, super.isHidden, super.fileStatusFromTransactions, required super.index, @@ -240,7 +235,7 @@ Widget _buildDataListContent( final cubit = context.read(); if (item is FolderDataTableItem) { if (item.id == cubit.selectedItem?.id) { - cubit.openFolder(path: item.path); + cubit.openFolder(folderId: item.id); } else { cubit.selectDataItem(item); } @@ -290,7 +285,7 @@ Widget _buildDataListContent( final cubit = context.read(); if (row is FolderDataTableItem) { if (row.id == cubit.selectedItem?.id) { - cubit.openFolder(path: folder.path); + cubit.openFolder(folderId: row.id); } else { cubit.selectDataItem(row); } @@ -384,7 +379,6 @@ class DriveDataTableItemMapper { ) { return FileDataTableItem( isOwner: isOwner, - path: file.path, lastModifiedDate: file.lastModifiedDate, name: file.name, size: file.size, @@ -419,7 +413,6 @@ class DriveDataTableItemMapper { isOwner: isOwner, isGhostFolder: folderEntry.isGhost, index: index, - path: folderEntry.path, driveId: folderEntry.driveId, folderId: folderEntry.id, parentFolderId: folderEntry.parentFolderId, @@ -454,7 +447,6 @@ class DriveDataTableItemMapper { static FileDataTableItem fromRevision(FileRevision revision, bool isOwner) { return FileDataTableItem( isOwner: isOwner, - path: '', lastModifiedDate: revision.lastModifiedDate, name: revision.name, size: revision.size, diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index e5c78e6a92..aa7112f2d4 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -287,8 +287,9 @@ class _DriveDetailPageState extends State { content: Row( children: [ DriveDetailBreadcrumbRow( - path: - driveDetailState.folderInView.folder.path, + rootFolderId: driveDetailState + .currentDrive.rootFolderId, + path: driveDetailState.pathSegments, driveName: driveDetailState.currentDrive.name, ), const Spacer(), @@ -750,7 +751,7 @@ class _DriveDetailPageState extends State { Flexible( child: MobileFolderNavigation( driveName: state.currentDrive.name, - path: state.folderInView.folder.path, + path: state.pathSegments, isShowingHiddenFiles: isShowingHiddenFiles, ), ), @@ -813,7 +814,7 @@ class ArDriveItemListTile extends StatelessWidget { onTap: () { final cubit = context.read(); if (item is FolderDataTableItem) { - cubit.openFolder(path: item.path); + cubit.openFolder(folderId: item.id); } else if (item is FileDataTableItem) { if (item.id == cubit.selectedItem?.id) { cubit.toggleSelectedItemDetails(); @@ -904,7 +905,7 @@ class ArDriveItemListTile extends StatelessWidget { } class MobileFolderNavigation extends StatelessWidget { - final String path; + final List path; final String driveName; final bool isShowingHiddenFiles; @@ -925,9 +926,13 @@ class MobileFolderNavigation extends StatelessWidget { Expanded( child: InkWell( onTap: () { - context - .read() - .openFolder(path: getParentFolderPath(path)); + String? targetId; + + if (path.isNotEmpty) { + targetId = path.first.targetId; + } + + context.read().openFolder(folderId: targetId); }, child: Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -947,7 +952,9 @@ class MobileFolderNavigation extends StatelessWidget { ? const EdgeInsets.only(left: 16, top: 6, bottom: 6) : EdgeInsets.zero, child: Text( - _pathToName(path), + _pathToName( + path.isEmpty ? driveName : path.last.text, + ), style: ArDriveTypography.body.buttonNormalBold(), ), ), diff --git a/lib/services/arweave/arweave_service.dart b/lib/services/arweave/arweave_service.dart index 26a6774352..f77990d3a6 100644 --- a/lib/services/arweave/arweave_service.dart +++ b/lib/services/arweave/arweave_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:collection'; import 'dart:convert'; import 'package:ardrive/core/crypto/crypto.dart'; @@ -267,13 +268,21 @@ class ArweaveService { entityTxs.map( (model) async { final entity = model.transactionCommonMixin; - final tags = entity.tags; - final isSnapshot = tags.any( - (tag) => - tag.name == EntityTag.entityType && - tag.value == EntityTypeTag.snapshot.toString(), + + final tags = HashMap.fromIterable( + entity.tags, + key: (tag) => tag.name, + value: (tag) => tag.value, ); + if (driveKey != null && tags[EntityTag.cipherIv] == null) { + logger.d('skipping unnecessary request for a broken entity'); + return Uint8List(0); + } + + final isSnapshot = + tags[EntityTag.entityType] == EntityTypeTag.snapshot; + // don't fetch data for snapshots if (isSnapshot) { logger.d('skipping unnecessary request for snapshot data'); @@ -297,6 +306,18 @@ class ArweaveService { for (var i = 0; i < entityTxs.length; i++) { final transaction = entityTxs[i].transactionCommonMixin; + + final tags = HashMap.fromIterable( + transaction.tags, + key: (tag) => tag.name, + value: (tag) => tag.value, + ); + + if (driveKey != null && tags[EntityTag.cipherIv] == null) { + logger.d('skipping unnecessary request for a broken entity'); + continue; + } + // If we encounter a transaction that has yet to be mined, we stop moving through history. // We can continue once the transaction is mined. if (transaction.block == null) { @@ -310,7 +331,7 @@ class ArweaveService { } try { - final entityType = transaction.getTag(EntityTag.entityType); + final entityType = tags[EntityTag.entityType]; final rawEntityData = entityDatas[i]; await metadataCache.put(transaction.id, rawEntityData); @@ -586,11 +607,13 @@ class ArweaveService { /// /// Returns `null` if no valid drive is found or the provided `driveKey` is incorrect. Future getLatestDriveEntityWithId( - String driveId, [ + String driveId, { + String? driveOwner, SecretKey? driveKey, int maxRetries = defaultMaxRetries, - ]) async { - final driveOwner = await getOwnerForDriveEntityWithId(driveId); + }) async { + driveOwner ??= await getOwnerForDriveEntityWithId(driveId); + if (driveOwner == null) { return null; } @@ -737,7 +760,9 @@ class ArweaveService { /// Gets the owner of the drive sorted by blockheight. /// Returns `null` if no valid drive is found or the provided `driveKey` is incorrect. - Future getOwnerForDriveEntityWithId(String driveId) async { + Future getOwnerForDriveEntityWithId( + String driveId, + ) async { String cursor = ''; while (true) { @@ -803,7 +828,8 @@ class ArweaveService { return await getLatestDriveEntityWithId( checkDriveId, - checkDriveKey, + driveOwner: await wallet.getAddress(), + driveKey: checkDriveKey, ); } diff --git a/lib/sync/domain/cubit/sync_cubit.dart b/lib/sync/domain/cubit/sync_cubit.dart index 62b5dfd813..23608e5eb8 100644 --- a/lib/sync/domain/cubit/sync_cubit.dart +++ b/lib/sync/domain/cubit/sync_cubit.dart @@ -6,7 +6,6 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/prompt_to_snapshot/prompt_to_snapshot_bloc.dart'; import 'package:ardrive/blocs/prompt_to_snapshot/prompt_to_snapshot_event.dart'; import 'package:ardrive/core/activity_tracker.dart'; -import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/sync/constants.dart'; import 'package:ardrive/sync/domain/ghost_folder.dart'; @@ -103,13 +102,6 @@ class SyncCubit extends Cubit { final isTimerDurationReadyToSync = minutesSinceLastSync >= syncInterval; if (!isTimerDurationReadyToSync) { - logger.d( - 'Cannot restart sync when the window is focused. Is it currently' - ' active? ${!isClosed}.' - ' Last sync occurred $minutesSinceLastSync seconds ago, but it' - ' should be at least $syncInterval seconds.', - ); - return; } } @@ -260,7 +252,6 @@ class SyncCubit extends Cubit { } int calculateSyncLastBlockHeight(int lastBlockHeight) { - logger.d('Calculating sync last block height: $lastBlockHeight'); if (_lastSync != null) { return lastBlockHeight; } else { @@ -268,22 +259,6 @@ class SyncCubit extends Cubit { } } - // Exposing this for use by create folder functions since they need to update - // folder tree - Future generateFsEntryPaths( - String driveId, - Map foldersByIdMap, - Map filesByIdMap, - ) async { - logger.i('Generating fs entry paths...'); - ghostFolders = await _syncRepository.generateFsEntryPaths( - driveId: driveId, - foldersByIdMap: foldersByIdMap, - filesByIdMap: filesByIdMap, - ghostFolders: ghostFolders, - ); - } - @override void onError(Object error, StackTrace stackTrace) { logger.e('An error occured on SyncCubit', error, stackTrace); @@ -301,7 +276,7 @@ class SyncCubit extends Cubit { @override Future close() async { - logger.d('Closing SyncCubit instance'); + logger.i('Closing SyncCubit instance'); await _syncSub?.cancel(); await _arconnectSyncSub?.cancel(); await _restartOnFocusStreamSubscription?.cancel(); @@ -314,6 +289,6 @@ class SyncCubit extends Cubit { await super.close(); - logger.d('SyncCubit closed'); + logger.i('SyncCubit closed'); } } diff --git a/lib/sync/domain/repositories/sync_repository.dart b/lib/sync/domain/repositories/sync_repository.dart index 4cefb53b44..7b4fb54896 100644 --- a/lib/sync/domain/repositories/sync_repository.dart +++ b/lib/sync/domain/repositories/sync_repository.dart @@ -80,13 +80,6 @@ abstract class SyncRepository { Future getCurrentBlockHeight(); - Future> generateFsEntryPaths({ - required String driveId, - required Map foldersByIdMap, - required Map filesByIdMap, - required Map ghostFolders, - }); - factory SyncRepository({ required ArweaveService arweave, required DriveDao driveDao, @@ -111,6 +104,9 @@ class _SyncRepository implements SyncRepository { final LicenseService _licenseService; final BatchProcessor _batchProcessor; + final Map _ghostFolders = {}; + final Set _folderIds = {}; + DateTime? _lastSync; _SyncRepository({ @@ -155,13 +151,10 @@ class _SyncRepository implements SyncRepository { ), ); - final ghostFolders = {}; - final driveSyncProcesses = drives.map((drive) async* { yield* _syncDrive( drive.id, cipherKey: cipherKey, - ghostFolders: ghostFolders, lastBlockHeight: syncDeep ? 0 : _calculateSyncLastBlockHeight(drive.lastBlockHeight!), @@ -206,11 +199,14 @@ class _SyncRepository implements SyncRepository { await createGhosts( driveDao: _driveDao, ownerAddress: await wallet?.getAddress(), - ghostFolders: ghostFolders, + ghostFolders: _ghostFolders, ); /// Clear the ghost folders after they are created - ghostFolders.clear(); + _ghostFolders.clear(); + + /// Clear the folder ids after they are created + _folderIds.clear(); logger.i('Ghosts created...'); @@ -270,7 +266,6 @@ class _SyncRepository implements SyncRepository { currentBlockHeight: 0, transactionParseBatchSize: 200, txFechedCallback: txFechedCallback, - ghostFolders: {}, // No ghost folders to start with ); } @@ -314,11 +309,11 @@ class _SyncRepository implements SyncRepository { driveId: drive.id, parentFolderId: drive.rootFolderId, name: ghostFolder.folderId, - path: rootPath, lastUpdated: DateTime.now(), isGhost: true, dateCreated: DateTime.now(), isHidden: ghostFolder.isHidden, + path: '', ); await driveDao.into(driveDao.folderEntries).insert(folderEntry); ghostFoldersByDrive.putIfAbsent( @@ -326,19 +321,6 @@ class _SyncRepository implements SyncRepository { () => {folderEntry.id: folderEntry.toCompanion(false)}, ); } - await Future.wait( - [ - ...ghostFoldersByDrive.entries.map( - (entry) => _generateFsEntryPaths( - driveDao: driveDao, - driveId: entry.key, - foldersByIdMap: entry.value, - ghostFolders: ghostFolders, - filesByIdMap: {}, - ), - ), - ], - ); } @override @@ -370,22 +352,6 @@ class _SyncRepository implements SyncRepository { ); } - @override - Future> generateFsEntryPaths({ - required String driveId, - required Map foldersByIdMap, - required Map filesByIdMap, - required Map ghostFolders, - }) { - return _generateFsEntryPaths( - driveDao: _driveDao, - driveId: driveId, - foldersByIdMap: foldersByIdMap, - filesByIdMap: filesByIdMap, - ghostFolders: ghostFolders, - ); - } - int _calculateSyncLastBlockHeight(int lastBlockHeight) { logger.d('Calculating sync last block height: $lastBlockHeight'); if (_lastSync != null) { @@ -539,7 +505,6 @@ class _SyncRepository implements SyncRepository { required int currentBlockHeight, required int lastBlockHeight, required int transactionParseBatchSize, - required Map ghostFolders, required String ownerAddress, Function(String driveId, int txCount)? txFechedCallback, }) async* { @@ -644,11 +609,6 @@ class _SyncRepository implements SyncRepository { ((currentBlockHeight - block.height) / totalBlockHeightDifference)); } - logger.d( - 'The transaction block is null. Transaction node id: ${t.transactionCommonMixin.id}', - ); - - logger.d('New fetch-phase percentage: $fetchPhasePercentage'); /// if the block is null, we don't calculate and keep the same percentage return fetchPhasePercentage; @@ -671,7 +631,6 @@ class _SyncRepository implements SyncRepository { } } - logger.d('Adding transaction ${t.transactionCommonMixin.id}'); transactions.add(t); /// We can only calculate the fetch percentage if we have the `firstBlockHeight` @@ -680,7 +639,7 @@ class _SyncRepository implements SyncRepository { fetchPhasePercentage = calculatePercentageBasedOnBlockHeights(); } else { // If the difference is zero means that the first phase was concluded. - logger.d('The first phase just finished!'); + logger.d('The syncs first phase just finished!'); fetchPhasePercentage = 1; } final percentage = @@ -701,7 +660,6 @@ class _SyncRepository implements SyncRepository { try { yield* _parseDriveTransactionsIntoDatabaseEntities( - ghostFolders: ghostFolders, transactions: transactions, drive: drive, driveKey: driveKey, @@ -813,7 +771,7 @@ class _SyncRepository implements SyncRepository { required int currentBlockHeight, required int batchSize, required SnapshotDriveHistory snapshotDriveHistory, - required Map ghostFolders, + // required Map ghostFolders, required String ownerAddress, }) async* { final numberOfDriveEntitiesToParse = transactions.length; @@ -845,12 +803,6 @@ class _SyncRepository implements SyncRepository { list: transactions, batchSize: batchSize, endOfBatchCallback: (items) async* { - final isReadingFromSnapshot = snapshotDriveHistory.items.isNotEmpty; - - if (!isReadingFromSnapshot) { - logger.d('Getting metadata from drive ${drive.id}'); - } - final entityHistory = await _arweave.createDriveEntityHistoryFromTransactions( items, @@ -893,6 +845,19 @@ class _SyncRepository implements SyncRepository { newEntities: newEntities.whereType(), ); + for (final entity in latestFileRevisions) { + if (!_folderIds.contains(entity.parentFolderId.value)) { + _ghostFolders.putIfAbsent( + entity.parentFolderId.value, + () => GhostFolder( + driveId: drive.id, + folderId: entity.parentFolderId.value, + isHidden: false, + ), + ); + } + } + // Check and handle cases where there's no more revisions final updatedDrive = latestDriveRevision != null ? await _computeRefreshedDriveFromRevision( @@ -929,16 +894,11 @@ class _SyncRepository implements SyncRepository { .updateFolderEntries(updatedFoldersById.values.toList()); await _driveDao.updateFileEntries(updatedFilesById.values.toList()); - await _generateFsEntryPaths( - ghostFolders: ghostFolders, - driveDao: _driveDao, - driveId: drive.id, - foldersByIdMap: updatedFoldersById, - filesByIdMap: updatedFilesById, - ); - numberOfDriveEntitiesParsed += updatedFoldersById.length + updatedFilesById.length; + + latestFolderRevisions.clear(); + latestFileRevisions.clear(); }); yield driveEntityParseProgress(); }); @@ -1043,6 +1003,7 @@ class _SyncRepository implements SyncRepository { required String driveId, required Iterable newEntities, }) async { + _folderIds.addAll(newEntities.map((e) => e.id!)); // The latest folder revisions, keyed by their entity ids. final latestRevisions = {}; @@ -1143,117 +1104,6 @@ Future> return updatedFoldersById; } -/// Generates paths for the folders (and their children) and files provided. -Future> _generateFsEntryPaths({ - required DriveDao driveDao, - required String driveId, - required Map foldersByIdMap, - required Map filesByIdMap, - required Map ghostFolders, -}) async { - final staleFolderTree = []; - for (final folder in foldersByIdMap.values) { - // Get trees of the updated folders and files for path generation. - final tree = await driveDao.getFolderTree(driveId, folder.id.value); - - // Remove any trees that are a subset of another. - var newTreeIsSubsetOfExisting = false; - var newTreeIsSupersetOfExisting = false; - for (final existingTree in staleFolderTree) { - if (existingTree.searchForFolder(tree.folder.id) != null) { - newTreeIsSubsetOfExisting = true; - } else if (tree.searchForFolder(existingTree.folder.id) != null) { - staleFolderTree.remove(existingTree); - staleFolderTree.add(tree); - newTreeIsSupersetOfExisting = true; - } - } - - if (!newTreeIsSubsetOfExisting && !newTreeIsSupersetOfExisting) { - staleFolderTree.add(tree); - } - } - - Future addMissingFolder(String folderId) async { - ghostFolders.putIfAbsent( - folderId, () => GhostFolder(folderId: folderId, driveId: driveId)); - } - - Future updateFolderTree(FolderNode node, String parentPath) async { - final folderId = node.folder.id; - // If this is the root folder, we should not include its name as part of the path. - final folderPath = node.folder.parentFolderId != null - ? '$parentPath/${node.folder.name}' - : rootPath; - - await driveDao - .updateFolderById(driveId, folderId) - .write(FolderEntriesCompanion(path: Value(folderPath))); - - for (final staleFileId in node.files.keys) { - final filePath = '$folderPath/${node.files[staleFileId]!.name}'; - - await driveDao - .updateFileById(driveId, staleFileId) - .write(FileEntriesCompanion(path: Value(filePath))); - } - - for (final staleFolder in node.subfolders) { - await updateFolderTree(staleFolder, folderPath); - } - } - - for (final treeRoot in staleFolderTree) { - // Get the path of this folder's parent. - String? parentPath; - if (treeRoot.folder.parentFolderId == null) { - parentPath = rootPath; - } else { - parentPath = (await driveDao - .folderById( - driveId: driveId, folderId: treeRoot.folder.parentFolderId!) - .map((f) => f.path) - .getSingleOrNull()); - } - if (parentPath != null) { - await updateFolderTree(treeRoot, parentPath); - } else { - await addMissingFolder( - treeRoot.folder.parentFolderId!, - ); - } - } - // Update paths of files whose parent folders were not updated. - final staleOrphanFiles = filesByIdMap.values - .where((f) => !foldersByIdMap.containsKey(f.parentFolderId)); - for (final staleOrphanFile in staleOrphanFiles) { - if (staleOrphanFile.parentFolderId.value.isNotEmpty) { - final parentPath = await driveDao - .folderById( - driveId: driveId, folderId: staleOrphanFile.parentFolderId.value) - .map((f) => f.path) - .getSingleOrNull(); - - if (parentPath != null) { - final filePath = '$parentPath/${staleOrphanFile.name.value}'; - - await driveDao.writeToFile(FileEntriesCompanion( - id: staleOrphanFile.id, - driveId: staleOrphanFile.driveId, - path: Value(filePath))); - } else { - logger.d( - 'Add missing folder to file with id ${staleOrphanFile.parentFolderId}'); - - await addMissingFolder( - staleOrphanFile.parentFolderId.value, - ); - } - } - } - return ghostFolders; -} - /// Computes the refreshed drive entries from the provided revisions and returns them as a map keyed by their ids. Future _computeRefreshedDriveFromRevision({ required DriveDao driveDao, diff --git a/lib/sync/utils/batch_processor.dart b/lib/sync/utils/batch_processor.dart index d35c5b6fec..8c15f526b9 100644 --- a/lib/sync/utils/batch_processor.dart +++ b/lib/sync/utils/batch_processor.dart @@ -26,5 +26,7 @@ class BatchProcessor { yield* endOfBatchCallback(currentBatch); } + + list.clear(); } } diff --git a/lib/utils/metadata_cache.dart b/lib/utils/metadata_cache.dart index 86fcb6e1d0..34fcd7002b 100644 --- a/lib/utils/metadata_cache.dart +++ b/lib/utils/metadata_cache.dart @@ -28,7 +28,6 @@ class MetadataCache { // FIXME: check for quota before attempting to write to cache try { - logger.d('Putting $key in metadata cache'); await _cache.putIfAbsent(key, data); } catch (e, s) { logger.e('Failed to put $key in metadata cache', e, s); @@ -45,11 +44,7 @@ class MetadataCache { Future get(String key) async { try { final value = await _cache.get(key); - if (value != null) { - logger.d('Cache hit for $key in metadata cache'); - } else { - logger.d('Cache miss for $key in metadata cache'); - } + return value; } catch (e, s) { logger.e('Failed to get $key from metadata cache', e, s); diff --git a/lib/utils/upload_plan_utils.dart b/lib/utils/upload_plan_utils.dart index b19d355505..dd23e258e6 100644 --- a/lib/utils/upload_plan_utils.dart +++ b/lib/utils/upload_plan_utils.dart @@ -53,9 +53,6 @@ class UploadPlanUtils { for (var file in files) { final fileName = file.ioFile.name; - // If path is a blob from drag and drop, use file name. Else use the path field from folder upload - final filePath = '${targetFolder.path}/${file.getIdentifier()}'; - final parentFolderId = foldersByPath[getDirname(file.getIdentifier())]; final fileSize = await file.ioFile.length; @@ -90,7 +87,6 @@ class UploadPlanUtils { if (fileSize < bundleSizeLimit) { fileDataItemUploadHandles[fileEntity.id!] = FileDataItemUploadHandle( entity: fileEntity, - path: filePath, file: file, driveKey: driveKey, fileKey: fileKey, @@ -102,7 +98,6 @@ class UploadPlanUtils { } else { fileV2UploadHandles[fileEntity.id!] = FileV2UploadHandle( entity: fileEntity, - path: filePath, file: file, driveKey: driveKey, fileKey: fileKey, diff --git a/test/authentication/ardrive_auth_test.dart b/test/authentication/ardrive_auth_test.dart index b8c5ad8dba..01b0b4d827 100644 --- a/test/authentication/ardrive_auth_test.dart +++ b/test/authentication/ardrive_auth_test.dart @@ -155,12 +155,21 @@ void main() { when(() => mockUserRepository.hasUser()) .thenAnswer((invocation) => Future.value(true)); - when(() => mockArweaveService.getLatestDriveEntityWithId( - any(), any(), any())) - .thenAnswer((invocation) => Future.value(DriveEntity( - id: 'some_id', - rootFolderId: 'some_id', - ))); + when( + () => mockArweaveService.getLatestDriveEntityWithId( + any(), + driveKey: any(named: 'driveKey'), + maxRetries: any(named: 'maxRetries'), + driveOwner: any(named: 'driveOwner'), + ), + ).thenAnswer( + (invocation) => Future.value( + DriveEntity( + id: 'some_id', + rootFolderId: 'some_id', + ), + ), + ); when(() => mockUserRepository.deleteUser()) .thenAnswer((invocation) async {}); @@ -188,9 +197,12 @@ void main() { test( 'should return the user, and save the password on secure storage when has private drives and login with sucess.', () async { - when(() => mockArweaveService.getFirstPrivateDriveTxId(wallet, - maxRetries: any(named: 'maxRetries'))) - .thenAnswer((_) async => 'some_id'); + when( + () => mockArweaveService.getFirstPrivateDriveTxId( + wallet, + maxRetries: any(named: 'maxRetries'), + ), + ).thenAnswer((_) async => 'some_id'); when(() => mockBiometricAuthentication.isEnabled()) .thenAnswer((_) async => true); @@ -211,12 +223,21 @@ void main() { when(() => mockUserRepository.hasUser()) .thenAnswer((invocation) => Future.value(true)); - when(() => mockArweaveService.getLatestDriveEntityWithId( - any(), any(), any())) - .thenAnswer((invocation) => Future.value(DriveEntity( - id: 'some_id', - rootFolderId: 'some_id', - ))); + when( + () => mockArweaveService.getLatestDriveEntityWithId( + any(), + driveKey: any(named: 'driveKey'), + maxRetries: any(named: 'maxRetries'), + driveOwner: any(named: 'driveOwner'), + ), + ).thenAnswer( + (invocation) => Future.value( + DriveEntity( + id: 'some_id', + rootFolderId: 'some_id', + ), + ), + ); when(() => mockUserRepository.deleteUser()) .thenAnswer((invocation) async {}); @@ -564,9 +585,12 @@ void main() { cipherKey: SecretKey([]), profileType: ProfileType.json, ); - when(() => mockArweaveService.getFirstPrivateDriveTxId(wallet, - maxRetries: any(named: 'maxRetries'))) - .thenAnswer((_) async => 'some_id'); + when( + () => mockArweaveService.getFirstPrivateDriveTxId( + wallet, + maxRetries: any(named: 'maxRetries'), + ), + ).thenAnswer((_) async => 'some_id'); when(() => mockBiometricAuthentication.isEnabled()) .thenAnswer((_) async => false); when( @@ -578,12 +602,17 @@ void main() { ).thenAnswer((invocation) => Future.value(SecretKey([]))); when(() => mockUserRepository.hasUser()) .thenAnswer((invocation) => Future.value(true)); - when(() => mockArweaveService.getLatestDriveEntityWithId( - any(), any(), any())) - .thenAnswer((invocation) => Future.value(DriveEntity( - id: 'some_id', - rootFolderId: 'some_id', - ))); + when( + () => mockArweaveService.getLatestDriveEntityWithId( + any(), + driveKey: any(named: 'driveKey'), + maxRetries: any(named: 'maxRetries'), + driveOwner: any(named: 'driveOwner'), + ), + ).thenAnswer((invocation) => Future.value(DriveEntity( + id: 'some_id', + rootFolderId: 'some_id', + ))); when(() => mockUserRepository.deleteUser()) .thenAnswer((invocation) async {}); when(() => mockUserRepository.saveUser( @@ -642,12 +671,17 @@ void main() { ).thenAnswer((invocation) => Future.value(SecretKey([]))); when(() => mockUserRepository.hasUser()) .thenAnswer((invocation) => Future.value(true)); - when(() => mockArweaveService.getLatestDriveEntityWithId( - any(), any(), any())) - .thenAnswer((invocation) => Future.value(DriveEntity( - id: 'some_id', - rootFolderId: 'some_id', - ))); + when( + () => mockArweaveService.getLatestDriveEntityWithId( + any(), + driveKey: any(named: 'driveKey'), + maxRetries: any(named: 'maxRetries'), + driveOwner: any(named: 'driveOwner'), + ), + ).thenAnswer((invocation) => Future.value(DriveEntity( + id: 'some_id', + rootFolderId: 'some_id', + ))); when(() => mockUserRepository.deleteUser()) .thenAnswer((invocation) async {}); when(() => mockUserRepository.saveUser( @@ -726,9 +760,10 @@ void main() { 'should call getFirstPrivateDriveTxId only once when has private drives and login with sucess. ', () async { // arrange - when(() => mockArweaveService.getFirstPrivateDriveTxId(wallet, - maxRetries: any(named: 'maxRetries'))) - .thenAnswer((_) async => 'some_id'); + when(() => mockArweaveService.getFirstPrivateDriveTxId( + wallet, + maxRetries: any(named: 'maxRetries'), + )).thenAnswer((_) async => 'some_id'); when(() => mockBiometricAuthentication.isEnabled()) .thenAnswer((_) async => false); @@ -744,8 +779,10 @@ void main() { when(() => mockUserRepository.hasUser()) .thenAnswer((invocation) => Future.value(true)); - when(() => mockArweaveService.getLatestDriveEntityWithId( - any(), any(), any())) + when(() => mockArweaveService.getLatestDriveEntityWithId(any(), + driveKey: any(named: 'driveKey'), + maxRetries: any(named: 'maxRetries'), + driveOwner: any(named: 'driveOwner'))) .thenAnswer((invocation) => Future.value(DriveEntity( id: 'some_id', rootFolderId: 'some_id', @@ -764,8 +801,12 @@ void main() { await arDriveAuth.login(wallet, 'password', ProfileType.json); await arDriveAuth.login(wallet, 'password', ProfileType.json); - verify(() => mockArweaveService.getFirstPrivateDriveTxId(wallet, - maxRetries: any(named: 'maxRetries'))).called(1); + verify( + () => mockArweaveService.getFirstPrivateDriveTxId( + wallet, + maxRetries: any(named: 'maxRetries'), + ), + ).called(1); }); test( diff --git a/test/blocs/drive_attach_cubit_test.dart b/test/blocs/drive_attach_cubit_test.dart index 3f048cc0fc..1afadbf4c3 100644 --- a/test/blocs/drive_attach_cubit_test.dart +++ b/test/blocs/drive_attach_cubit_test.dart @@ -68,7 +68,7 @@ void main() { when(() => arweave.getLatestDriveEntityWithId( validPrivateDriveId, - validPrivateDriveKey, + driveKey: validPrivateDriveKey, )).thenAnswer( (_) => Future.value( DriveEntity( @@ -145,7 +145,7 @@ void main() { setUp(() async { when(() => arweave.getLatestDriveEntityWithId( validPrivateDriveId, - invalidDriveKey, + driveKey: invalidDriveKey, )).thenAnswer((_) => Future.value(null)); }); diff --git a/test/blocs/drive_detail/utils/breadcrumb_builder_test.dart b/test/blocs/drive_detail/utils/breadcrumb_builder_test.dart new file mode 100644 index 0000000000..eaf76012ac --- /dev/null +++ b/test/blocs/drive_detail/utils/breadcrumb_builder_test.dart @@ -0,0 +1,184 @@ +import 'package:ardrive/blocs/drive_detail/utils/breadcrumb_builder.dart'; +import 'package:ardrive/models/database/database.dart'; +import 'package:ardrive/pages/drive_detail/drive_detail_page.dart'; // Adjust if necessary +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../test_utils/mocks.dart'; + +class FakeBreadCrumbRowInfo extends Fake implements BreadCrumbRowInfo {} + +void main() { + group('BreadcrumbBuilder Tests', () { + late MockFolderRepository mockFolderRepository; + late BreadcrumbBuilder breadcrumbBuilder; + + setUp(() { + mockFolderRepository = MockFolderRepository(); + breadcrumbBuilder = BreadcrumbBuilder(mockFolderRepository); + + // Register fallback values + registerFallbackValue(FakeBreadCrumbRowInfo()); + }); + + test('returns empty breadcrumbs for root folder', () async { + final breadcrumbs = await breadcrumbBuilder.buildForFolder( + folderId: 'rootFolderId', + rootFolderId: 'rootFolderId', + driveId: 'driveId', + ); + + expect(breadcrumbs, isEmpty); + }); + + test('returns single breadcrumb for single folder', () async { + when(() => mockFolderRepository.getLatestFolderRevisionInfo(any(), any())) + .thenAnswer((_) async => FolderRevision( + folderId: 'folderId', + driveId: 'driveId', + name: 'Folder', + metadataTxId: 'metaTxId', + dateCreated: DateTime.now(), + action: 'create', + isHidden: false, + )); + + final breadcrumbs = await breadcrumbBuilder.buildForFolder( + folderId: 'folderId', + rootFolderId: 'rootFolderId', + driveId: 'driveId', + ); + + expect(breadcrumbs, hasLength(1)); + expect(breadcrumbs.first.text, 'Folder'); + }); + + test('builds correct breadcrumbs for nested folders', () async { + when(() => mockFolderRepository + .getLatestFolderRevisionInfo('driveId', 'subFolderId')) + .thenAnswer((_) async => FolderRevision( + folderId: 'subFolderId', + parentFolderId: 'parentFolderId', + driveId: 'driveId', + name: 'SubFolder', + metadataTxId: 'metaTxId', + dateCreated: DateTime.now(), + action: 'create', + isHidden: false, + )); + + when(() => mockFolderRepository + .getLatestFolderRevisionInfo('driveId', 'parentFolderId')) + .thenAnswer((_) async => FolderRevision( + folderId: 'parentFolderId', + parentFolderId: 'rootFolderId', // Linking to the root folder + driveId: 'driveId', + name: 'ParentFolder', + metadataTxId: 'metaTxId', + dateCreated: DateTime.now(), + action: 'create', + isHidden: false, + )); + + final breadcrumbs = await breadcrumbBuilder.buildForFolder( + folderId: 'subFolderId', + rootFolderId: 'rootFolderId', + driveId: 'driveId', + ); + + expect(breadcrumbs, hasLength(2)); + expect(breadcrumbs[0].text, 'ParentFolder'); + expect(breadcrumbs[1].text, 'SubFolder'); + }); + + test('throws for invalid starting folder ID', () async { + /// Mocking the repository to return null for any folder ID + when(() => mockFolderRepository.getLatestFolderRevisionInfo(any(), any())) + .thenAnswer((_) async => null); + + expect( + () => breadcrumbBuilder.buildForFolder( + folderId: 'invalidFolderId', + rootFolderId: 'rootFolderId', + driveId: 'driveId', + ), + throwsA(isA()), + ); + }); + + test('throws for Intermediate Missing Folder', () async { + /// Mocking the repository to return null for any folder ID + + /// FolderC is Valid and FolderB is missing + /// FolderA is linked to the root folder + when(() => mockFolderRepository + .getLatestFolderRevisionInfo('driveId', 'folderC')) + .thenAnswer((_) async => FolderRevision( + folderId: 'folderC', + parentFolderId: 'folderB', + driveId: 'driveId', + name: 'SubFolder', + metadataTxId: 'metaTxId', + dateCreated: DateTime.now(), + action: 'create', + isHidden: false, + )); + when(() => mockFolderRepository.getLatestFolderRevisionInfo( + 'driveId', 'folderB')).thenAnswer((_) async => null); + when(() => mockFolderRepository + .getLatestFolderRevisionInfo('driveId', 'parentFolderId')) + .thenAnswer((_) async => FolderRevision( + folderId: 'folderA', + parentFolderId: 'rootFolderId', // Linking to the root folder + driveId: 'driveId', + name: 'ParentFolder', + metadataTxId: 'metaTxId', + dateCreated: DateTime.now(), + action: 'create', + isHidden: false, + )); + + expect( + () => breadcrumbBuilder.buildForFolder( + folderId: 'folderC', + rootFolderId: 'rootFolderId', + driveId: 'driveId', + ), + throwsA(isA()), + ); + }); + test( + 'builds correct breadcrumbs for 10 levels deep (root folder does not count)', + () async { + // Set up a chain of folder revisions, each pointing to its parent + for (int i = 9; i >= 0; i--) { + final folderRevision = FolderRevision( + folderId: 'folderId$i', + driveId: 'driveId', + name: 'Folder $i', + parentFolderId: + i > 0 ? 'folderId${i - 1}' : null, // Linking to the parent folder + metadataTxId: 'metaTxId$i', + dateCreated: DateTime.now(), + action: 'create', + isHidden: false, + ); + + when(() => mockFolderRepository.getLatestFolderRevisionInfo( + 'driveId', 'folderId$i')).thenAnswer((_) async => folderRevision); + } + + final breadcrumbs = await breadcrumbBuilder.buildForFolder( + folderId: 'folderId9', // Deepest folder + rootFolderId: 'folderId0', // Root folder + driveId: 'driveId', + ); + + expect(breadcrumbs, hasLength(9)); + for (int i = 0; i < 9; i++) { + expect(breadcrumbs[i].text, 'Folder ${i + 1}'); + expect(breadcrumbs[i].targetId, 'folderId${i + 1}'); + } + }); + }); +} diff --git a/test/blocs/fs_entry_license_bloc_test.dart b/test/blocs/fs_entry_license_bloc_test.dart index 431b25627c..61dde0cc5b 100644 --- a/test/blocs/fs_entry_license_bloc_test.dart +++ b/test/blocs/fs_entry_license_bloc_test.dart @@ -66,25 +66,28 @@ void main() { // Create fake root folder for drive and sub folders batch.insertAll(db.folderEntries, [ FolderEntriesCompanion.insert( - id: rootFolderId, - driveId: driveId, - name: 'fake-drive-name', - path: '', - isHidden: const Value(false)), + id: rootFolderId, + driveId: driveId, + name: 'fake-drive-name', + isHidden: const Value(false), + path: '', + ), FolderEntriesCompanion.insert( - id: nestedFolderId, - driveId: driveId, - parentFolderId: Value(rootFolderId), - name: nestedFolderId, - path: '/$nestedFolderId', - isHidden: const Value(false)), + id: nestedFolderId, + driveId: driveId, + parentFolderId: Value(rootFolderId), + name: nestedFolderId, + isHidden: const Value(false), + path: '', + ), FolderEntriesCompanion.insert( - id: conflictTestFolderId, - driveId: driveId, - parentFolderId: Value(rootFolderId), - name: conflictTestFolderId, - path: '/$conflictTestFolderId', - isHidden: const Value(false)), + id: conflictTestFolderId, + driveId: driveId, + parentFolderId: Value(rootFolderId), + name: conflictTestFolderId, + isHidden: const Value(false), + path: '', + ), ]); // Insert fake files batch.insertAll( @@ -95,17 +98,18 @@ void main() { (i) { final fileId = '$rootFolderId$i'; return FileEntriesCompanion.insert( - id: fileId, - driveId: driveId, - parentFolderId: rootFolderId, - name: fileId, - path: '/$fileId', - dataTxId: '${fileId}Data', - size: 500, - dateCreated: Value(defaultDate), - lastModifiedDate: defaultDate, - dataContentType: const Value(''), - isHidden: const Value(false)); + id: fileId, + driveId: driveId, + parentFolderId: rootFolderId, + name: fileId, + dataTxId: '${fileId}Data', + size: 500, + dateCreated: Value(defaultDate), + lastModifiedDate: defaultDate, + dataContentType: const Value(''), + isHidden: const Value(false), + path: '', + ); }, ), ...List.generate( @@ -113,17 +117,18 @@ void main() { (i) { final fileId = '$conflictTestFolderId$i'; return FileEntriesCompanion.insert( - id: fileId, - driveId: driveId, - parentFolderId: conflictTestFolderId, - name: fileId, - path: '/$fileId', - dataTxId: '${fileId}Data', - size: 500, - dateCreated: Value(defaultDate), - lastModifiedDate: defaultDate, - dataContentType: const Value(''), - isHidden: const Value(false)); + id: fileId, + driveId: driveId, + parentFolderId: conflictTestFolderId, + name: fileId, + dataTxId: '${fileId}Data', + size: 500, + dateCreated: Value(defaultDate), + lastModifiedDate: defaultDate, + dataContentType: const Value(''), + isHidden: const Value(false), + path: '', + ); }, ), ], diff --git a/test/blocs/fs_entry_move_bloc_test.dart b/test/blocs/fs_entry_move_bloc_test.dart index 07fb9d7f63..b79b1a6885 100644 --- a/test/blocs/fs_entry_move_bloc_test.dart +++ b/test/blocs/fs_entry_move_bloc_test.dart @@ -69,24 +69,24 @@ void main() { id: rootFolderId, driveId: driveId, name: 'fake-drive-name', - path: '', isHidden: const Value(false), + path: '', ), FolderEntriesCompanion.insert( id: nestedFolderId, driveId: driveId, parentFolderId: Value(rootFolderId), name: nestedFolderId, - path: '/$nestedFolderId', isHidden: const Value(false), + path: '', ), FolderEntriesCompanion.insert( id: conflictTestFolderId, driveId: driveId, parentFolderId: Value(rootFolderId), name: conflictTestFolderId, - path: '/$conflictTestFolderId', isHidden: const Value(false), + path: '', ), ]); // Insert fake files @@ -102,13 +102,13 @@ void main() { driveId: driveId, parentFolderId: rootFolderId, name: fileId, - path: '/$fileId', dataTxId: '${fileId}Data', size: 500, dateCreated: Value(defaultDate), lastModifiedDate: defaultDate, dataContentType: const Value(''), isHidden: const Value(false), + path: '', ); }, ), @@ -121,13 +121,13 @@ void main() { driveId: driveId, parentFolderId: conflictTestFolderId, name: fileId, - path: '/$fileId', dataTxId: '${fileId}Data', size: 500, dateCreated: Value(defaultDate), lastModifiedDate: defaultDate, dataContentType: const Value(''), isHidden: const Value(false), + path: '', ); }, ), @@ -245,9 +245,7 @@ void main() { ); turboUploadService = DontUseUploadService(); syncBloc = MockSyncBloc(); - when(() => syncBloc.generateFsEntryPaths(any(), any(), any())).thenAnswer( - (_) async => Future.value(), - ); + profileCubit = MockProfileCubit(); final keyBytes = Uint8List(32); diff --git a/test/blocs/upload_cubit_test.dart b/test/blocs/upload_cubit_test.dart index 71bb165011..c0212d1792 100644 --- a/test/blocs/upload_cubit_test.dart +++ b/test/blocs/upload_cubit_test.dart @@ -106,8 +106,8 @@ void main() { lastUpdated: tDefaultDate, name: '', parentFolderId: '', - path: '', isHidden: false, + path: '', )); registerFallbackValue(Drive( diff --git a/test/core/arfs/folder_repository_test.dart b/test/core/arfs/folder_repository_test.dart new file mode 100644 index 0000000000..379a28a9f8 --- /dev/null +++ b/test/core/arfs/folder_repository_test.dart @@ -0,0 +1,66 @@ +import 'package:ardrive/core/arfs/repository/folder_repository.dart'; +import 'package:ardrive/models/database/database.dart'; // Ensure this imports FolderRevision +import 'package:drift/drift.dart' as drift; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../test_utils/mocks.dart'; + +class MockSelectable extends Mock implements drift.Selectable {} + +void main() { + group('FolderRepository Tests', () { + late MockDriveDao mockDriveDao; + late FolderRepository folderRepository; + late MockSelectable selectable = + MockSelectable(); + setUp(() { + mockDriveDao = MockDriveDao(); + folderRepository = FolderRepository(mockDriveDao); + + // Setup mock responses + when(() => mockDriveDao.latestFolderRevisionByFolderId( + driveId: any(named: 'driveId'), folderId: any(named: 'folderId'))) + .thenReturn(selectable); // Default case for not found + when(() => selectable.getSingleOrNull()).thenAnswer((_) async => null); + }); + + test('getLatestFolderRevisionInfo returns a FolderRevision on valid input', + () async { + final expectedFolderRevision = FolderRevision( + folderId: 'validFolderId', + driveId: 'validDriveId', + name: 'TestFolder', + metadataTxId: 'meta123', + dateCreated: DateTime.now(), + action: 'create', + isHidden: false, + // Add more fields as necessary + ); + + when(() => mockDriveDao.latestFolderRevisionByFolderId( + driveId: 'validDriveId', folderId: 'validFolderId')) + .thenReturn(selectable); // Default case for not found + when(() => selectable.getSingleOrNull()).thenAnswer((_) async { + return expectedFolderRevision; + }); + + final folderRevision = await folderRepository.getLatestFolderRevisionInfo( + 'validDriveId', 'validFolderId'); + + expect(folderRevision, equals(expectedFolderRevision)); + // Verify all relevant fields + expect(folderRevision?.folderId, equals('validFolderId')); + expect(folderRevision?.driveId, equals('validDriveId')); + // Add more verifications as necessary + }); + + test('getLatestFolderRevisionInfo returns null when no data exists', + () async { + // The null setup is already done in setUp() + final folderRevision = await folderRepository.getLatestFolderRevisionInfo( + 'invalidDriveId', 'invalidFolderId'); + expect(folderRevision, isNull); + }); + }); +} diff --git a/test/core/upload/uploader_test.dart b/test/core/upload/uploader_test.dart index 44547d3d11..a2de2c953f 100644 --- a/test/core/upload/uploader_test.dart +++ b/test/core/upload/uploader_test.dart @@ -1012,11 +1012,11 @@ FolderEntry getFakeFolder() => FolderEntry( id: 'id', driveId: 'drive id', name: 'name', - path: 'path', dateCreated: DateTime.now(), lastUpdated: DateTime.now(), isGhost: false, isHidden: false, + path: '', ); Drive getFakeDrive() => Drive( diff --git a/test/entities/manifest_data_test.dart b/test/entities/manifest_data_test.dart index a4eb65d853..41ce244c34 100644 --- a/test/entities/manifest_data_test.dart +++ b/test/entities/manifest_data_test.dart @@ -1,9 +1,12 @@ +import 'package:ardrive/core/arfs/repository/file_repository.dart'; +import 'package:ardrive/core/arfs/repository/folder_repository.dart'; import 'package:ardrive/entities/entities.dart'; import 'package:ardrive/entities/manifest_data.dart'; import 'package:ardrive/models/daos/daos.dart'; import 'package:ardrive/models/database/database.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/utils.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:test/test.dart'; @@ -15,47 +18,49 @@ void main() { const stubTxId = '0000000000000000000000000000000000000000001'; final stubCurrentDate = DateTime.now(); + late final FileRepository fileRepository; + late final FolderRepository folderRepository; + final stubRootFolderEntry = FolderEntry( - id: stubEntityId, + id: 'stubRootFolderEntry', dateCreated: stubCurrentDate, driveId: stubEntityId, isGhost: false, parentFolderId: stubEntityId, - path: '/root-folder', name: 'root-folder', lastUpdated: stubCurrentDate, isHidden: false, + path: '', ); final stubParentFolderEntry = FolderEntry( - id: stubEntityId, + id: 'stubParentFolderEntry', dateCreated: stubCurrentDate, driveId: stubEntityId, isGhost: false, parentFolderId: stubEntityId, - path: '/root-folder/parent-folder', name: 'parent-folder', lastUpdated: stubCurrentDate, isHidden: false, + path: '', ); final stubChildFolderEntry = FolderEntry( - id: stubEntityId, + id: 'stubChildFolderEntry', dateCreated: stubCurrentDate, driveId: stubEntityId, isGhost: false, parentFolderId: stubEntityId, - path: '/root-folder/parent-folder/child-folder', name: 'child-folder', lastUpdated: stubCurrentDate, isHidden: false, + path: '', ); final stubFileInRoot1 = FileEntry( dataTxId: stubTxId, dateCreated: stubCurrentDate, size: 10, - path: '/root-folder/file-in-root-1', name: 'file-in-root-1', parentFolderId: stubEntityId, lastUpdated: stubCurrentDate, @@ -63,13 +68,13 @@ void main() { id: 'file-in-root-1-entity-id', driveId: stubEntityId, isHidden: false, + path: '', ); final stubFileInRoot2 = FileEntry( dataTxId: stubTxId, dateCreated: stubCurrentDate, size: 10, - path: '/root-folder/file-in-root-2', name: 'file-in-root-2', parentFolderId: stubEntityId, lastUpdated: stubCurrentDate, @@ -77,13 +82,13 @@ void main() { id: 'file-in-root-2-entity-id', driveId: stubEntityId, isHidden: false, + path: '', ); final stubFileInParent1 = FileEntry( dataTxId: stubTxId, dateCreated: stubCurrentDate, size: 10, - path: '/root-folder/parent-folder/file-in-parent-1', name: 'file-in-parent-1', parentFolderId: stubEntityId, lastUpdated: stubCurrentDate, @@ -91,13 +96,13 @@ void main() { id: 'file-in-parent-1-entity-id', driveId: stubEntityId, isHidden: false, + path: '', ); final stubFileInParent2 = FileEntry( dataTxId: stubTxId, dateCreated: stubCurrentDate, size: 10, - path: '/root-folder/parent-folder/file-in-parent-2', name: 'file-in-parent-2', parentFolderId: stubEntityId, lastUpdated: stubCurrentDate, @@ -105,13 +110,13 @@ void main() { id: 'file-in-parent-2-entity-id', driveId: stubEntityId, isHidden: false, + path: '', ); final stubFileInChild1 = FileEntry( dataTxId: stubTxId, dateCreated: stubCurrentDate, size: 10, - path: '/root-folder/parent-folder/child-folder/file-in-child-1', name: 'file-in-child-1', parentFolderId: stubEntityId, lastUpdated: stubCurrentDate, @@ -119,13 +124,13 @@ void main() { id: 'file-in-child-1-entity-id', driveId: stubEntityId, isHidden: false, + path: '', ); final stubFileInChild2 = FileEntry( dataTxId: stubTxId, dateCreated: stubCurrentDate, size: 10, - path: '/root-folder/parent-folder/child-folder/file-in-child-2', name: 'file-in-child-2', parentFolderId: stubEntityId, lastUpdated: stubCurrentDate, @@ -133,13 +138,13 @@ void main() { id: 'file-in-child-2-entity-id', driveId: stubEntityId, isHidden: false, + path: '', ); final stubManifestFileInChild = FileEntry( dataTxId: stubTxId, dateCreated: stubCurrentDate, size: 10, - path: '/root-folder/parent-folder/child-folder/file-in-child-2', name: 'manifest-file-in-child', parentFolderId: stubEntityId, lastUpdated: stubCurrentDate, @@ -148,6 +153,7 @@ void main() { driveId: stubEntityId, dataContentType: ContentType.manifest, isHidden: false, + path: '', ); final stubChildFolderNode = @@ -173,66 +179,103 @@ void main() { stubFileInRoot2.id: stubFileInRoot2, }); + setUpAll(() { + fileRepository = MockFileRepository(); + folderRepository = MockFolderRepository(); + + when(() => folderRepository.getFolderPath( + stubRootFolderNode.folder.driveId, stubRootFolderNode.folder.id)) + .thenAnswer((_) async => 'root-folder'); + when(() => folderRepository.getFolderPath( + stubParentFolderEntry.driveId, stubParentFolderEntry.id)) + .thenAnswer((_) async => 'root-folder/parent-folder'); + when(() => folderRepository.getFolderPath( + stubChildFolderEntry.driveId, stubChildFolderEntry.id)) + .thenAnswer((_) async => 'root-folder/parent-folder/child-folder'); + + when(() => fileRepository.getFilePath( + stubFileInRoot1.driveId, stubFileInRoot1.id)) + .thenAnswer((_) async => 'root-folder/file-in-root-1'); + when(() => fileRepository.getFilePath( + stubFileInRoot2.driveId, stubFileInRoot2.id)) + .thenAnswer((_) async => 'root-folder/file-in-root-2'); + when(() => fileRepository.getFilePath( + stubFileInParent1.driveId, stubFileInParent1.id)) + .thenAnswer((_) async => 'root-folder/parent-folder/file-in-parent-1'); + when(() => fileRepository.getFilePath( + stubFileInParent2.driveId, stubFileInParent2.id)) + .thenAnswer((_) async => 'root-folder/parent-folder/file-in-parent-2'); + when(() => + fileRepository.getFilePath( + stubFileInChild1.driveId, stubFileInChild1.id)).thenAnswer( + (_) async => 'root-folder/parent-folder/child-folder/file-in-child-1'); + when(() => + fileRepository.getFilePath( + stubFileInChild2.driveId, stubFileInChild2.id)).thenAnswer( + (_) async => 'root-folder/parent-folder/child-folder/file-in-child-2'); + }); group('ManifestEntity Tests', () { - group('fromFolderNode static method', () { - test('returns a ManifestEntity with a valid expected manifest shape', - () async { - final manifest = ManifestData.fromFolderNode( - folderNode: stubRootFolderNode, - ); + test('returns a ManifestEntity with a valid expected manifest shape', + () async { + final manifest = await ManifestData.fromFolderNode( + folderNode: stubRootFolderNode, + fileRepository: fileRepository, + folderRepository: folderRepository, + ); - expect( - manifest.toJson(), - equals({ - 'manifest': 'arweave/paths', - 'version': '0.1.0', - 'index': {'path': 'file-in-root-1'}, - 'paths': { - 'file-in-root-1': { - 'id': '0000000000000000000000000000000000000000001' - }, - 'file-in-root-2': { - 'id': '0000000000000000000000000000000000000000001' - }, - 'parent-folder/file-in-parent-1': { - 'id': '0000000000000000000000000000000000000000001' - }, - 'parent-folder/file-in-parent-2': { - 'id': '0000000000000000000000000000000000000000001' - }, - 'parent-folder/child-folder/file-in-child-1': { - 'id': '0000000000000000000000000000000000000000001' - }, - 'parent-folder/child-folder/file-in-child-2': { - 'id': '0000000000000000000000000000000000000000001' - } + expect( + manifest.toJson(), + equals({ + 'manifest': 'arweave/paths', + 'version': '0.1.0', + 'index': {'path': 'file-in-root-1'}, + 'paths': { + 'file-in-root-1': { + 'id': '0000000000000000000000000000000000000000001' + }, + 'file-in-root-2': { + 'id': '0000000000000000000000000000000000000000001' + }, + 'parent-folder/file-in-parent-1': { + 'id': '0000000000000000000000000000000000000000001' + }, + 'parent-folder/file-in-parent-2': { + 'id': '0000000000000000000000000000000000000000001' + }, + 'parent-folder/child-folder/file-in-child-1': { + 'id': '0000000000000000000000000000000000000000001' + }, + 'parent-folder/child-folder/file-in-child-2': { + 'id': '0000000000000000000000000000000000000000001' } - })); - }); + } + })); + }); - test( - 'returns a ManifestEntity with a valid expected manifest shape with a nested child folder', - () async { - final manifest = ManifestData.fromFolderNode( - folderNode: stubChildFolderNode, - ); + test( + 'returns a ManifestEntity with a valid expected manifest shape with a nested child folder', + () async { + final manifest = await ManifestData.fromFolderNode( + folderNode: stubChildFolderNode, + fileRepository: fileRepository, + folderRepository: folderRepository, + ); - expect( - manifest.toJson(), - equals({ - 'manifest': 'arweave/paths', - 'version': '0.1.0', - 'index': {'path': 'file-in-child-1'}, - 'paths': { - 'file-in-child-1': { - 'id': '0000000000000000000000000000000000000000001' - }, - 'file-in-child-2': { - 'id': '0000000000000000000000000000000000000000001' - } + expect( + manifest.toJson(), + equals({ + 'manifest': 'arweave/paths', + 'version': '0.1.0', + 'index': {'path': 'file-in-child-1'}, + 'paths': { + 'file-in-child-1': { + 'id': '0000000000000000000000000000000000000000001' + }, + 'file-in-child-2': { + 'id': '0000000000000000000000000000000000000000001' } - })); - }); + } + })); }); group('asPreparedDataItem method', () { @@ -246,8 +289,10 @@ void main() { test('returns a DataItem with the expected tags, owner, and data', () async { - final manifest = ManifestData.fromFolderNode( + final manifest = await ManifestData.fromFolderNode( folderNode: stubRootFolderNode, + fileRepository: fileRepository, + folderRepository: folderRepository, ); final wallet = getTestWallet(); diff --git a/test/models/daos/drive_dao_test.dart b/test/models/daos/drive_dao_test.dart index fa5f6a2d8c..f35c26d89f 100644 --- a/test/models/daos/drive_dao_test.dart +++ b/test/models/daos/drive_dao_test.dart @@ -9,7 +9,6 @@ void main() { group('DriveDao', () { const driveId = 'drive-id'; - const rootPath = ''; const rootFolderId = 'root-folder-id'; const rootFolderFileCount = 5; @@ -40,21 +39,22 @@ void main() { await db.close(); }); // Any empty string is a root path - test("watchFolder() with root path ('') returns root folder", () async { + test( + 'watchFolder() with root path (no folderId provided) returns root folder', + () async { final folderStream = driveDao.watchFolderContents( driveId, - folderPath: rootPath, ); await Future.wait([ expectLater(folderStream.map((f) => f.folder.id), emits(rootFolderId)), ]); }); - test('watchFolder() returns correct number of files in root folder', + test('watchFolder() returns correcta number of files in root folder', () async { final folderStream = driveDao.watchFolderContents( driveId, - folderPath: rootPath, + folderId: rootFolderId, ); await Future.wait([ @@ -70,7 +70,7 @@ void main() { () async { final folderStream = driveDao.watchFolderContents( driveId, - folderPath: rootPath, + folderId: rootFolderId, ); await Future.wait([ @@ -82,11 +82,10 @@ void main() { ]); }); - test('watchFolder() with subfolder path returns correct subfolder', - () async { + test('watchFolder() with subfolder id returns correct subfolder', () async { final folderStream = driveDao.watchFolderContents( driveId, - folderPath: '/$emptyNestedFolderIdPrefix' '0', + folderId: '${emptyNestedFolderIdPrefix}0', ); await Future.wait([ @@ -97,7 +96,7 @@ void main() { test('watchFolder() returns correct folders inside empty folder', () async { final folderStream = driveDao.watchFolderContents( driveId, - folderPath: '/$emptyNestedFolderIdPrefix' '0', + folderId: '${emptyNestedFolderIdPrefix}0', ); await Future.wait([ @@ -110,7 +109,7 @@ void main() { test('watchFolder() returns correct files inside empty folder', () async { final folderStream = driveDao.watchFolderContents( driveId, - folderPath: '/$emptyNestedFolderIdPrefix' '0', + folderId: '${emptyNestedFolderIdPrefix}0', ); await Future.wait([ @@ -170,29 +169,8 @@ void main() { for (var i = 0; i < filesInFolderTree.length; i++) { final file = filesInFolderTree[i]; expect(file.id, equals(expectedTreeResults[i][0])); - expect(file.path, equals(expectedTreeResults[i][1])); } }); - - // test('getRecursiveFiles with a maxDepth of 0 returns just the files in the root folder', - // () async { - // final treeRoot = await driveDao.getFolderTree(driveId, rootFolderId); - // final filesInFolderTree = treeRoot.getRecursiveFiles(maxDepth: 1); - - // expect(filesInFolderTree.length, equals(5)); - // for (var i = 0; i < filesInFolderTree.length; i++) { - // final file = filesInFolderTree[i]; - // expect(file.id, equals(expectedTreeResults[i][0])); - // expect(file.path, equals(expectedTreeResults[i][1])); - // } - // }); - - // test('getRecursiveFiles with a maxDepth of -1 returns no files', () async { - // final treeRoot = await driveDao.getFolderTree(driveId, rootFolderId); - // final filesInFolderTree = treeRoot.getRecursiveFiles(maxDepth: -1); - - // expect(filesInFolderTree.length, equals(0)); - // }); }); } diff --git a/test/test_utils/mocks.dart b/test/test_utils/mocks.dart index 99d0c41f31..a6564c1b01 100644 --- a/test/test_utils/mocks.dart +++ b/test/test_utils/mocks.dart @@ -4,6 +4,8 @@ import 'package:ardrive/blocs/prompt_to_snapshot/prompt_to_snapshot_bloc.dart'; import 'package:ardrive/blocs/upload/upload_file_checker.dart'; import 'package:ardrive/core/arfs/entities/arfs_entities.dart'; import 'package:ardrive/core/arfs/repository/arfs_repository.dart'; +import 'package:ardrive/core/arfs/repository/file_repository.dart'; +import 'package:ardrive/core/arfs/repository/folder_repository.dart'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/core/download_service.dart'; import 'package:ardrive/models/database/database_helpers.dart'; @@ -107,6 +109,10 @@ class MockLicenseService extends Mock implements LicenseService {} class MockPromptToSnapshotBloc extends Mock implements PromptToSnapshotBloc {} +class MockFolderRepository extends Mock implements FolderRepository {} + +class MockFileRepository extends Mock implements FileRepository {} + class MockARFSFile extends ARFSFileEntity { MockARFSFile({ required super.appName, @@ -226,7 +232,6 @@ FileDataTableItem createMockFileDataTableItem( size: size, dateCreated: dateCreated ?? DateTime.now(), contentType: 'contentType', - path: path, index: index, pinnedDataOwnerAddress: pinnedDataOwnerAddress, isOwner: isOwner, @@ -256,7 +261,6 @@ FolderDataTableItem createMockFolderDataTableItem( lastUpdated: lastUpdated ?? DateTime.now(), dateCreated: dateCreated ?? DateTime.now(), contentType: contentType, - path: path, fileStatusFromTransactions: fileStatusFromTransactions, parentFolderId: parentFolderId, isGhostFolder: isGhostFolder, @@ -302,10 +306,10 @@ FolderEntry createMockFolderEntry( driveId: driveId, lastUpdated: DateTime.now(), dateCreated: DateTime.now(), - path: path, parentFolderId: parentFolderId, isGhost: isGhost, isHidden: false, + path: '', ); } @@ -336,9 +340,9 @@ FileEntry createMockFileEntry( lastModifiedDate: lastModifiedDate ?? DateTime.now(), dateCreated: dateCreated ?? DateTime.now(), lastUpdated: lastUpdated ?? DateTime.now(), - path: path, parentFolderId: parentFolderId, bundledIn: bundledIn, isHidden: false, + path: '', ); } diff --git a/test/test_utils/utils.dart b/test/test_utils/utils.dart index 3ef54542db..d99d8b6446 100644 --- a/test/test_utils/utils.dart +++ b/test/test_utils/utils.dart @@ -65,16 +65,16 @@ Future addTestFilesToDb( id: rootFolderId, driveId: driveId, name: 'fake-drive-name', - path: '', isHidden: const Value(false), + path: '', ), FolderEntriesCompanion.insert( id: nestedFolderId, driveId: driveId, parentFolderId: Value(rootFolderId), name: nestedFolderId, - path: '/$nestedFolderId', isHidden: const Value(false), + path: '', ), ...List.generate( emptyNestedFolderCount, @@ -85,8 +85,8 @@ Future addTestFilesToDb( driveId: driveId, parentFolderId: Value(rootFolderId), name: folderId, - path: '/$folderId', isHidden: const Value(false), + path: '', ); }, )..shuffle(Random(0)), @@ -105,13 +105,13 @@ Future addTestFilesToDb( driveId: driveId, parentFolderId: rootFolderId, name: fileId, - path: '/$fileId', dataTxId: '${fileId}Data', size: 500, dateCreated: Value(defaultDate), lastModifiedDate: defaultDate, dataContentType: const Value(''), isHidden: const Value(false), + path: '', ); }, )..shuffle(Random(0)), @@ -124,13 +124,13 @@ Future addTestFilesToDb( driveId: driveId, parentFolderId: nestedFolderId, name: fileId, - path: '/$nestedFolderId/$fileId', dataTxId: '${fileId}Data', size: 500, dateCreated: Value(defaultDate), lastModifiedDate: defaultDate, dataContentType: const Value(''), isHidden: const Value(false), + path: '', ); }, )..shuffle(Random(0)), diff --git a/test/utils/link_generators_test.dart b/test/utils/link_generators_test.dart index 7d92c1652a..93b7bf2e05 100644 --- a/test/utils/link_generators_test.dart +++ b/test/utils/link_generators_test.dart @@ -87,7 +87,6 @@ void main() { driveId: 'driveId', parentFolderId: 'parentFolderId', name: 'testFile', - path: '/test/test', dataTxId: 'Data', size: 500, dateCreated: DateTime.now(), @@ -95,6 +94,7 @@ void main() { lastUpdated: DateTime.now(), dataContentType: '', isHidden: false, + path: '', ); testFileKeyBase64 = 'X123YZAB-CD4e5fgHIjKlmN6O7pqrStuVwxYzaBcd8E'; testFileKey = SecretKey(decodeBase64ToBytes(testFileKeyBase64));