From f9938e0aac5b4681d437f69a206d59b0dd1c2174 Mon Sep 17 00:00:00 2001 From: Jason Barden Date: Thu, 5 Mar 2026 04:03:14 +0000 Subject: [PATCH 1/5] feat: adding projects from other repos --- .../AStar.Dev.OneDrive.Client.Core.csproj | 14 + .../MsalConfigurationSettings.cs | 11 + .../Dtos/DeltaPage.cs | 5 + .../Dtos/LocalFileInfo.cs | 3 + .../Dtos/UploadSessionInfo.cs | 3 + .../Entities/Account.cs | 19 + .../Entities/AccountEntity.cs | 19 + .../Entities/AccountInfo.cs | 29 + .../Entities/DebugLog.cs | 15 + .../Entities/DebugLogEntity.cs | 15 + .../Entities/DebugLogEntry.cs | 21 + .../Entities/DeltaToken.cs | 3 + .../Entities/DriveItemRecord.cs | 14 + .../Entities/Enums/SelectionState.cs | 25 + .../Entities/Enums/SyncState.cs | 12 + .../Entities/Enums/TransferStatus.cs | 3 + .../Entities/Enums/TransferType.cs | 3 + .../Entities/FileChangeEvent.cs | 19 + .../Entities/FileMetadata.cs | 33 + .../Entities/FileMetadataEntity.cs | 20 + .../Entities/FileOperationLog.cs | 32 + .../Entities/FileOperationLogEntity.cs | 21 + .../Entities/LocalFileRecord.cs | 11 + .../Entities/SyncConfiguration.cs | 13 + .../Entities/SyncConfigurationEntity.cs | 13 + .../Entities/SyncConflict.cs | 38 + .../Entities/SyncConflictEntity.cs | 64 + .../Entities/SyncSessionLog.cs | 41 + .../Entities/SyncSessionLogEntity.cs | 18 + .../Entities/SyncState.cs | 55 + .../Entities/TransferLog.cs | 15 + .../Entities/WindowPreferences.cs | 19 + .../Entities/WindowPreferencesEntity.cs | 14 + .../Interfaces/IAuthService.cs | 9 + .../Interfaces/IFileSystemAdapter.cs | 14 + .../Interfaces/IGraphClient.cs | 20 + .../Interfaces/ISyncRepository.cs | 33 + .../Enums/ConflictResolutionStrategy.cs | 32 + .../Models/Enums/FileChangeType.cs | 27 + .../Models/Enums/FileOperation.cs | 27 + .../Models/Enums/FileSyncStatus.cs | 47 + .../Models/Enums/SyncDirection.cs | 17 + .../Models/Enums/SyncStatus.cs | 37 + .../Models/SyncConfiguration.cs | 17 + .../Utilities/Result.cs | 11 + .../Utilities/SyncSettings.cs | 5 + ....Dev.OneDrive.Client.Infrastructure.csproj | 33 + .../Auth/MsalAuthService.cs | 89 + .../Data/AppDbContext.cs | 138 ++ .../Configurations/AccountConfiguration.cs | 17 + .../AccountEntityConfiguration.cs | 17 + .../Configurations/DeltaTokenConfiguration.cs | 21 + .../DriveItemRecordConfiguration.cs | 23 + .../FileMetadataConfiguration.cs | 26 + .../LocalFileRecordConfiguration.cs | 23 + .../SyncConfigurationConfiguration.cs | 21 + .../SyncConflictConfiguration.cs | 24 + .../TransferLogConfiguration.cs | 23 + .../WindowPreferencesConfiguration.cs | 15 + .../Data/DbInitializer.cs | 14 + .../Data/ModelBuilderExtensions.cs | 72 + .../Data/Repositories/AccountRepository.cs | 117 + .../Data/Repositories/DebugLogRepository.cs | 92 + .../Data/Repositories/EfSyncRepository.cs | 188 ++ .../Repositories/FileMetadataRepository.cs | 181 ++ .../FileOperationLogRepository.cs | 113 + .../Data/Repositories/IAccountRepository.cs | 53 + .../Data/Repositories/IDebugLogRepository.cs | 49 + .../Repositories/IFileMetadataRepository.cs | 79 + .../IFileOperationLogRepository.cs | 50 + .../ISyncConfigurationRepository.cs | 66 + .../Repositories/ISyncConflictRepository.cs | 70 + .../Repositories/ISyncSessionLogRepository.cs | 47 + .../SyncConfigurationRepository.cs | 175 ++ .../Repositories/SyncConflictRepository.cs | 122 + .../Repositories/SyncSessionLogRepository.cs | 107 + .../Data/SqliteTypeConverters.cs | 32 + ...frastructureServiceCollectionExtensions.cs | 94 + .../FileSystem/LocalFileSystemAdapter.cs | 62 + .../Graph/GraphClientWrapper.cs | 156 ++ .../Graph/GraphPathHelpers.cs | 40 + .../HealthChecks/DatabaseHealthCheck.cs | 40 + .../HealthChecks/GraphApiHealthCheck.cs | 45 + ...20251221025933_InitialCreation.Designer.cs | 137 ++ .../20251221025933_InitialCreation.cs | 92 + ...BytesTransferredToTransferLogs.Designer.cs | 144 ++ ...55438_AddBytesTransferredToTransferLogs.cs | 159 ++ .../20251221085945_Test.Designer.cs | 144 ++ .../Migrations/20251221085945_Test.cs | 21 + ...AdditionalTablesForConsumption.Designer.cs | 370 +++ ...16201123_AdditionalTablesForConsumption.cs | 178 ++ ...223008_RenameTokenValueToToken.Designer.cs | 430 ++++ .../20260116223008_RenameTokenValueToToken.cs | 146 ++ .../20260118103718_V3Tables.Designer.cs | 777 +++++++ .../Migrations/20260118103718_V3Tables.cs | 417 ++++ .../Migrations/AppDbContextModelSnapshot.cs | 774 +++++++ .../AStar.Dev.OneDrive.Client.Services.csproj | 31 + .../ChannelFactory.cs | 24 + .../ConfigurationSettings/AppPathHelper.cs | 55 + .../ApplicationSettings.cs | 134 ++ .../ConfigurationSettings/EntraIdSettings.cs | 39 + .../ConfigurationSettings/FileServices.cs | 8 + .../ConfigurationSettings/UiSettings.cs | 101 + .../ConfigurationSettings/UserPreferences.cs | 54 + .../ConfigurationSettings/WindowSettings.cs | 60 + .../DeltaPageProcessor.cs | 114 + .../ServiceCollectionExtensions.cs | 79 + .../DownloadQueueConsumer.cs | 27 + .../DownloadQueueProducer.cs | 26 + .../HealthCheckService.cs | 52 + .../IChannelFactory.cs | 24 + .../IDeltaPageProcessor.cs | 23 + .../IDownloadQueueConsumer.cs | 19 + .../IDownloadQueueProducer.cs | 14 + .../ILocalFileScanner.cs | 12 + .../ISyncEngine.cs | 39 + .../ISyncErrorLogger.cs | 14 + .../ISyncProgressReporter.cs | 13 + .../ITransferService.cs | 26 + .../IUploadQueueConsumer.cs | 9 + .../IUploadQueueProducer.cs | 9 + .../LocalFileScanner.cs | 91 + .../SyncEngine.cs | 171 ++ .../SyncErrorLogger.cs | 10 + .../SyncExtensions.cs | 13 + .../SyncOperationType.cs | 10 + .../SyncProgress.cs | 46 + .../SyncProgressReporter.cs | 17 + .../SyncSettings.cs | 42 + .../ISyncronisationCoordinator.cs | 12 + .../SyncronisationCoordinator.cs | 38 + .../TransferProgress.cs | 3 + .../TransferService.cs | 439 ++++ .../Untitled-1.sqlite3-query | 29 + .../UploadQueueConsumer.cs | 21 + .../UploadQueueProducer.cs | 19 + .../AStar.Dev.OneDrive.Client.csproj | 58 + .../AStar.Dev.OneDrive.Client/App.axaml | 7 + .../AStar.Dev.OneDrive.Client/App.axaml.cs | 41 + .../ApplicationMetadata.cs | 22 + .../Assets/astar-logo.png | Bin 0 -> 14032 bytes .../Assets/astar.ico | Bin 0 -> 16958 bytes .../Assets/astar.png | Bin 0 -> 15984 bytes .../Common/AutoSaveService.cs | 34 + .../Common/FileWriter.cs | 13 + .../Common/IAutoSaveService.cs | 22 + .../Common/OneDriveClientConstants.cs | 34 + .../Converters/BooleanNegationConverter.cs | 16 + .../Converters/EnumToBooleanConverter.cs | 31 + .../Data/ApplicationName.cs | 6 + .../AStar.Dev.OneDrive.Client/Data/DriveId.cs | 3 + .../AStar.Dev.OneDrive.Client/Data/ItemId.cs | 3 + .../Data/LocalDriveId.cs | 6 + .../AStar.Dev.OneDrive.Client/FodyWeavers.xml | 3 + .../Authentication/AuthConfiguration.cs | 53 + .../FromV3/Authentication/AuthService.cs | 190 ++ .../Authentication/AuthenticationClient.cs | 52 + .../Authentication/AuthenticationResult.cs | 15 + .../FromV3/Authentication/IAuthService.cs | 53 + .../Authentication/IAuthenticationClient.cs | 45 + .../FromV3/Authentication/MsalAuthResult.cs | 44 + .../FromV3/AutoSyncCoordinator.cs | 137 ++ .../FromV3/AutoSyncSchedulerService.cs | 138 ++ .../AccountEntityConfiguration.cs | 17 + .../FromV3/DatabaseConfiguration.cs | 28 + .../FromV3/DebugLog.cs | 47 + .../FromV3/DebugLogContext.cs | 27 + .../FromV3/DebugLogger.cs | 57 + .../FromV3/FileWatcherService.cs | 186 ++ .../FromV3/IAutoSyncCoordinator.cs | 26 + .../FromV3/IAutoSyncSchedulerService.cs | 30 + .../FromV3/IDebugLogger.cs | 38 + .../FromV3/IFileWatcherService.cs | 38 + .../FromV3/IFolderTreeService.cs | 46 + .../FromV3/ILocalFileScanner.cs | 31 + .../FromV3/IRemoteChangeDetector.cs | 23 + .../FromV3/ISyncEngine.cs | 32 + .../FromV3/ISyncSelectionService.cs | 103 + .../FromV3/IWindowPreferencesService.cs | 23 + .../FromV3/LocalFileScanner.cs | 176 ++ .../FromV3/LogCleanupBackgroundService.cs | 61 + .../OneDriveServices/FolderTreeService.cs | 169 ++ .../FromV3/OneDriveServices/GraphApiClient.cs | 310 +++ .../OneDriveServices/IGraphApiClient.cs | 94 + .../FromV3/ProgressReporterService.cs | 53 + .../FromV3/RemoteChangeDetector.cs | 253 +++ .../FromV3/ServiceConfiguration.cs | 109 + .../FromV3/SyncEngine.cs | 900 ++++++++ .../FromV3/SyncSelectionService.cs | 362 +++ .../FromV3/WindowPreferencesService.cs | 69 + .../HostExtensions.cs | 130 ++ .../Models/OneDriveFolderNode.cs | 127 ++ .../OneDrive Notes and Issues.txt | 2 + .../OperationConstants.cs | 8 + .../AStar.Dev.OneDrive.Client/Program.cs | 72 + .../ISettingsAndPreferencesService.cs | 24 + .../SettingsAndPreferencesService.cs | 16 + .../SettingsAndPreferences/UiSettings.cs | 103 + .../SettingsAndPreferences/WindowSettings.cs | 60 + .../Styles/Theme.axaml | 19 + .../SyncConflicts/ConflictItemViewModel.cs | 114 + .../ConflictResolutionView.axaml | 152 ++ .../ConflictResolutionView.axaml.cs | 14 + .../ConflictResolutionViewModel.cs | 234 ++ .../SyncConflicts/ConflictResolver.cs | 263 +++ .../SyncConflicts/IConflictResolver.cs | 20 + .../SyncConflicts/SyncConflict.cs | 42 + .../Theme/IThemeMapper.cs | 22 + .../Theme/IThemeSelectionHandler.cs | 24 + .../Theme/IThemeService.cs | 15 + .../Theme/ThemeMapper.cs | 26 + .../Theme/ThemeSelectionHandler.cs | 35 + .../Theme/ThemeService.cs | 33 + .../ViewModels/AccountManagementViewModel.cs | 327 +++ .../ViewModels/DashboardViewModel.cs | 12 + .../ViewModels/DebugLogViewModel.cs | 246 ++ .../ViewModels/IMainWindowCoordinator.cs | 24 + .../ViewModels/ISyncCommandService.cs | 36 + .../ViewModels/ISyncStatusTarget.cs | 30 + .../ViewModels/IWindowPositionValidator.cs | 23 + .../ViewModels/IWindowPositionable.cs | 24 + .../ViewModels/MainWindow.axaml | 133 ++ .../ViewModels/MainWindow.axaml.cs | 51 + .../ViewModels/MainWindowCoordinator.cs | 45 + .../ViewModels/MainWindowViewModel.cs | 404 ++++ .../ViewModels/SyncCommandService.cs | 164 ++ .../ViewModels/SyncProgressViewModel.cs | 338 +++ .../ViewModels/SyncTreeViewModel.cs | 414 ++++ .../UpdateAccountDetailsViewModel.cs | 271 +++ .../ViewModels/ViewModelBase.cs | 5 + .../ViewModels/ViewSyncHistoryViewModel.cs | 184 ++ .../ViewModels/WindowPositionValidator.cs | 16 + .../ViewModels/WindowPositionable.cs | 10 + .../Views/AccountManagementView.axaml | 138 ++ .../Views/AccountManagementView.axaml.cs | 25 + .../Views/DebugLogWindow.axaml | 148 ++ .../Views/DebugLogWindow.axaml.cs | 24 + .../Views/SyncProgressView.axaml | 117 + .../Views/SyncProgressView.axaml.cs | 14 + .../Views/SyncTreeView.axaml | 234 ++ .../Views/SyncTreeView.axaml.cs | 62 + .../Views/UpdateAccountDetailsWindow.axaml | 206 ++ .../Views/UpdateAccountDetailsWindow.axaml.cs | 26 + .../Views/ViewSyncHistoryWindow.axaml | 137 ++ .../Views/ViewSyncHistoryWindow.axaml.cs | 25 + .../appsettings.json | 100 + ...ev.OneDrive.Sync.Client.Application.csproj | 17 + .../Interfaces/IMigrationService.cs | 12 + .../Interfaces/ISyncService.cs | 12 + .../Services/SyncService.cs | 16 + ...tar.Dev.OneDrive.Sync.Client.Domain.csproj | 16 + .../Entities/SyncFile.cs | 13 + .../Interfaces/ISyncFileRepository.cs | 12 + ...OneDrive.Sync.Client.Infrastructure.csproj | 18 + .../Data/AstarOneDriveDbContext.cs | 29 + .../Data/AstarOneDriveDbContextFactory.cs | 32 + .../Configurations/AccountConfiguration.cs | 53 + .../Configurations/SettingConfiguration.cs | 38 + .../Configurations/SyncFileConfiguration.cs | 108 + .../Data/Contracts/AccountState.cs | 10 + .../Data/Contracts/FolderNodeState.cs | 12 + .../Data/Contracts/SettingsState.cs | 10 + .../Data/DatabasePathResolver.cs | 44 + .../Data/Entities/AccountEntity.cs | 52 + .../Data/Entities/SettingEntity.cs | 27 + .../Data/Entities/SyncFileEntity.cs | 102 + .../20260223150000_InitialCreate.cs | 132 ++ .../AstarOneDriveDbContextModelSnapshot.cs | 198 ++ .../Repositories/SqliteAccountsRepository.cs | 57 + .../SqliteFolderTreeRepository.cs | 116 + .../Repositories/SqliteSettingsRepository.cs | 90 + .../Data/SqliteDatabaseMigrator.cs | 26 + .../OneDriveSyncFileRepository.cs | 15 + .../ServiceCollectionExtensions.cs | 17 + .../AStar.Dev.OneDrive.Sync.Client.UI.csproj | 48 + .../AccountManagement/AccountDialogView.axaml | 30 + .../AccountDialogView.axaml.cs | 51 + .../AccountDialogViewModel.cs | 213 ++ .../AccountManagement/AccountInfo.cs | 13 + .../AccountManagement/AccountListView.axaml | 37 + .../AccountListView.axaml.cs | 44 + .../AccountManagement/AccountListViewModel.cs | 168 ++ .../App.axaml | 16 + .../App.axaml.cs | 73 + .../ApplicationMetadata.cs | 22 + .../Assets/astar.png | Bin 0 -> 15984 bytes .../Common/ErrorDialog.axaml | 53 + .../Common/ErrorDialog.axaml.cs | 40 + .../Common/ErrorHandler.cs | 99 + .../Common/LayoutType.cs | 11 + .../Common/RelayCommand.cs | 26 + .../Common/ViewModelBase.cs | 9 + .../Composition/CompositionRoot.cs | 57 + .../ExceptionBootstrap.cs | 46 + .../FolderTrees/FolderNode.cs | 13 + .../FolderTrees/FolderTreeView.axaml | 28 + .../FolderTrees/FolderTreeView.axaml.cs | 10 + .../FolderTrees/FolderTreeViewModel.cs | 177 ++ .../Home/MainWindow.axaml | 34 + .../Home/MainWindow.axaml.cs | 77 + .../Home/MainWindowViewModel.cs | 176 ++ .../InMemoryLogSink.cs | 45 + .../Layouts/DashboardLayoutView.axaml | 64 + .../Layouts/DashboardLayoutView.axaml.cs | 10 + .../Layouts/DashboardLayoutViewModel.cs | 54 + .../Layouts/ExplorerLayoutView.axaml | 49 + .../Layouts/ExplorerLayoutView.axaml.cs | 10 + .../Layouts/ExplorerLayoutView.cs | 50 + .../Layouts/TerminalLayoutView.axaml | 66 + .../Layouts/TerminalLayoutView.axaml.cs | 10 + .../Layouts/TerminalLayoutView.cs | 41 + .../Locales/en-GB.axaml | 103 + .../Locales/en-US.axaml | 92 + .../Localization/LocalizationManager.cs | 156 ++ .../LoggingBootstrap.cs | 59 + .../Metrics.cs | 19 + .../Program.cs | 51 + .../SerilogTraceListener.cs | 16 + .../Settings/SettingsView.axaml | 45 + .../Settings/SettingsView.axaml.cs | 20 + .../Settings/SettingsViewModel.cs | 158 ++ .../SyncStatus/SyncActivityEntry.cs | 9 + .../SyncStatus/SyncStatusView.axaml | 28 + .../SyncStatus/SyncStatusView.axaml.cs | 10 + .../SyncStatus/SyncStatusViewModel.cs | 126 ++ .../ThemeManager/ThemeManager.cs | 102 + .../Themes/Base.axaml | 41 + .../Themes/Colorful.axaml | 23 + .../Themes/Dark.axaml | 23 + .../Themes/Hacker.axaml | 23 + .../Themes/HighContrast.axaml | 24 + .../Themes/Light.axaml | 27 + .../Themes/Professional.axaml | 23 + .../ViewLocator.cs | 23 + .../app.manifest | 18 + .../Data/DatabasePathResolver.cs | 49 + ...AStar.Dev.OneDrive.Sync.Client.Core.csproj | 41 + .../AccountIdHasher.cs | 29 + .../AdminAccountMetadata.cs | 18 + .../ApplicationMetadata.cs | 22 + .../Data/DatabaseConfiguration.cs | 28 + .../Data/Entities/AccountEntity.cs | 38 + .../Data/Entities/DebugLogEntity.cs | 17 + .../Data/Entities/DriveItemEntity.cs | 78 + .../Data/Entities/FileOperationLogEntity.cs | 23 + .../Data/Entities/SyncConflictEntity.cs | 70 + .../Data/Entities/SyncSessionLogEntity.cs | 20 + .../Data/Entities/WindowPreferencesEntity.cs | 19 + ...DebugLogMetadata.DeltaProcessingService.cs | 14 + .../DebugLogMetadata.cs | 26 + .../Models/AccountInfo.cs | 20 + .../Models/AccountInfoExtensions.cs | 25 + .../Models/DebugLogEntry.cs | 32 + .../Models/DeltaPage.cs | 5 + .../Models/DeltaToken.cs | 3 + .../Enums/ConflictResolutionStrategy.cs | 32 + .../Models/Enums/FileChangeType.cs | 27 + .../Models/Enums/FileOperation.cs | 27 + .../Models/Enums/FileSyncStatus.cs | 52 + .../Models/Enums/SyncDirection.cs | 22 + .../Models/Enums/SyncStatus.cs | 47 + .../Models/Enums/ThemePreference.cs | 37 + .../Models/FileChangeEvent.cs | 19 + .../Models/FileMetadata.cs | 37 + .../Models/FileOperationLog.cs | 32 + .../Models/HashedAccountId.cs | 6 + .../Models/MsalConfigurationSettings.cs | 3 + .../Models/OneDrive/FileSystemInfo.cs | 10 + .../Models/OneDrive/Hashes.cs | 11 + .../Models/OneDrive/Image.cs | 10 + .../Models/OneDrive/OneDriveFile.cs | 10 + .../Models/OneDrive/OneDriveFolder.cs | 10 + .../Models/OneDrive/OneDriveResponse.cs | 19 + .../Models/OneDrive/ParentReference.cs | 13 + .../Models/OneDrive/SpecialFolder.cs | 9 + .../Models/OneDrive/User.cs | 11 + .../Models/OneDrive/Value.cs | 27 + .../Models/OneDrive/View.cs | 11 + .../Models/OneDriveFolderNode.cs | 128 ++ .../Models/SelectionState.cs | 25 + .../Models/SyncConfiguration.cs | 11 + .../Models/SyncConflict.cs | 45 + .../Models/SyncSessionLog.cs | 40 + .../Models/SyncState.cs | 46 + .../Models/WindowPreferences.cs | 22 + ...OneDrive.Sync.Client.Infrastructure.csproj | 51 + .../AccountEntityConfiguration.cs | 23 + .../DebugLogEntityConfiguration.cs | 25 + .../Configuration/DeltaTokenConfiguration.cs | 26 + .../DriveItemRecordConfiguration.cs | 29 + .../FileOperationLogEntityConfiguration.cs | 23 + .../SyncConflictEntityConfiguration.cs | 28 + .../SyncSessionLogEntityConfiguration.cs | 23 + .../WindowPreferencesEntityConfiguration.cs | 16 + ...0260217130717_InitialCreation3.Designer.cs | 454 ++++ .../20260217130717_InitialCreation3.cs | 295 +++ ...20260218165504_SessionIdToGuid.Designer.cs | 454 ++++ .../20260218165504_SessionIdToGuid.cs | 52 + .../Migrations/SyncDbContextModelSnapshot.cs | 451 ++++ .../Data/ModelBuilderExtensions.cs | 90 + .../Data/SqliteTypeConverters.cs | 39 + .../Data/SyncDbContext.cs | 61 + .../Data/SyncDbContextFactory.cs | 23 + .../Repositories/AccountRepository.cs | 116 + .../Repositories/DebugLogRepository.cs | 116 + .../Repositories/DriveItemsRepository.cs | 124 + .../Repositories/EfSyncRepository.cs | 61 + .../FileOperationLogRepository.cs | 106 + .../Repositories/FolderSelectionExtensions.cs | 55 + .../Repositories/IAccountRepository.cs | 61 + .../Repositories/IDebugLogRepository.cs | 57 + .../Repositories/IDriveItemsRepository.cs | 62 + .../IFileOperationLogRepository.cs | 50 + .../ISyncConfigurationRepository.cs | 87 + .../Repositories/ISyncConflictRepository.cs | 70 + .../Repositories/ISyncRepository.cs | 43 + .../Repositories/ISyncSessionLogRepository.cs | 47 + .../SyncConfigurationRepository.cs | 224 ++ .../Repositories/SyncConflictRepository.cs | 132 ++ .../Repositories/SyncSessionLogRepository.cs | 95 + .../SerilogLogParser.cs | 52 + .../Authentication/AuthConfiguration.cs | 52 + .../Services/Authentication/AuthService.cs | 146 ++ .../Authentication/AuthenticationClient.cs | 37 + .../Authentication/AuthenticationResult.cs | 12 + .../AuthenticationResultExtensions.cs | 28 + .../Services/Authentication/IAuthService.cs | 55 + .../Authentication/IAuthenticationClient.cs | 46 + .../Services/Authentication/MsalAuthResult.cs | 26 + .../Services/AutoSyncCoordinator.cs | 100 + .../Services/AutoSyncSchedulerService.cs | 124 + .../Services/ConflictDetectionService.cs | 168 ++ .../Services/DebugLog.cs | 105 + .../Services/DebugLogContext.cs | 30 + .../Services/DebugLoggerService.cs | 57 + .../Services/DeletionSyncService.cs | 98 + .../Services/DeltaPageProcessor.cs | 82 + .../Services/DeltaProcessingService.cs | 51 + .../Services/FileWatcherService.cs | 163 ++ .../Services/GraphApiClient.cs | 334 +++ .../Services/GraphPathHelpers.cs | 40 + .../Services/IAutoSyncCoordinator.cs | 29 + .../Services/IAutoSyncSchedulerService.cs | 33 + .../Services/IConflictDetectionService.cs | 57 + .../Services/IDebugLogger.cs | 44 + .../Services/IDeletionSyncService.cs | 41 + .../Services/IDeltaPageProcessor.cs | 20 + .../Services/IDeltaProcessingService.cs | 42 + .../Services/IFileWatcherService.cs | 38 + .../Services/IGraphApiClient.cs | 119 + .../Services/ILocalFileScanner.cs | 31 + .../Services/IRemoteChangeDetector.cs | 24 + .../Services/ISyncEngine.cs | 35 + .../Services/ISyncSelectionService.cs | 113 + .../Services/ISyncStateCoordinator.cs | 99 + .../Services/IThemeService.cs | 27 + .../Services/IThemeStartupCoordinator.cs | 14 + .../Services/IWindowPreferencesService.cs | 23 + .../Services/LocalFileScanner.cs | 156 ++ .../Services/LogCleanupBackgroundService.cs | 53 + .../OneDriveServices/FileTransferService.cs | 278 +++ .../OneDriveServices/FolderTreeService.cs | 141 ++ .../OneDriveServices/IFileTransferService.cs | 27 + .../OneDriveServices/IFolderTreeService.cs | 50 + .../Services/ProgressReporterService.cs | 51 + .../Services/RemoteChangeDetector.cs | 203 ++ .../Services/SyncEngine.cs | 448 ++++ .../Services/SyncError.cs | 24 + .../Services/SyncSelectionService.cs | 324 +++ .../Services/SyncStateCoordinator.cs | 186 ++ .../Services/ThemeService.cs | 119 + .../Services/ThemeStartupCoordinator.cs | 40 + .../Services/WindowPreferencesService.cs | 74 + .../AStar.Dev.OneDrive.Sync.Client.csproj | 75 + .../Accounts/AccountManagementView.axaml | 143 ++ .../Accounts/AccountManagementView.axaml.cs | 16 + .../Accounts/AccountManagementViewModel.cs | 310 +++ .../Accounts/UpdateAccountDetailsViewModel.cs | 269 +++ .../Accounts/UpdateAccountDetailsWindow.axaml | 206 ++ .../UpdateAccountDetailsWindow.axaml.cs | 27 + .../AStar.Dev.OneDrive.Sync.Client/App.axaml | 14 + .../App.axaml.cs | 54 + .../AStar.Dev.OneDrive.Sync.Client/AppHost.cs | 62 + .../ApplicationMetadata.cs | 35 + .../ApplicationName.cs | 6 + .../ApplicationServiceExtensions.cs | 58 + .../Assets/astar.png | Bin 0 -> 15984 bytes .../AuthenticationExtensions.cs | 32 + .../ConfigurationSettings/AppPathHelper.cs | 54 + .../ApplicationSettings.cs | 142 ++ .../ConfigurationSettings/EntraIdSettings.cs | 39 + .../Converters/BoolToStatusColorConverter.cs | 16 + .../Converters/BoolToStatusTextConverter.cs | 11 + .../Converters/BooleanNegationConverter.cs | 16 + .../Converters/EnumToBooleanConverter.cs | 31 + .../Converters/InitialsConverter.cs | 19 + .../ThemePreferenceToDisplayNameConverter.cs | 29 + .../DatabaseServiceExtensions.cs | 27 + .../DebugLogs/DebugLogViewModel.cs | 239 ++ .../DebugLogs/DebugLogWindow.axaml | 151 ++ .../DebugLogs/DebugLogWindow.axaml.cs | 27 + .../HttpClientExtension.cs | 39 + .../MainWindow/MainWindow.axaml | 71 + .../MainWindow/MainWindow.axaml.cs | 105 + .../MainWindow/MainWindowViewModel.cs | 394 ++++ .../AStar.Dev.OneDrive.Sync.Client/Program.cs | 35 + .../Settings/SettingsViewModel.cs | 85 + .../Settings/SettingsWindow.axaml | 63 + .../Settings/SettingsWindow.axaml.cs | 13 + .../Syncronisation/SyncProgressView.axaml | 117 + .../Syncronisation/SyncProgressView.axaml.cs | 16 + .../Syncronisation/SyncProgressViewModel.cs | 338 +++ .../Syncronisation/SyncTreeView.axaml | 271 +++ .../Syncronisation/SyncTreeView.axaml.cs | 71 + .../Syncronisation/SyncTreeViewModel.cs | 456 ++++ .../ViewSyncHistoryViewModel.cs | 186 ++ .../ViewSyncHistoryWindow.axaml | 137 ++ .../ViewSyncHistoryWindow.axaml.cs | 28 + .../ConflictItemViewModel.cs | 118 + .../ConflictResolutionView.axaml | 151 ++ .../ConflictResolutionView.axaml.cs | 16 + .../ConflictResolutionViewModel.cs | 233 ++ .../ConflictResolver.cs | 213 ++ .../IConflictResolver.cs | 18 + .../Themes/ColourfulTheme.axaml | 101 + .../Themes/ProfessionalTheme.axaml | 91 + .../Themes/TerminalTheme.axaml | 117 + .../ViewModelExtensions.cs | 22 + .../appsettings.json | 44 + .../AStar.Dev.Admin.Api.Client.Sdk.csproj | 55 + libs/AStar.Dev.Admin.Api.Client.Sdk/AStar.png | Bin 0 -> 15984 bytes .../AdminApi/AdminApiClient.cs | 137 ++ .../AdminApi/AdminApiConfiguration.cs | 23 + .../Constants.cs | 12 + libs/AStar.Dev.Admin.Api.Client.Sdk/LICENSE | 21 + .../Models/ModelToIgnore.cs | 23 + .../Models/ScrapeDirectories.cs | 37 + .../Models/SearchCategory.cs | 37 + .../Models/SearchConfiguration.cs | 92 + .../Models/SiteConfiguration.cs | 42 + .../Models/TagToIgnore.cs | 29 + libs/AStar.Dev.Admin.Api.Client.Sdk/Readme.md | 1 + .../ServiceCollectionExtensions.cs | 48 + libs/AStar.Dev.Admin.Api.Client.Sdk/astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Api.Client.Sdk.Shared.csproj | 53 + .../AStar.Dev.Api.Client.Sdk.Shared/AStar.png | Bin 0 -> 15984 bytes .../AddApiHttpClient.cs | 53 + .../IApiConfiguration.cs | 17 + libs/AStar.Dev.Api.Client.Sdk.Shared/LICENSE | 21 + .../AStar.Dev.Api.Client.Sdk.Shared/Readme.md | 1 + .../AStar.Dev.Api.Client.Sdk.Shared/astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Api.HealthChecks.csproj | 66 + libs/AStar.Dev.Api.HealthChecks/AStar.png | Bin 0 -> 15984 bytes .../HealthCheckExtensions.cs | 84 + .../HealthStatusResponse.cs | 30 + libs/AStar.Dev.Api.HealthChecks/IApiClient.cs | 17 + libs/AStar.Dev.Api.HealthChecks/LICENSE | 21 + libs/AStar.Dev.Api.HealthChecks/Program.cs | 8 + .../Properties/launchSettings.json | 12 + libs/AStar.Dev.Api.HealthChecks/Readme.md | 1 + libs/AStar.Dev.Api.HealthChecks/astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Api.Usage.Sdk.csproj | 72 + libs/AStar.Dev.Api.Usage.Sdk/AStar.png | Bin 0 -> 15984 bytes .../ApiUsageConfiguration.cs | 22 + libs/AStar.Dev.Api.Usage.Sdk/ApiUsageEvent.cs | 19 + libs/AStar.Dev.Api.Usage.Sdk/LICENSE | 21 + .../Metrics/IEndpointName.cs | 17 + .../Metrics/UsageMetricHandler.cs | 40 + .../Metrics/UsageMetricHandlerExtensions.cs | 15 + libs/AStar.Dev.Api.Usage.Sdk/Readme.md | 1 + libs/AStar.Dev.Api.Usage.Sdk/Send.cs | 28 + .../ServiceCollectionExtensions.cs | 33 + libs/AStar.Dev.Api.Usage.Sdk/astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.AspNet.Extensions.csproj | 75 + libs/AStar.Dev.AspNet.Extensions/AStar.png | Bin 0 -> 15984 bytes .../ApiConfiguration.cs | 19 + .../ConfigurationManagerExtensions.cs | 30 + .../Handlers/GlobalExceptionHandler.cs | 65 + libs/AStar.Dev.AspNet.Extensions/LICENSE | 21 + .../PipelineExtensions/PipelineExtensions.cs | 56 + libs/AStar.Dev.AspNet.Extensions/Readme.md | 1 + .../RootEndpoint/RootEndpointConfiguration.cs | 38 + .../ServiceCollectionExtensions.cs | 101 + .../WebApplicationBuilderExtensions.cs | 28 + libs/AStar.Dev.AspNet.Extensions/astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Auth.Extensions.csproj | 53 + libs/AStar.Dev.Auth.Extensions/JwtEvents.cs | 66 + libs/AStar.Dev.Auth.Extensions/astar.ico | Bin 0 -> 16958 bytes libs/AStar.Dev.Auth.Extensions/astar.png | Bin 0 -> 15984 bytes .../AStar.Dev.Files.Api.Client.SDK.csproj | 51 + libs/AStar.Dev.Files.Api.Client.SDK/AStar.png | Bin 0 -> 15984 bytes .../Constants.cs | 12 + .../FilesApi/FilesApiClient.cs | 410 ++++ .../FilesApi/FilesApiConfiguration.cs | 23 + libs/AStar.Dev.Files.Api.Client.SDK/LICENSE | 21 + .../Models/DirectoryChangeRequest.cs | 35 + .../Models/DuplicateGroup.cs | 144 ++ .../Models/Duplicates.cs | 65 + .../Models/ExcludedViewSettings.cs | 27 + .../Models/FileAccessDetail.cs | 53 + .../Models/FileDetail.cs | 67 + .../Models/FileDetailClassification.cs | 22 + .../Models/FileDetailClassifications.cs | 14 + .../Models/FileDimensionsWithSize.cs | 39 + .../Models/GetDuplicatesCountQueryResponse.cs | 6 + .../Models/SearchParameters.cs | 112 + .../Models/SearchType.cs | 27 + .../Models/SortOrder.cs | 27 + libs/AStar.Dev.Files.Api.Client.SDK/Readme.md | 1 + libs/AStar.Dev.Files.Api.Client.SDK/astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Fluent.Assignments.csproj | 38 + libs/AStar.Dev.Fluent.Assignments/AStar.png | Bin 0 -> 15984 bytes .../FluentAssignmentsExtensions.cs | 112 + libs/AStar.Dev.Fluent.Assignments/LICENSE | 21 + libs/AStar.Dev.Fluent.Assignments/Readme.md | 1 + libs/AStar.Dev.Fluent.Assignments/astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Functional.Extensions.Blog.md | 543 +++++ .../AStar.Dev.Functional.Extensions.csproj | 44 + .../AStar.Dev.Functional.Extensions.xml | 986 ++++++++ .../CollectionAndStatusExtensions.cs | 47 + .../ConvenienceResultExtensions.cs | 31 + .../EnumerableExtensions.cs | 23 + .../ErrorResponse.cs | 17 + libs/AStar.Dev.Functional.Extensions/LICENSE | 21 + .../AStar.Dev.Functional.Extensions/Option.cs | 19 + .../OptionExtensions.cs | 335 +++ .../OptionLinqExtensions.cs | 32 + .../Option{T}.cs | 139 ++ .../Pattern.cs | 59 + .../AStar.Dev.Functional.Extensions/Readme.md | 299 +++ .../AStar.Dev.Functional.Extensions/Result.cs | 126 ++ .../ResultExtensions.cs | 416 ++++ libs/AStar.Dev.Functional.Extensions/Try.cs | 82 + .../TryExtensions.cs | 27 + libs/AStar.Dev.Functional.Extensions/Unit.cs | 49 + .../UnreachableException.cs | 11 + .../ViewModelResultExtensions.cs | 52 + .../AStar.Dev.Functional.Extensions/astar.png | Bin 0 -> 15984 bytes .../AStar.Dev.Guard.Clauses.csproj | 38 + libs/AStar.Dev.Guard.Clauses/AStar.png | Bin 0 -> 15984 bytes libs/AStar.Dev.Guard.Clauses/GuardAgainst.cs | 25 + libs/AStar.Dev.Guard.Clauses/LICENSE | 21 + libs/AStar.Dev.Guard.Clauses/Readme.md | 1 + libs/AStar.Dev.Guard.Clauses/astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Images.Api.Client.SDK.csproj | 51 + .../AStar.Dev.Images.Api.Client.SDK/AStar.png | Bin 0 -> 15984 bytes .../Constants.cs | 12 + .../ImagesApi/ImagesApiClient.cs | 95 + .../ImagesApi/ImagesApiConfiguration.cs | 23 + libs/AStar.Dev.Images.Api.Client.SDK/LICENSE | 21 + .../Models/NotFound.cs | 12 + .../AStar.Dev.Images.Api.Client.SDK/Readme.md | 1 + .../AStar.Dev.Images.Api.Client.SDK/astar.ico | Bin 0 -> 16958 bytes .../.config/dotnet-tools.json | 13 + .../AStar.Dev.Infrastructure.AdminDb.csproj | 60 + .../AStar.png | Bin 0 -> 15984 bytes .../AdminContext.cs | 141 ++ .../ScrapeDirectoryConfiguration.cs | 28 + .../SearchCategoryConfiguration.cs | 25 + .../SearchConfigurationConfiguration.cs | 29 + .../SiteConfigurationConfiguration.cs | 30 + .../Create-Db-User.sql | 11 + libs/AStar.Dev.Infrastructure.AdminDb/LICENSE | 21 + ...20250326164249_InitialCreation.Designer.cs | 244 ++ .../20250326164249_InitialCreation.cs | 130 ++ .../Migrations/AdminContextModelSnapshot.cs | 241 ++ .../Models/ScrapeDirectory.cs | 45 + .../Models/SearchCategory.cs | 45 + .../Models/SearchConfiguration.cs | 97 + .../Models/SiteConfiguration.cs | 45 + .../Readme.md | 37 + .../SeedData/ScrapeDirectoryData.cs | 31 + .../SeedData/SearchCategoryData.cs | 26 + .../SeedData/SearchConfigurationData.cs | 40 + .../SeedData/SiteConfigurationData.cs | 35 + .../astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Infrastructure.FilesDb.csproj | 58 + .../AStar.png | Bin 0 -> 15984 bytes .../ComplexPropertyBuilderConfiguration.cs | 22 + .../DeletionStatusConfiguration.cs | 30 + .../DirectoryNameConfiguration.cs | 13 + .../Configurations/EventConfiguration.cs | 25 + .../Configurations/EventTypeConfiguration.cs | 14 + .../FileAccessDetailConfiguration.cs | 16 + .../FileClassificationConfiguration.cs | 23 + .../Configurations/FileDetailConfiguration.cs | 43 + .../Configurations/FileNameConfiguration.cs | 13 + .../FileNamePartConfiguration.cs | 20 + .../IComplexPropertyConfiguration.cs | 15 + .../ImageDetailConfiguration.cs | 17 + .../ModelToIgnoreConfiguration.cs | 20 + .../TagToIgnoreConfiguration.cs | 20 + .../Constants.cs | 10 + .../Data/FilesContext.cs | 87 + .../Data/FilesContextExtensions.cs | 92 + .../EnumerableExtensions.cs | 83 + libs/AStar.Dev.Infrastructure.FilesDb/LICENSE | 21 + ...20250919204057_InitialCreation.Designer.cs | 430 ++++ .../20250919204057_InitialCreation.cs | 270 +++ ...ructureDeletionAndImageDetails.Designer.cs | 460 ++++ ...1728_RestructureDeletionAndImageDetails.cs | 247 ++ .../Migrations/FilesContextModelSnapshot.cs | 457 ++++ .../Models/DeletionStatus.cs | 27 + .../Models/DirectoryName.cs | 6 + .../Models/DuplicateDetail.cs | 90 + .../Models/Event.cs | 57 + .../Models/EventType.cs | 99 + .../Models/FileAccessDetail.cs | 37 + .../Models/FileClassification.cs | 49 + .../Models/FileDetail.cs | 91 + .../Models/FileHandle.cs | 21 + .../Models/FileId.cs | 7 + .../Models/FileName.cs | 16 + .../Models/FileNamePart.cs | 28 + .../Models/FileSize.cs | 57 + .../Models/FileSizeEqualityComparer.cs | 44 + .../Models/ImageDetail.cs | 18 + .../Models/ModelToIgnore.cs | 28 + .../Models/SearchType.cs | 27 + .../Models/SortOrder.cs | 30 + .../Models/TagToIgnore.cs | 36 + .../Readme.md | 7 + .../astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Infrastructure.UsageDb.csproj | 56 + .../AStar.png | Bin 0 -> 15984 bytes .../SiteConfigurationConfiguration.cs | 30 + .../Data/ApiUsageContext.cs | 44 + libs/AStar.Dev.Infrastructure.UsageDb/LICENSE | 21 + ...311203727_InitialConfiguration.Designer.cs | 62 + .../20250311203727_InitialConfiguration.cs | 48 + .../20250314122139_UseDateTime.Designer.cs | 62 + .../Migrations/20250314122139_UseDateTime.cs | 36 + ...50314123039_ClusterOnTimestamp.Designer.cs | 66 + .../20250314123039_ClusterOnTimestamp.cs | 29 + .../20250327113904_AddHttpMethod.Designer.cs | 70 + .../20250327113904_AddHttpMethod.cs | 49 + .../ApiUsageContextModelSnapshot.cs | 67 + .../Readme.md | 1 + .../astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Infrastructure.csproj | 38 + libs/AStar.Dev.Infrastructure/AStar.png | Bin 0 -> 15984 bytes .../Data/AStarDbContextOptions.cs | 17 + .../Data/AuditableEntity.cs | 17 + .../Data/ConnectionString.cs | 28 + libs/AStar.Dev.Infrastructure/LICENSE | 21 + libs/AStar.Dev.Infrastructure/Readme.md | 1 + libs/AStar.Dev.Infrastructure/astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Logging.Extensions.csproj | 64 + .../AStar.Dev.Logging.Extensions.xml | 599 +++++ libs/AStar.Dev.Logging.Extensions/AStar.png | Bin 0 -> 15984 bytes .../AStarEventIds.cs | 15 + .../AStarLogger.cs | 43 + .../CloudRoleNameTelemetryInitializer.cs | 25 + .../Configuration.cs | 13 + .../EventIds/AStarEventIds.Application.cs | 32 + .../EventIds/AStarEventIds.OneDriveSync.cs | 26 + .../EventIds/AStarEventIds.Website.cs | 17 + .../EventIds/AStarEventIds.cs | 11 + .../ILoggerAstar.cs | 20 + .../ITelemetryClient.cs | 13 + libs/AStar.Dev.Logging.Extensions/LICENSE | 21 + .../LogExtensions.cs | 18 + .../LoggingExtensions.cs | 82 + .../Messages/AStarLog.Application.cs | 45 + .../Messages/AStarLog.OneDriveSync.cs | 26 + .../Messages/AStarLog.Web.cs | 104 + .../Messages/AStarLog.cs | 8 + .../Models/ApplicationInsights.cs | 12 + .../Models/Args.cs | 12 + .../Models/Console.cs | 17 + .../Models/FormatterOptions.cs | 32 + .../Models/JsonWriterOptions.cs | 12 + .../Models/Logging.cs | 17 + .../Models/Loglevel.cs | 22 + .../Models/Minimumlevel.cs | 17 + .../Models/Override.cs | 22 + .../Models/Serilog.cs | 35 + .../Models/SerilogConfig.cs | 27 + .../Models/WriteTo.cs | 17 + .../Properties/AssemblyInfo.cs | 3 + libs/AStar.Dev.Logging.Extensions/Readme.md | 75 + .../SerilogConfigure.cs | 26 + .../SerilogExtensions.cs | 62 + .../SerilogLogFileLocator.cs | 32 + .../astar-logging-settings.json | 54 + libs/AStar.Dev.Logging.Extensions/astar.ico | Bin 0 -> 16958 bytes libs/AStar.Dev.Logging.Extensions/astar.png | Bin 0 -> 15984 bytes .../AStar.Dev.Minimal.Api.Extensions.csproj | 38 + .../ApiVersion.cs | 12 + .../RouteHandlerBuilderExtensions.cs | 40 + .../astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Source.Analyzers.csproj | 18 + .../AnalyzerReleases.Shipped.md | 3 + .../AnalyzerReleases.Unshipped.md | 8 + .../AutoRegisterOptionsPartialAnalyzer.cs | 93 + ...ar.Dev.Source.Generators.Attributes.csproj | 9 + .../AutoRegisterEndpointAttribute.cs | 38 + .../AutoRegisterOptions.cs | 21 + .../AutoRegisterServiceAttribute.cs | 22 + .../ServiceLifetime.cs | 6 + .../StrongIdAttribute.cs | 20 + .../AStar.Dev.Source.Generators.Sample.csproj | 13 + .../SampleEntities.cs | 16 + .../AStar.Dev.Source.Generators.csproj | 83 + .../AttributeConstants.cs | 7 + .../OptionBindingGenerator.cs | 101 + .../OptionsBindingCodeGenerator.cs | 32 + .../OptionsTypeInfo.cs | 40 + .../Properties/launchSettings.json | 9 + libs/AStar.Dev.Source.Generators/Readme.md | 37 + .../ServiceCollectionCodeGenerator.cs | 79 + .../ServiceModel.cs | 83 + .../ServiceRegistrationGenerator.cs | 68 + .../StrongIdCodeGenerator.cs | 48 + .../StrongIdGenerator.cs | 58 + .../StrongIdCodeGeneration/StrongIdModel.cs | 22 + .../StrongIdModelExtensions.cs | 31 + libs/AStar.Dev.Source.Generators/astar.png | Bin 0 -> 15984 bytes .../build/AStar.Dev.Source.Generators.props | 9 + .../AStar.Dev.Technical.Debt.Reporting.csproj | 38 + .../AStar.png | Bin 0 -> 15984 bytes .../LICENSE | 21 + .../Readme.md | 1 + .../RefactorAttribute.cs | 26 + .../astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Test.Helpers.EndToEnd.csproj | 44 + .../AStar.Dev.Test.Helpers.EndToEnd/AStar.png | Bin 0 -> 15984 bytes libs/AStar.Dev.Test.Helpers.EndToEnd/LICENSE | 21 + .../AStar.Dev.Test.Helpers.EndToEnd/Readme.md | 1 + .../AStar.Dev.Test.Helpers.EndToEnd/astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Test.Helpers.Integration.csproj | 38 + .../AStar.png | Bin 0 -> 15984 bytes .../LICENSE | 21 + .../Readme.md | 1 + .../astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Test.Helpers.Unit.csproj | 38 + libs/AStar.Dev.Test.Helpers.Unit/AStar.png | Bin 0 -> 15984 bytes libs/AStar.Dev.Test.Helpers.Unit/LICENSE | 21 + libs/AStar.Dev.Test.Helpers.Unit/Readme.md | 1 + libs/AStar.Dev.Test.Helpers.Unit/astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Test.Helpers.csproj | 38 + libs/AStar.Dev.Test.Helpers/AStar.png | Bin 0 -> 15984 bytes libs/AStar.Dev.Test.Helpers/LICENSE | 21 + libs/AStar.Dev.Test.Helpers/Readme.md | 1 + libs/AStar.Dev.Test.Helpers/astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Usage.Api.Client.SDK.csproj | 43 + libs/AStar.Dev.Usage.Api.Client.SDK/AStar.png | Bin 0 -> 15984 bytes .../Constants.cs | 12 + libs/AStar.Dev.Usage.Api.Client.SDK/LICENSE | 21 + libs/AStar.Dev.Usage.Api.Client.SDK/Readme.md | 1 + .../UsageApi/ImagesApiConfiguration.cs | 23 + .../UsageApi/UsageApiClient.cs | 75 + libs/AStar.Dev.Usage.Api.Client.SDK/astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Utilities.csproj | 46 + .../AStar.Dev.Utilities.xml | 379 ++++ libs/AStar.Dev.Utilities/BLOG.md | 451 ++++ libs/AStar.Dev.Utilities/Constants.cs | 15 + .../EncryptionExtensions.cs | 79 + libs/AStar.Dev.Utilities/EnumExtensions.cs | 18 + libs/AStar.Dev.Utilities/LICENSE | 21 + libs/AStar.Dev.Utilities/LinqExtensions.cs | 20 + libs/AStar.Dev.Utilities/ObjectExtensions.cs | 21 + libs/AStar.Dev.Utilities/Readme.md | 42 + libs/AStar.Dev.Utilities/RegexExtensions.cs | 44 + libs/AStar.Dev.Utilities/StringExtensions.cs | 115 + libs/AStar.Dev.Utilities/astar.ico | Bin 0 -> 16958 bytes libs/AStar.Dev.Utilities/astar.png | Bin 0 -> 15984 bytes libs/ExampleLib/ExampleLib.csproj | 6 - libs/ExampleLib/MyClass.cs | 7 - libs/libs.sln | 24 - ...Dev.OneDrive.Client.Core.Tests.Unit.csproj | 63 + .../MsalConfigurationSettingsShould.cs | 27 + ....ContainTheExpectedProperties.approved.txt | 28 + .../Dtos/DeltaPageShould.cs | 19 + ....ContainTheExpectedProperties.approved.txt | 6 + .../Dtos/LocalFileInfoShould.cs | 15 + ....ContainTheExpectedProperties.approved.txt | 5 + .../Dtos/UploadSessionInfoShould.cs | 15 + ....ContainTheExpectedProperties.approved.txt | 5 + .../Entities/DeltaTokenShould.cs | 15 + .../Entities/DriveItemRecord.cs | 15 + ....ContainTheExpectedProperties.approved.txt | 11 + .../Entities/LocalFileRecord.cs | 37 + ....ContainTheExpectedProperties.approved.txt | 8 + .../Entities/TransferLog.cs | 16 + ....ContainTheExpectedProperties.approved.txt | 10 + .../Models/FileOperationLogShould.cs | 118 + .../Models/OneDriveFolderNodeShould.cs | 150 ++ .../Models/SyncConfigurationShould.cs | 47 + .../Models/SyncConflictShould.cs | 40 + .../Models/SyncSessionLogShould.cs | 24 + .../Repositories/AccountRepositoryShould.cs | 206 ++ .../Repositories/DebugLogRepositoryShould.cs | 173 ++ .../FileMetadataRepositoryShould.cs | 209 ++ .../SyncConfigurationRepositoryShould.cs | 163 ++ .../xunit.runner.json | 3 + ...nt.Infrastructure.Tests.Integration.csproj | 45 + .../Data/AppDbContextShould.cs | 296 +++ .../Data/DbInitializerShould.cs | 176 ++ .../Repositories/EfSyncRepositoryShould.cs | 532 +++++ .../LocalFileSystemAdapterShould.cs | 201 ++ .../xunit.runner.json | 3 + ...ve.Client.Infrastructure.Tests.Unit.csproj | 67 + .../Data/ModelBuilderExtensionsShould.cs | 58 + .../Data/SqliteTypeConvertersShould.cs | 316 +++ .../DatabaseHealthCheckShould.cs | 46 + .../Graph/GraphPathHelpersShould.cs | 152 ++ .../GraphApiHealthCheckShould.cs | 68 + .../GraphClientWrapperShould.cs | 168 ++ .../xunit.runner.json | 3 + ...e.Client.Services.Tests.Integration.csproj | 47 + .../SyncEngineShould.cs | 405 ++++ .../TransferServiceShould.cs | 411 ++++ .../xunit.runner.json | 4 + ...OneDrive.Client.Services.Tests.Unit.csproj | 62 + .../ChannelFactoryShould.cs | 16 + .../AppPathHelperShould.cs | 192 ++ ...dPropertiesWithExpectedValues.approved.txt | 11 + ...opertiesWithTheExpectedValues.approved.txt | 9 + .../ApplicationSettingsShould.cs | 142 ++ .../AutoRegisteredOptionsShould.cs | 133 ++ ...dPropertiesWithExpectedValues.approved.txt | 8 + ...opertiesWithTheExpectedValues.approved.txt | 8 + .../EntraIdSettingsShould.cs | 22 + .../FileServicesShould.cs | 29 + .../ConfigurationSettings/UiSettingsShould.cs | 126 ++ .../UserPreferencesShould.cs | 136 ++ .../WindowSettingsShould.cs | 91 + .../DownloadQueueConsumerShould.cs | 30 + .../DownloadQueueProducerShould.cs | 33 + .../ApplicationHealthCheckServiceShould.cs | 65 + .../SyncEngineShould.cs | 122 + .../SyncProgressReporterShould.cs | 30 + .../SyncProgressShould.cs | 340 +++ .../SyncSettingsShould.cs | 292 +++ .../TransferServiceShould.cs | 506 +++++ .../UploadQueueConsumerShould.cs | 29 + .../UploadQueueProducerShould.cs | 32 + .../xunit.runner.json | 3 + ...Star.Dev.OneDrive.Client.Tests.Unit.csproj | 70 + .../Common/AutoSaveServiceShould.cs | 161 ++ .../Data/ApplicationNameShould.cs | 16 + .../Data/DriveIdShould.cs | 16 + .../Data/ItemIdShould.cs | 16 + .../Data/LocalDriveIdShould.cs | 24 + .../Authentication/AuthServiceShould.cs | 243 ++ .../Integration/ServiceConfigurationShould.cs | 85 + .../Services/FileWatcherServiceShould.cs | 306 +++ .../FromV3/Services/LocalFileScannerShould.cs | 165 ++ .../LogCleanupBackgroundServiceShould.cs | 117 + .../FolderTreeServiceShould.cs | 204 ++ .../Services/RemoteChangeDetectorShould.cs | 179 ++ .../Services/Sync/ConflictResolverShould.cs | 372 +++ ...ineFormatScanningFolderForDisplayShould.cs | 20 + .../FromV3/Services/SyncEngineShould.cs | 962 ++++++++ .../SyncSelectionServicePersistenceShould.cs | 226 ++ .../Services/SyncSelectionServiceShould.cs | 352 +++ .../WindowPreferencesServiceShould.cs | 155 ++ .../AccountManagementIntegrationShould.cs | 201 ++ .../AccountManagementViewModelShould.cs | 444 ++++ .../ViewModels/ConflictItemViewModelShould.cs | 138 ++ .../ConflictResolutionViewModelShould.cs | 322 +++ .../ViewModels/DebugLogViewModelShould.cs | 172 ++ .../MainWindowViewModelIntegrationShould.cs | 278 +++ .../ViewModels/MainWindowViewModelShould.cs | 153 ++ .../ViewModels/SyncProgressViewModelShould.cs | 480 ++++ ...eeViewModelPersistenceIntegrationShould.cs | 233 ++ .../ViewModels/SyncTreeViewModelShould.cs | 375 +++ .../UpdateAccountDetailsViewModelShould.cs | 540 +++++ .../ViewSyncHistoryViewModelShould.cs | 139 ++ .../SettingsAndPreferencesServiceShould.cs | 149 ++ .../Theme/ThemeMapperShould.cs | 154 ++ .../Theme/ThemeSelectionHandlerShould.cs | 154 ++ .../Theme/ThemeServiceShould.cs | 34 + .../UnitTest1.cs | 7 + .../ViewModels/DashboardViewModelShould.cs | 296 +++ .../ViewModels/MainWindowCoordinatorShould.cs | 226 ++ .../ViewModels/MainWindowViewModelShould.cs | 260 +++ ...inWindowViewModelSyncStatusTargetShould.cs | 125 + .../ViewModels/SyncCommandServiceShould.cs | 188 ++ .../WindowPositionValidatorShould.cs | 64 + .../xunit.runner.json | 3 + ...Drive.Sync.Client.Application.Tests.csproj | 40 + .../GlobalSuppressions.cs | 8 + .../Services/SyncServiceShould.cs | 64 + ...v.OneDrive.Sync.Client.Domain.Tests.csproj | 40 + .../Entities/SyncFileShould.cs | 16 + .../GlobalSuppressions.cs | 8 + ...ve.Sync.Client.Infrastructure.Tests.csproj | 41 + .../Data/SqlitePersistenceShould.cs | 136 ++ .../GlobalSuppressions.cs | 8 + .../ServiceCollectionExtensionsShould.cs | 29 + ...r.Dev.OneDrive.Sync.Client.UI.Tests.csproj | 41 + .../AccountDialogViewModelShould.cs | 91 + .../AccountListViewModelShould.cs | 88 + .../AssemblyInfo.cs | 3 + .../Common/ErrorHandlerShould.cs | 55 + .../Common/RelayCommandShould.cs | 109 + .../Composition/AppCompositionWiringShould.cs | 40 + .../Composition/CompositionRootShould.cs | 42 + .../FolderTrees/FolderTreeViewModelShould.cs | 93 + .../GlobalSuppressions.cs | 8 + .../Home/MainWindowViewModelShould.cs | 184 ++ .../Integration/SettingsIntegrationShould.cs | 103 + .../Integration/SyncIntegrationShould.cs | 78 + .../ExplorerLayoutIntegrationShould.cs | 51 + .../Layouts/LayoutEdgeCasesShould.cs | 112 + .../LayoutSharedViewModelBindingShould.cs | 53 + .../Layouts/LayoutViewModelShould.cs | 77 + .../Layouts/LayoutsShould.cs | 114 + .../SyncStatusDataContextBindingShould.cs | 40 + .../Localization/LocalizationManagerShould.cs | 80 + .../Logging/LoggingBootstrapShould.cs | 54 + .../Settings/SettingsViewModelShould.cs | 179 ++ .../SyncStatus/SyncStatusViewModelShould.cs | 78 + .../ThemeManager/ThemeManagerShould.cs | 80 + .../ThemeManagerTestCollection.cs | 26 + .../AstarOneDrive.Infrastructure.Tests.csproj | 41 + .../Data/SqlitePersistenceShould.cs | 137 ++ .../GlobalSuppressions.cs | 8 + ...neDrive.Sync.Client.Core.Tests.Unit.csproj | 60 + .../Data/DatabaseConfigurationShould.cs | 25 + .../Data/Entities/AccountEntityShould.cs | 59 + .../Data/Entities/DebugLogEntityShould.cs | 32 + .../DriveItemEntitySelectionUpdateShould.cs | 36 + .../Data/Entities/DriveItemEntityShould.cs | 49 + .../Entities/FileOperationLogEntityShould.cs | 42 + .../Data/Entities/SyncConflictEntityShould.cs | 67 + .../Entities/SyncSessionLogEntityShould.cs | 35 + .../Entities/WindowPreferencesEntityShould.cs | 30 + .../Models/FileOperationLogShould.cs | 120 + .../Models/HashedAccountIdShould.cs | 16 + .../Models/OneDriveFolderNodeShould.cs | 148 ++ .../Models/SyncConfigurationShould.cs | 47 + .../Models/SyncConflictShould.cs | 35 + .../Models/SyncSessionLogShould.cs | 23 + .../Models/SyncStateShould.cs | 30 + .../xunit.runner.json | 3 + ...nc.Client.Infrastructure.Tests.Unit.csproj | 61 + .../Repositories/AccountRepositoryShould.cs | 170 ++ .../Repositories/DebugLogRepositoryShould.cs | 183 ++ .../DriveItemsRepositoryShould.cs | 173 ++ .../FolderSelectionExtensionsTests.cs | 262 +++ .../ConflictDetectionServiceShould.cs | 287 +++ .../Services/DeletionSyncServiceShould.cs | 187 ++ .../Services/DeltaProcessingServiceShould.cs | 204 ++ .../Services/FileWatcherServiceShould.cs | 263 +++ .../Services/LocalFileScannerShould.cs | 166 ++ .../FileTransferServiceShould.cs | 411 ++++ .../FolderTreeServiceIntegrationShould.cs | 196 ++ .../FolderTreeServiceShould.cs | 153 ++ .../Services/RemoteChangeDetectorShould.cs | 177 ++ ...ineFormatScanningFolderForDisplayShould.cs | 19 + .../Services/SyncEngineResultPatternShould.cs | 295 +++ .../SyncSelectionServicePersistenceShould.cs | 196 ++ .../Services/SyncSelectionServiceShould.cs | 347 +++ .../Services/SyncStateCoordinatorShould.cs | 258 +++ .../Services/ThemeServiceShould.cs | 173 ++ .../Services/ThemeStartupCoordinatorShould.cs | 111 + .../WindowPreferencesServiceShould.cs | 143 ++ ...referencesServiceShould_ThemePreference.cs | 216 ++ .../xunit.runner.json | 3 + ...Drive.Sync.Client.Tests.Integration.csproj | 65 + .../MainWindowViewModelIntegrationShould.cs | 240 ++ .../FolderTreeServiceIntegrationShould.cs | 192 ++ .../ThemePersistenceShould.cs | 104 + .../ThemeResourceDictionariesShould.cs | 59 + .../appsettings.json | 12 + .../xunit.runner.json | 3 + ...Dev.OneDrive.Sync.Client.Tests.Unit.csproj | 64 + .../AccountManagementIntegrationShould.cs | 212 ++ .../AccountManagementViewModelShould.cs | 399 ++++ .../UpdateAccountDetailsViewModelShould.cs | 754 ++++++ .../Authentication/AuthServiceShould.cs | 270 +++ .../AppPathHelperShould.cs | 36 + ...uld.HaveExpectedDefaultValues.approved.txt | 11 + .../ApplicationSettingsShould.cs | 15 + ...uld.HaveExpectedDefaultValues.approved.txt | 5 + .../EntraIdSettingsShould.cs | 15 + .../BoolToStatusColorConverterShould.cs | 66 + .../BoolToStatusTextConverterShould.cs | 55 + .../BooleanNegationConverterShould.cs | 37 + .../EnumToBooleanConverterShould.cs | 106 + .../Converters/InitialsConverterShould.cs | 103 + ...ePreferenceToDisplayNameConverterShould.cs | 40 + .../DebugLogs/DebugLogViewModelShould.cs | 617 +++++ .../GlobalUsings.cs | 5 + .../Integration/ServiceConfigurationShould.cs | 80 + .../MainWindow/MainWindowViewModelShould.cs | 366 +++ .../SampleTest.cs | 15 + .../Settings/SettingsViewModelShould.cs | 262 +++ .../SyncProgressViewModelShould.cs | 476 ++++ ...eeViewModelPersistenceIntegrationShould.cs | 199 ++ .../Syncronisation/SyncTreeViewModelShould.cs | 331 +++ .../ViewSyncHistoryViewModelShould.cs | 92 + .../ConflictItemViewModelShould.cs | 127 ++ .../ConflictResolutionViewModelShould.cs | 314 +++ .../ConflictResolverShould.cs | 329 +++ .../appsettings.json | 12 + ...Dev.Admin.Api.Client.Sdk.Tests.Unit.csproj | 44 + ...tar.Dev.Api.HealthChecks.Tests.Unit.csproj | 44 + ...ar.Dev.AspNet.Extensions.Tests.Unit.csproj | 56 + .../ApiConfigurationTest.cs | 22 + .../ConfigurationManagerExtensionsShould.cs | 29 + .../Handlers/GlobalExceptionHandlerShould.cs | 12 + .../PipelineExtensionsShould.cs | 12 + .../ServiceCollectionExtensionsShould.cs | 12 + .../TestData/appsettings.json | 29 + .../WebApplicationBuilderExtensionsShould.cs | 12 + ...Dev.Files.Api.Client.Sdk.Tests.Unit.csproj | 51 + .../FilesApi/FilesApiClientShould.cs | 391 ++++ .../FilesApi/FilesApiConfigurationShould.cs | 14 + .../Helpers/FilesApiClientFactory.cs | 31 + .../MockDeletionSuccessHttpMessageHandler.cs | 11 + ...RequestExceptionErrorHttpMessageHandler.cs | 8 + ...ckInternalServerErrorHttpMessageHandler.cs | 11 + .../MockSuccessHttpMessageHandler.cs | 41 + ...cessMessageWithValue0HttpMessageHandler.cs | 10 + ...uld.ReturnTheExpectedToString.approved.txt | 5 + .../Models/DirectoryChangeRequestShould.cs | 10 + ...uld.ReturnTheExpectedToString.approved.txt | 10 + .../Models/DuplicateGroupShould.cs | 10 + ...uld.ReturnTheExpectedToString.approved.txt | 4 + .../Models/ExcludedViewSettingsShould.cs | 10 + ...uld.ReturnTheExpectedToString.approved.txt | 9 + .../Models/FileAccessDetailShould.cs | 11 + .../Models/FileDetailShould.cs | 10 + ...uld.ReturnTheExpectedToString.approved.txt | 6 + .../Models/FileDimensionsWithSizeShould.cs | 10 + ...uld.ReturnTheExpectedToString.approved.txt | 1 + .../Models/HealthStatusResponseShould.cs | 10 + ...eturnTheExpectedToQueryString.approved.txt | 1 + ...uld.ReturnTheExpectedToString.approved.txt | 17 + .../Models/SearchParametersShould.cs | 14 + .../Models/SearchTypeShould.cs | 26 + .../Models/SortOrderShould.cs | 26 + ...r.Dev.Fluent.Assignments.Tests.Unit.csproj | 48 + .../FluentAssignmentsExtensionsShould.cs | 138 ++ ...ev.Functional.Extensions.Tests.Unit.csproj | 52 + .../CollectionAndStatusExtensionsShould.cs | 85 + .../ConvenienceResultExtensionsShould.cs | 42 + .../CustomTestException.cs | 3 + .../EnumerableExtensionsShould.cs | 47 + ....ContainTheExpectedProperties.approved.txt | 3 + .../ErrorResponseShould.cs | 9 + .../OptionLinqExtensionsShould.cs | 91 + .../OptionShould.cs | 1016 +++++++++ .../OptionToResultTests.cs | 20 + .../PatternTests.cs | 43 + .../ResultExtensionBindShould.cs | 191 ++ .../ResultExtensionMapShould.cs | 259 +++ .../ResultExtensionMatchAsyncShould.cs | 122 + .../ResultExtensionTapShould.cs | 236 ++ .../ResultImplicitConversionShould.cs | 36 + .../ResultLinqExtensionsTests.cs | 109 + .../ResultShould.cs | 120 + .../TryExtensionsShould.cs | 52 + .../TryShould.cs | 274 +++ .../UnreachableExceptionShould.cs | 7 + .../ViewModelResultExtensionsShould.cs | 149 ++ ...r.Dev.Logging.Extensions.Tests.Unit.csproj | 57 + .../AStarEventIdsShould.cs | 17 + .../AStarLoggerTest.cs | 119 + .../CloudRoleNameTelemetryInitializerTest.cs | 85 + .../ConfigurationShould.cs | 42 + .../ConfigurationTest.cs | 1 + .../Helpers/serilog.config | 10 + .../LogShould.cs | 205 ++ .../LoggingExtensionsShould.cs | 39 + .../LoggingExtensionsTest.cs | 87 + .../Models/ApplicationInsightsShould.cs | 57 + .../Models/ArgsShould.cs | 35 + .../Models/ConsoleShould.cs | 107 + .../Models/ConsoleTest.cs | 1 + .../Models/FormatterOptionsShould.cs | 100 + .../Models/JsonWriterOptionsShould.cs | 45 + ...tring_ShouldListAllProperties.approved.txt | 5 + .../Models/LogLevelShould.cs | 109 + ...tring_ShouldListAllProperties.approved.txt | 5 + .../Models/LoggingShould.cs | 103 + .../Models/LoggingTest.cs | 1 + .../Models/MinimumLevelShould.cs | 60 + .../Models/OverrideShould.cs | 87 + .../Models/SerilogConfigShould.cs | 142 ++ .../Models/SerilogShould.cs | 79 + .../Models/WriteToShould.cs | 49 + .../Properties/launchSettings.json | 12 + ....ContainTheExpectedProperties.approved.txt | 42 + .../SerilogConfigShould.cs | 10 + ...eLoggerWhenParametersAreValid.approved.txt | 9 + ...ureLogger_WithValidParameters.approved.txt | 9 + .../SerilogConfigureShould.cs | 84 + ...ureLogger_WithValidParameters.approved.txt | 1 + .../SerilogExtensionsShould.cs | 70 + ...tar.Dev.Source.Analyzers.Tests.Unit.csproj | 37 + ...utoRegisterOptionsPartialAnalyzerShould.cs | 59 + .../Testing/AnalyzerVerifier.cs | 29 + ...ce.Generators.Attributes.Tests.Unit.csproj | 43 + .../AutoRegisterEndpointAttributeShould.cs | 35 + .../AutoRegisterOptionsAttributeShould.cs | 27 + .../AutoRegisterServiceAttributeShould.cs | 28 + .../StrongIdAttributeShould.cs | 27 + .../xunit.runner.json | 3 + ...ar.Dev.Source.Generators.Tests.Unit.csproj | 41 + .../OptionsBindingGeneratorShould.cs | 180 ++ .../ServiceRegistrationGeneratorShould.cs | 339 +++ .../StrongIdGeneratorShould.cs | 159 ++ .../StrongIdCodeGeneration/TestStrongIds.cs | 13 + .../Utilitites/CompilationHelpers.cs | 50 + .../Utils/TestAdditionalFile.cs | 20 + .../AStar.Dev.Utilities.Tests.Unit.csproj | 42 + .../AnyClass.cs | 8 + .../AStar.Dev.Utilities.Tests.Unit/AnyEnum.cs | 7 + ...eserialisationSettingsSetting.approved.txt | 32 + .../ConstantsShould.cs | 11 + .../EncryptionExtensionsShould.cs | 10 + .../EnumExtensionsShould.cs | 15 + .../LinqExtensionsShould.cs | 12 + ...WhichReturnsTheExpectedString.approved.txt | 4 + .../ObjectExtensionsShould.cs | 9 + .../RegexExtensionsShould.cs | 50 + .../StringExtensionsShould.cs | 85 + .../AStar.Dev.Web.Tests.Unit.csproj | 0 .../AStar.Dev.Web.Tests.Unit/UnitTest1.cs | 0 .../xunit.runner.json | 0 .../AStar.Dev.Admin.Api.Tests.EndToEnd.csproj | 45 + ...tar.Dev.Admin.Api.Tests.Integration.csproj | 53 + .../BasicTests.cs | 18 + .../CustomWebApplicationFactory.cs | 31 + .../AStar.Dev.Admin.Api.Tests.Unit.csproj | 52 + .../AddEndpointsShould.cs | 17 + .../EndpointConstantsShould.cs | 19 + .../Properties/launchSettings.json | 12 + ...ould.ContainTheExpectedValues.approved.txt | 7 + .../V1/GetAllScrapeDirectoriesQueryShould.cs | 26 + ...llScrapeDirectoriesRequestHandlerShould.cs | 1 + ...ould.ContainTheExpectedValues.approved.txt | 11 + .../V1/GetScrapeDirectoriesResponseShould.cs | 30 + ...ould.ContainTheExpectedValues.approved.txt | 9 + ...dateScrapeDirectoriesCommandForDbShould.cs | 26 + ...ould.ContainTheExpectedValues.approved.txt | 7 + .../UpdateScrapeDirectoriesCommandShould.cs | 16 + ...teScrapeDirectoriesRequestHandlerShould.cs | 1 + ...ould.ContainTheExpectedValues.approved.txt | 10 + .../UpdateScrapeDirectoriesResponseShould.cs | 28 + ...AllSearchCategoriesRequestHandlerShould.cs | 1 + ...Test.ContainTheExpectedValues.approved.txt | 7 + .../V1/GetAllSiteConfigurationsQueryShould.cs | 26 + ...ould.ContainTheExpectedValues.approved.txt | 10 + .../V1/GetSiteConfigurationResponseShould.cs | 31 + ...ould.ContainTheExpectedValues.approved.txt | 4 + .../V1/GetSearchCategoriesByIdQueryShould.cs | 26 + ...GetSearchCategoriesRequestHandlerShould.cs | 1 + ...ould.ContainTheExpectedValues.approved.txt | 10 + .../V1/GetSearchCategoriesResponseShould.cs | 30 + .../Update/V1/BACKUP DATABASE FilesDb.sql | 6 + ...ateSearchCategoriesRequestHandlerShould.cs | 1 + ...ould.ContainTheExpectedValues.approved.txt | 10 + .../UpdateSearchCategoriesResponseShould.cs | 28 + ...ould.ContainTheExpectedValues.approved.txt | 9 + .../UpdateSearchCategoryCommandForDbShould.cs | 26 + ...ould.ContainTheExpectedValues.approved.txt | 7 + .../V1/UpdateSearchCategoryCommandShould.cs | 16 + ...SearchConfigurationRequestHandlerShould.cs | 1 + ...ould.ContainTheExpectedValues.approved.txt | 19 + ...GetAllSearchConfigurationResponseShould.cs | 39 + ...ould.ContainTheExpectedValues.approved.txt | 7 + .../GetAllSearchConfigurationsQueryShould.cs | 26 + ...ould.ContainTheExpectedValues.approved.txt | 8 + .../V1/GetSearchConfigurationQueryShould.cs | 26 + ...SearchConfigurationRequestHandlerShould.cs | 1 + ...ould.ContainTheExpectedValues.approved.txt | 8 + .../GetSearchConfigurationResponseShould.cs | 39 + ...ould.ContainTheExpectedValues.approved.txt | 18 + ...teSearchConfigurationCommandForDbShould.cs | 26 + ...ould.ContainTheExpectedValues.approved.txt | 17 + .../UpdateSearchConfigurationCommandShould.cs | 16 + ...SearchConfigurationRequestHandlerShould.cs | 1 + ...ould.ContainTheExpectedValues.approved.txt | 17 + ...UpdateSearchConfigurationResponseShould.cs | 16 + .../V1/GetAllSiteConfigurationsQueryShould.cs | 27 + ...Test.ContainTheExpectedValues.approved.txt | 7 + ...lSiteConfigurationsRequestHandlerShould.cs | 1 + .../V1/GetSiteConfigurationResponseShould.cs | 31 + ...Test.ContainTheExpectedValues.approved.txt | 4 + .../V1/GetSiteConfigurationQueryShould.cs | 27 + ...Test.ContainTheExpectedValues.approved.txt | 3 + ...etSiteConfigurationRequestHandlerShould.cs | 1 + .../V1/GetSiteConfigurationResponseShould.cs | 30 + .../SiteConfigurationExtensionsShould.cs | 13 + ...ould.ContainTheExpectedValues.approved.txt | 9 + ...dateSiteConfigurationCommandForDbShould.cs | 27 + .../UpdateSiteConfigurationCommandShould.cs | 17 + ...Test.ContainTheExpectedValues.approved.txt | 7 + ...teSiteConfigurationRequestHandlerShould.cs | 1 + .../UpdateSiteConfigurationResponseShould.cs | 17 + ...Test.ContainTheExpectedValues.approved.txt | 8 + .../AStar.Dev.Files.Api.Tests.EndToEnd.csproj | 45 + ...tar.Dev.Files.Api.Tests.Integration.csproj | 45 + .../AStar.Dev.Files.Api.Tests.Unit.csproj | 44 + ...AStar.Dev.Images.Api.Tests.EndToEnd.csproj | 45 + ...ar.Dev.Images.Api.Tests.Integration.csproj | 45 + .../AStar.Dev.Images.Api.Tests.Unit.csproj | 48 + .../Models/DimensionsShould.cs | 68 + ...Dev.Admin.Api.Client.Sdk.Tests.Unit.csproj | 44 + ...tar.Dev.Api.HealthChecks.Tests.Unit.csproj | 44 + ...ar.Dev.AspNet.Extensions.Tests.Unit.csproj | 56 + .../ApiConfigurationTest.cs | 22 + .../ConfigurationManagerExtensionsShould.cs | 29 + .../Handlers/GlobalExceptionHandlerShould.cs | 12 + .../PipelineExtensionsShould.cs | 12 + .../ServiceCollectionExtensionsShould.cs | 12 + .../TestData/appsettings.json | 29 + .../WebApplicationBuilderExtensionsShould.cs | 12 + ...Dev.Files.Api.Client.Sdk.Tests.Unit.csproj | 51 + .../FilesApi/FilesApiClientShould.cs | 391 ++++ .../FilesApi/FilesApiConfigurationShould.cs | 14 + .../Helpers/FilesApiClientFactory.cs | 31 + .../MockDeletionSuccessHttpMessageHandler.cs | 11 + ...RequestExceptionErrorHttpMessageHandler.cs | 8 + ...ckInternalServerErrorHttpMessageHandler.cs | 11 + .../MockSuccessHttpMessageHandler.cs | 41 + ...cessMessageWithValue0HttpMessageHandler.cs | 10 + ...uld.ReturnTheExpectedToString.approved.txt | 5 + .../Models/DirectoryChangeRequestShould.cs | 10 + ...uld.ReturnTheExpectedToString.approved.txt | 10 + .../Models/DuplicateGroupShould.cs | 10 + ...uld.ReturnTheExpectedToString.approved.txt | 4 + .../Models/ExcludedViewSettingsShould.cs | 10 + ...uld.ReturnTheExpectedToString.approved.txt | 9 + .../Models/FileAccessDetailShould.cs | 11 + .../Models/FileDetailShould.cs | 10 + ...uld.ReturnTheExpectedToString.approved.txt | 6 + .../Models/FileDimensionsWithSizeShould.cs | 10 + ...uld.ReturnTheExpectedToString.approved.txt | 1 + .../Models/HealthStatusResponseShould.cs | 10 + ...eturnTheExpectedToQueryString.approved.txt | 1 + ...uld.ReturnTheExpectedToString.approved.txt | 17 + .../Models/SearchParametersShould.cs | 14 + .../Models/SearchTypeShould.cs | 26 + .../Models/SortOrderShould.cs | 26 + ...r.Dev.Fluent.Assignments.Tests.Unit.csproj | 48 + .../FluentAssignmentsExtensionsShould.cs | 138 ++ ...ev.Functional.Extensions.Tests.Unit.csproj | 37 + .../CollectionAndStatusExtensionsShould.cs | 103 + .../ConvenienceResultExtensionsShould.cs | 92 + .../CustomTestException.cs | 3 + .../EnumerableExtensionsShould.cs | 47 + ....ContainTheExpectedProperties.approved.txt | 3 + .../ErrorResponseShould.cs | 10 + .../OptionLinqExtensionsShould.cs | 112 + .../OptionShould.cs | 1016 +++++++++ .../OptionToResultTests.cs | 20 + .../PatternTests.cs | 43 + .../ResultAsyncLinqExtensionsTests.cs | 55 + .../ResultExtensionBindShould.cs | 191 ++ .../ResultExtensionMapShould.cs | 259 +++ .../ResultExtensionTapShould.cs | 236 ++ .../ResultLinqExtensionsTests.cs | 110 + .../ResultShould.cs | 120 + .../TryExtensionsShould.cs | 64 + .../TryShould.cs | 213 ++ .../ViewModelResultExtensionsShould.cs | 173 ++ .../AStar.Dev.Guard.Clauses.Tests.Unit.csproj | 44 + ...ev.Images.Api.Client.Sdk.Tests.Unit.csproj | 50 + .../ImagesApiClientShould.cs | 52 + .../ImagesApiConfigurationShould.cs | 14 + .../MockDeletionSuccessHttpMessageHandler.cs | 11 + ...RequestExceptionErrorHttpMessageHandler.cs | 8 + ...kInternalServerErrorHttpMessageHandler1.cs | 11 + .../MockSuccessHttpMessageHandler.cs | 29 + ...v.Infrastructure.AdminDb.Tests.Unit.csproj | 44 + .../UserConfigurationShould.cs | 5 + ...v.Infrastructure.FilesDb.Tests.Unit.csproj | 56 + .../Data/FilesContextExtensionsShould.cs | 201 ++ .../EnumerableExtensionsShould.cs | 82 + .../Fixtures/FilesContextFixture.cs | 25 + .../Fixtures/MockFilesContext.cs | 65 + ...xpectedToStringRepresentation.approved.txt | 22 + .../Models/FileDetailShould.cs | 12 + .../Models/FileSizeEqualityComparerShould.cs | 24 + ...turnTheExpectedToStringOutput.approved.txt | 5 + .../Models/FileSizeShould.cs | 8 + ...turnTheExpectedToStringOutput.approved.txt | 4 + .../Models/TagToIgnoreCompletelyShould.cs | 8 + ...turnTheExpectedToStringOutput.approved.txt | 5 + .../Models/TagToIgnoreShould.cs | 8 + .../TestFiles/files.json | 2016 +++++++++++++++++ ...AStar.Dev.Infrastructure.Tests.Unit.csproj | 44 + ...r.Dev.Logging.Extensions.Tests.Unit.csproj | 49 + .../Helpers/serilog.config | 10 + .../LoggingExtensionsShould.cs | 34 + ....ContainTheExpectedProperties.approved.txt | 35 + .../SerilogConfigShould.cs | 11 + ...Technical.Debt.Reporting.Tests.Unit.csproj | 44 + ...ev.Test.Helpers.EndToEnd.Tests.Unit.csproj | 44 + ...Test.Helpers.Integration.Tests.Unit.csproj | 44 + ...ar.Dev.Test.Helpers.Tests.Unit.Unit.csproj | 44 + .../AStar.Dev.Test.Helpers.Tests.Unit.csproj | 44 + .../AStar.Dev.Utilities.Tests.Unit.csproj | 37 + .../AnyClass.cs | 8 + .../AStar.Dev.Utilities.Tests.Unit/AnyEnum.cs | 7 + ...eserialisationSettingsSetting.approved.txt | 32 + .../ConstantsShould.cs | 10 + .../EncryptionExtensionsShould.cs | 12 + .../EnumExtensionsShould.cs | 16 + .../LinqExtensionsShould.cs | 12 + ...WhichReturnsTheExpectedString.approved.txt | 4 + .../ObjectExtensionsShould.cs | 10 + .../RegexExtensionsShould.cs | 54 + .../StringExtensionsShould.cs | 96 + ...FilesDb.MigrationService.Tests.Unit.csproj | 35 + .../UnitTest1.cs | 10 + .../xunit.runner.json | 3 + .../AStar.Dev.Usage.Logger.Tests.Unit.csproj | 35 + .../UnitTest1.cs | 10 + .../xunit.runner.json | 3 + .../AStar.Dev.Web.Tests.Integration.csproj | 49 + .../CustomWebApplicationFactory.cs | 42 + .../PlaceHolderTests.cs | 23 + .../WebTests.cs | 41 + .../AStar.Dev.Web.Tests.Unit.csproj | 36 + .../Components/ApiStatusCheckShould.cs | 82 + .../Components/Pages/Shared/SearchShould.cs | 59 + .../AStar.Dev.Web.Tests.Unit/MockApiClient.cs | 10 + .../Pages/Admin/AddFilesToDatabaseShould.cs | 19 + .../Pages/Admin/ApiUsageShould.cs | 19 + .../Pages/Admin/AuthenticationCheckShould.cs | 19 + .../Pages/Admin/FileClassificationsShould.cs | 19 + .../Pages/Admin/ModelsToIgnoreShould.cs | 19 + .../Pages/Admin/ScrapeDirectoriesShould.cs | 19 + .../Pages/Admin/SiteConfigurationShould.cs | 19 + .../Pages/Admin/TagsToIgnoreShould.cs | 19 + .../Pages/HomeShould.cs | 38 + .../Shared/NavMenuShould.cs | 53 + .../AddApiHttpClientShould.cs | 51 + .../StartupConfiguration/ServicesShould.cs | 99 + .../StartupConfiguration/appSettings.json | 27 + .../xunit.runner.json | 3 + .../AStar.Dev.Web.AppHost.csproj | 24 + .../aspire/AStar.Dev.Web.AppHost/AppHost.cs | 14 + .../Properties/launchSettings.json | 18 + .../AStar.Dev.Web.AppHost/appsettings.json | 9 + .../AStar.Dev.Web.Aspire.Common.csproj | 16 + .../AspireConstants.cs | 38 + .../AStar.Dev.Web.ServiceDefaults.csproj | 27 + .../Extensions.cs | 122 + .../AStar.Dev.Admin.Api.csproj | 55 + .../apis/AStar.Dev.Admin.Api/AStar.png | Bin 0 -> 15984 bytes .../apis/AStar.Dev.Admin.Api/AddEndpoints.cs | 32 + .../apis/AStar.Dev.Admin.Api/Create-User.sql | 14 + .../apis/AStar.Dev.Admin.Api/Dockerfile | 33 + .../AStar.Dev.Admin.Api/EndpointConstants.cs | 63 + .../AStar.Dev.Admin.Api/IAssemblyMarker.cs | 7 + .../apis/AStar.Dev.Admin.Api/IEndpoint.cs | 10 + .../apis/AStar.Dev.Admin.Api/Program.cs | 104 + .../Properties/launchSettings.json | 15 + .../V1/GetAllScrapeDirectoriesEndpoint.cs | 52 + .../GetAll/V1/GetAllScrapeDirectoriesQuery.cs | 24 + .../GetAllScrapeDirectoriesQueryResponse.cs | 54 + .../V1/GetScrapeDirectoriesByIdEndpoint.cs | 50 + .../V1/GetScrapeDirectoriesByIdQuery.cs | 30 + ...crapeDirectoriesByIdQueryRequestHandler.cs | 25 + .../V1/GetScrapeDirectoriesByIdResponse.cs | 55 + .../V1/UpdateScrapeDirectoriesCommand.cs | 32 + .../V1/UpdateScrapeDirectoriesCommandForDb.cs | 65 + .../V1/UpdateScrapeDirectoriesEndpoint.cs | 73 + .../V1/UpdateScrapeDirectoriesResponse.cs | 52 + .../V1/GetAllSearchCategoriesEndpoint.cs | 48 + .../GetAll/V1/GetAllSearchCategoriesQuery.cs | 24 + .../V1/GetAllSearchCategoriesResponse.cs | 46 + .../V1/GetSearchCategoriesByIdEndpoint.cs | 50 + .../V1/GetSearchCategoriesByIdQuery.cs | 31 + .../V1/GetSearchCategoriesByIdResponse.cs | 50 + .../Update/V1/UpdateSearchCategoryCommand.cs | 32 + .../V1/UpdateSearchCategoryCommandForDb.cs | 65 + .../Update/V1/UpdateSearchCategoryEndpoint.cs | 72 + .../Update/V1/UpdateSearchCategoryResponse.cs | 49 + .../V1/GetAllSearchConfigurationsEndpoint.cs | 46 + .../V1/GetAllSearchConfigurationsQuery.cs | 24 + .../V1/GetAllSearchConfigurationsResponse.cs | 93 + .../GetSearchConfigurationBySlugEndpoint.cs | 45 + .../V1/GetSearchConfigurationBySlugQuery.cs | 29 + .../GetSearchConfigurationBySlugResponse.cs | 37 + .../V1/UpdateSearchConfigurationCommand.cs | 85 + .../UpdateSearchConfigurationCommandForDb.cs | 110 + .../V1/UpdateSearchConfigurationEndpoint.cs | 81 + .../V1/UpdateSearchConfigurationResponse.cs | 81 + .../V1/GetAllSiteConfigurationsEndpoint.cs | 49 + .../V1/GetAllSiteConfigurationsQuery.cs | 23 + .../V1/GetAllSiteConfigurationsResponse.cs | 39 + .../V1/GetSiteConfigurationBySlugEndpoint.cs | 49 + .../V1/GetSiteConfigurationBySlugQuery.cs | 29 + .../V1/GetSiteConfigurationBySlugResponse.cs | 34 + .../SiteConfigurationExtensions.cs | 21 + .../V1/UpdateSiteConfigurationCommand.cs | 35 + .../V1/UpdateSiteConfigurationCommandForDb.cs | 66 + .../V1/UpdateSiteConfigurationEndpoint.cs | 80 + .../V1/UpdateSiteConfigurationResponse.cs | 37 + .../apis/AStar.Dev.Admin.Api/appsettings.json | 37 + .../astar-logging-settings.json | 59 + .../apis/AStar.Dev.Admin.Api/astar.ico | Bin 0 -> 16958 bytes .../wwwroot/swagger-ui/SwaggerDark.css | 1359 +++++++++++ .../AStar.Dev.Files.Api.csproj | 64 + .../apis/AStar.Dev.Files.Api/AStar.png | Bin 0 -> 15984 bytes .../apis/AStar.Dev.Files.Api/AddEndpoints.cs | 32 + .../AllFiles/V1/GetAllFilesEndpoint.cs | 68 + .../AllFiles/V1/GetAllFilesQuery.cs | 77 + .../AllFiles/V1/GetAllFilesQueryResponse.cs | 12 + .../V1/GetAllClassificationsEndpoint.cs | 43 + .../V1/GetAllClassificationsResponse.cs | 16 + .../GetAllClassificationsForFileEndpoint.cs | 45 + .../V1/GetAllClassificationsForFileQuery.cs | 10 + .../GetAllClassificationsForFileResponse.cs | 5 + .../AStar.Dev.Files.Api/Config/SearchType.cs | 23 + .../GetAll/V1/GetAllDirectoriesEndpoint.cs | 41 + .../GetAll/V1/GetAllDirectoriesQuery.cs | 19 + .../V1/GetAllDirectoriesQueryResponse.cs | 54 + .../apis/AStar.Dev.Files.Api/Dockerfile | 33 + .../V1/FileDetailsQueryableExtensions.cs | 75 + .../Count/V1/GetDuplicatesCountEndpoint.cs | 53 + .../Count/V1/GetDuplicatesCountQuery.cs | 75 + .../V1/GetDuplicatesCountQueryResponse.cs | 6 + .../V1/GetDuplicatesCountQueryWithUser.cs | 66 + .../Duplicates/FileGrouping.cs | 3 + .../Get/V1/FileDetailsQueryableExtensions.cs | 115 + .../Get/V1/GetDuplicatesEndpoint.cs | 54 + .../Duplicates/Get/V1/GetDuplicatesQuery.cs | 77 + .../Get/V1/GetDuplicatesQueryResponse.cs | 21 + .../Get/V1/GetDuplicatesQueryWithUser.cs | 66 + .../AStar.Dev.Files.Api/EndpointConstants.cs | 132 ++ .../Endpoints/BaseSearchParameters.cs | 35 + .../Endpoints/Count.CountSearchParameters.cs | 44 + .../AStar.Dev.Files.Api/Endpoints/Count.cs | 55 + ...licates.CountDuplicatesSearchParameters.cs | 44 + .../Endpoints/CountDuplicates.cs | 54 + .../Endpoints/FileAccessDetail.cs | 42 + .../Endpoints/FileDetail.cs | 45 + .../Endpoints/FilesDatabaseUpdateEndpoint.cs | 7 + .../Endpoints/List.ListSearchParameters.cs | 70 + .../AStar.Dev.Files.Api/Endpoints/List.cs | 61 + .../ListDuplicates.DuplicatesResponse.cs | 19 + .../Endpoints/ListDuplicates.FileSizeDto.cs | 21 + ...plicates.ListDuplicatesSearchParameters.cs | 70 + .../Endpoints/ListDuplicates.cs | 78 + .../Endpoints/MarkForHardDeletion.cs | 45 + .../Endpoints/MarkForMoving.cs | 45 + .../Endpoints/MarkForSoftDeletion.Request.cs | 10 + .../Endpoints/MarkForSoftDeletion.cs | 44 + .../Endpoints/UndoMarkForHardDeletion.cs | 45 + .../Endpoints/UndoMarkForMoving.cs | 45 + .../Endpoints/UndoMarkForSoftDeletion.cs | 45 + .../Update.DirectoryChangeRequest.cs | 23 + .../AStar.Dev.Files.Api/Endpoints/Update.cs | 131 ++ .../FileInfoDtoExtensions.cs | 17 + .../V1/MarkFileForHardDeletionCommand.cs | 17 + .../MarkFileForHardDeletionCommandResponse.cs | 3 + .../V1/MarkFileForHardDeletionEndpoint.cs | 56 + .../V1/UnMarkFileForHardDeletionCommand.cs | 17 + ...nMarkFileForHardDeletionCommandResponse.cs | 3 + .../V1/UnMarkFileForHardDeletionEndpoint.cs | 55 + .../AStar.Dev.Files.Api/IAssemblyMarker.cs | 7 + .../apis/AStar.Dev.Files.Api/IEndpoint.cs | 10 + .../V1/MarkFileForMovingCommand.cs | 17 + .../V1/MarkFileForMovingCommandResponse.cs | 3 + .../V1/MarkFileForMovingEndpoint.cs | 54 + .../V1/UnMarkFileForMovingCommand.cs | 17 + .../V1/UnMarkFileForMovingCommandResponse.cs | 3 + .../V1/UnMarkFileForMovingEndpoint.cs | 54 + .../Models/FileAccessDetailDto.cs | 66 + .../AStar.Dev.Files.Api/Models/FileInfoDto.cs | 100 + .../apis/AStar.Dev.Files.Api/Program.cs | 103 + .../Properties/launchSettings.json | 15 + .../V1/MarkFileForSoftDeletionCommand.cs | 17 + .../MarkFileForSoftDeletionCommandResponse.cs | 5 + .../V1/MarkFileForSoftDeletionEndpoint.cs | 57 + .../V1/UnMarkFileForSoftDeletionCommand.cs | 17 + ...nMarkFileForSoftDeletionCommandResponse.cs | 5 + .../V1/UnMarkFileForSoftDeletionEndpoint.cs | 55 + .../StartupConfiguration/Services.cs | 27 + .../apis/AStar.Dev.Files.Api/appsettings.json | 37 + .../astar-logging-settings.json | 59 + .../apis/AStar.Dev.Files.Api/astar.ico | Bin 0 -> 16958 bytes .../wwwroot/swagger-ui/SwaggerDark.css | 1359 +++++++++++ ...AStar.Dev.Files.Classifications.Api.csproj | 13 + .../AStar.Dev.Files.Classifications.Api.http | 6 + .../FileClassification.cs | 6 + .../Program.cs | 39 + .../Properties/launchSettings.json | 23 + .../appsettings.Development.json | 8 + .../appsettings.json | 9 + .../AStar.Dev.Images.Api.csproj | 55 + .../apis/AStar.Dev.Images.Api/AStar.png | Bin 0 -> 15984 bytes .../apis/AStar.Dev.Images.Api/AddEndpoints.cs | 32 + .../apis/AStar.Dev.Images.Api/Dockerfile | 34 + .../AStar.Dev.Images.Api/EndpointConstants.cs | 22 + .../Extensions/FileInfoDtoExtensions.cs | 17 + .../Extensions/FileInfoExtensions.cs | 14 + .../Extensions/StringExtensions.cs | 30 + .../FileSizeEqualityComparer.cs | 43 + .../AStar.Dev.Images.Api/IAssemblyMarker.cs | 7 + .../apis/AStar.Dev.Images.Api/IEndpoint.cs | 10 + .../Images/GetImageEndpoint.cs | 149 ++ .../AStar.Dev.Images.Api/Models/Dimensions.cs | 52 + .../Models/FileInfoDto.cs | 42 + .../AStar.Dev.Images.Api/Models/FileSize.cs | 55 + .../apis/AStar.Dev.Images.Api/Program.cs | 132 ++ .../Properties/launchSettings.json | 15 + .../Services/IImageService.cs | 14 + .../Services/ImageService.cs | 42 + .../StartupConfiguration/Services.cs | 46 + .../AStar.Dev.Images.Api/appsettings.json | 40 + .../astar-logging-settings.json | 65 + .../apis/AStar.Dev.Images.Api/astar.ico | Bin 0 -> 16958 bytes .../wwwroot/swagger-ui/SwaggerDark.css | 1359 +++++++++++ .../AStar.Dev.ToDo.Api.csproj | 24 + .../AStar.Dev.ToDo.Api/Controllers/ToDo.cs | 10 + .../Controllers/ToDoListController.cs | 59 + .../apis/AStar.Dev.ToDo.Api/Dockerfile | 23 + .../apis/AStar.Dev.ToDo.Api/Program.cs | 14 + .../Properties/launchSettings.json | 14 + .../Properties/serviceDependencies.json | 7 + .../Properties/serviceDependencies.local.json | 7 + .../apis/AStar.Dev.ToDo.Api/Startup.cs | 35 + .../apis/AStar.Dev.ToDo.Api/appsettings.json | 14 + .../modules/apis/AStar.Dev.ToDo.Api/astar.ico | Bin 0 -> 16958 bytes .../AStar.Web.ApiService.csproj | 24 + .../AStar.Web.ApiService.http | 6 + .../apis/AStar.Web.ApiService/Program.cs | 62 + .../Properties/launchSettings.json | 14 + .../AStar.Web.ApiService/appsettings.json | 9 + .../AStar.Dev.FilesDb.MigrationService.csproj | 20 + .../Program.cs | 15 + .../Properties/launchSettings.json | 12 + .../Worker.cs | 85 + .../appsettings.json | 8 + .../AStar.Dev.Usage.Logger.csproj | 40 + .../AStar.Dev.Usage.Logger.http | 6 + .../services/AStar.Dev.Usage.Logger/AStar.png | Bin 0 -> 15984 bytes .../AStar.Dev.Usage.Logger/AddEndpoints.cs | 32 + .../AStar.Dev.Usage.Logger/Dockerfile | 30 + .../EndpointConstants.cs | 22 + .../HalfSecondPeriodicTimer.cs | 12 + .../AStar.Dev.Usage.Logger/IAssemblyMarker.cs | 7 + .../AStar.Dev.Usage.Logger/IEndpoint.cs | 10 + .../AStar.Dev.Usage.Logger/IPeriodicTimer.cs | 6 + .../AStar.Dev.Usage.Logger/JsonSettings.cs | 8 + .../services/AStar.Dev.Usage.Logger/LICENSE | 21 + .../ProcessUsageEventsService.cs | 100 + .../AStar.Dev.Usage.Logger/Program.cs | 85 + .../Properties/launchSettings.json | 15 + .../services/AStar.Dev.Usage.Logger/Readme.md | 1 + .../Usage/GetAll/V1/ApiUsageEventDto.cs | 19 + .../GetAll/V1/GetAllApiUsageEventsEndpoint.cs | 60 + .../AStar.Dev.Usage.Logger/appsettings.json | 33 + .../astar-logging-settings.json | 58 + .../services/AStar.Dev.Usage.Logger/astar.ico | Bin 0 -> 16958 bytes .../wwwroot/swagger-ui/SwaggerDark.css | 1359 +++++++++++ .../microsoft-identity-association.json | 7 + .../uis/AStar.Dev.Web/AStar.Dev.Web.csproj | 44 + .../uis/AStar.Dev.Web/Components/App.razor | 54 + .../uis/AStar.Dev.Web/Components/App.razor.cs | 6 + .../Components/Layout/LoginDisplay.razor | 0 .../Components/Layout/LoginDisplay.razor.cs | 1 + .../Components/Layout/MainLayout.razor | 35 + .../Components/Layout/MainLayout.razor.cs | 10 + .../Components/Layout/MainLayout.razor.css | 0 .../Layout/Menu/AdminMenuService.cs | 1 + .../Layout/Menu/DirectoriesMenuService.cs | 1 + .../Components/Layout/Menu/FileMenuService.cs | 1 + .../Layout/Menu/GamesMenuService.cs | 1 + .../Layout/Menu/IMenuItemsService.cs | 1 + .../Layout/Menu/ImagesMenuService.cs | 1 + .../Layout/Menu/MenuItemsService.cs | 1 + .../Layout/Menu/NuGetMenuService.cs | 1 + .../Components/Layout/NavMenu.razor | 57 + .../Components/Layout/NavMenu.razor.cs | 42 + .../Components/Layout/NavMenu.razor.css | 0 .../Components/Layout/SettingsPanel.razor | 51 + .../Components/Layout/SettingsPanel.razor.cs | 72 + .../Pages/Admin/AddFilesToDatabase.razor | 5 + .../Pages/Admin/AddFilesToDatabase.razor.cs | 11 + .../Components/Pages/Admin/ApiUsage.razor | 5 + .../Components/Pages/Admin/ApiUsage.razor.cs | 12 + .../Pages/Admin/AuthenticationCheck.razor | 45 + .../Pages/Admin/AuthenticationCheck.razor.cs | 41 + .../Pages/Admin/FileClassifications.razor | 199 ++ .../Pages/Admin/FileClassifications.razor.cs | 9 + .../Pages/Admin/ModelsToIgnore.razor | 5 + .../Pages/Admin/ModelsToIgnore.razor.cs | 11 + .../Pages/Admin/ScrapeDirectories.razor | 6 + .../Pages/Admin/ScrapeDirectories.razor.cs | 11 + .../Pages/Admin/SiteConfiguration.razor | 7 + .../Pages/Admin/SiteConfiguration.razor.cs | 11 + .../Components/Pages/Admin/TagsToIgnore.razor | 5 + .../Pages/Admin/TagsToIgnore.razor.cs | 11 + .../Components/Pages/Counter.razor | 13 + .../Components/Pages/Counter.razor.cs | 10 + .../Components/Pages/Dashboard.razor | 6 + .../Components/Pages/Dashboard.razor.cs | 8 + .../Components/Pages/Dashboard.razor.css | 0 .../Pages/Directories/MoveDirectories.razor | 6 + .../Directories/MoveDirectories.razor.cs | 7 + .../Pages/Directories/RenameDirectories.razor | 6 + .../Directories/RenameDirectories.razor.cs | 7 + .../Components/Pages/Error.razor | 37 + .../Pages/Files/DuplicateFiles.razor | 6 + .../Pages/Files/DuplicateFiles.razor.cs | 7 + .../Components/Pages/Files/MoveFiles.razor | 6 + .../Components/Pages/Files/MoveFiles.razor.cs | 7 + .../Components/Pages/Files/RenameFiles.razor | 6 + .../Pages/Files/RenameFiles.razor.cs | 7 + .../AStar.Dev.Web/Components/Pages/Home.razor | 41 + .../Components/Pages/Home.razor.cs | 1 + .../Pages/Images/DuplicateImages.razor | 8 + .../Pages/Images/DuplicateImages.razor.cs | 25 + .../Components/Pages/Images/MoveImages.razor | 6 + .../Pages/Images/MoveImages.razor.cs | 7 + .../Pages/Images/RandomImages.razor | 5 + .../Pages/Images/RandomImages.razor.cs | 7 + .../Pages/Images/RenameImages.razor | 5 + .../Pages/Images/RenameImages.razor.cs | 7 + .../Pages/Images/ScrapeImages.razor | 5 + .../Pages/Images/ScrapeImages.razor.cs | 7 + .../Components/Pages/KidsGames/Games.razor | 39 + .../Components/Pages/KidsGames/Games.razor.cs | 19 + .../Components/Pages/KidsGames/Halving.razor | 54 + .../Pages/KidsGames/Halving.razor.cs | 20 + .../Components/Pages/KidsGames/Matching.razor | 39 + .../Pages/KidsGames/Matching.razor.cs | 20 + .../Pages/KidsGames/halving/Halving10.razor | 114 + .../KidsGames/halving/Halving10.razor.cs | 21 + .../Pages/KidsGames/halving/Halving2.razor | 77 + .../Pages/KidsGames/halving/Halving2.razor.cs | 21 + .../Pages/KidsGames/halving/Halving4.razor | 84 + .../Pages/KidsGames/halving/Halving4.razor.cs | 21 + .../Pages/KidsGames/halving/Halving6.razor | 100 + .../Pages/KidsGames/halving/Halving6.razor.cs | 21 + .../Pages/KidsGames/halving/Halving8.razor | 104 + .../Pages/KidsGames/halving/Halving8.razor.cs | 21 + .../KidsGames/matching/MatchAnimals.razor | 91 + .../KidsGames/matching/MatchAnimals.razor.cs | 21 + .../KidsGames/matching/MatchHouses.razor | 91 + .../KidsGames/matching/MatchHouses.razor.cs | 21 + .../AStarDevFunctionalResults.razor | 81 + .../AStarDevFunctionalResults.razor.cs | 21 + .../NuGetDocumentation/MarkdownView.razor | 4 + .../NuGetDocumentation/MarkdownView.razor.cs | 35 + .../NuGetDocumentation.razor | 12 + .../NuGetDocumentation.razor.cs | 19 + .../Components/Pages/Shared/Search.razor | 93 + .../Components/Pages/Shared/Search.razor.cs | 62 + .../Components/Pages/Weather.razor | 27 + .../Components/Pages/Weather.razor.cs | 32 + .../Components/Pages/_Imports.razor | 2 + .../uis/AStar.Dev.Web/Components/Routes.razor | 25 + .../AStar.Dev.Web/Components/_Imports.razor | 12 + .../uis/AStar.Dev.Web/FilesApiOptions.cs | 7 + .../uis/AStar.Dev.Web/IAssemblyMarker.cs | 7 + .../Models/FileClassification.cs | 53 + .../uis/AStar.Dev.Web/Models/SearchModel.cs | 65 + .../uis/AStar.Dev.Web/Models/SearchType.cs | 27 + .../uis/AStar.Dev.Web/Models/SortOrder.cs | 27 + .../uis/AStar.Dev.Web/Program.cs | 20 + .../Properties/launchSettings.json | 14 + .../Services/FileClassificationsService.cs | 12 + .../Services/IFileClassificationsService.cs | 15 + .../uis/AStar.Dev.Web/WeatherApiClient.cs | 30 + .../WebApplicationBuilderExtensions.cs | 50 + .../AStar.Dev.Web/WebApplicationExtensions.cs | 95 + web/astar-dev-old/uis/AStar.Dev.Web/app.http | 4 + .../uis/AStar.Dev.Web/appsettings.json | 86 + .../AStar.Dev.Web/astar-logging-settings.json | 59 + .../uis/AStar.Dev.Web/astar-logo-large.png | Bin 0 -> 333770 bytes web/astar-dev-old/uis/AStar.Dev.Web/astar.ico | Bin 0 -> 16958 bytes web/astar-dev-old/uis/AStar.Dev.Web/astar.png | Bin 0 -> 15984 bytes .../uis/AStar.Dev.Web/wwwroot/app.css | 191 ++ .../wwwroot/assets/astar-logo.png | Bin 0 -> 14032 bytes .../AStar.Dev.Web/wwwroot/assets/astar.ico | Bin 0 -> 16958 bytes .../AStar.Dev.Web/wwwroot/assets/astar.png | Bin 0 -> 15984 bytes .../wwwroot/assets/fontawesome/LICENSE.txt | 165 ++ .../assets/fontawesome/css/all.min.css | 9 + .../assets/fontawesome/css/brands.min.css | 6 + .../fontawesome/css/fontawesome.min.css | 8 + .../fontawesome/webfonts/fa-brands-400.woff2 | Bin 0 -> 101088 bytes .../fontawesome/webfonts/fa-regular-400.woff2 | Bin 0 -> 18892 bytes .../fontawesome/webfonts/fa-solid-900.woff2 | Bin 0 -> 113108 bytes .../webfonts/fa-v4compatibility.woff2 | Bin 0 -> 4132 bytes .../uis/AStar.Dev.Web/wwwroot/css/app.css | 11 + .../wwwroot/docs/FunctionalResult.md | 65 + .../uis/AStar.Dev.Web/wwwroot/favicon.ico | Bin 0 -> 16958 bytes .../uis/AStar.Dev.Web/wwwroot/favicon.png | Bin 0 -> 1148 bytes .../wwwroot/games/css/drag-n-drop.css | 22 + .../AStar.Dev.Web/wwwroot/games/css/grid.css | 19 + .../AStar.Dev.Web/wwwroot/games/css/index.css | 28 + .../AStar.Dev.Web/wwwroot/games/css/menu.css | 8 + .../wwwroot/games/halving/css/halving.css | 3 + .../wwwroot/games/halving/images/burger.png | Bin 0 -> 6790 bytes .../wwwroot/games/halving/images/cookie.png | Bin 0 -> 6599 bytes .../wwwroot/games/halving/images/cupcake.png | Bin 0 -> 9177 bytes .../wwwroot/games/halving/images/fries.png | Bin 0 -> 9693 bytes .../wwwroot/games/halving/images/icecream.png | Bin 0 -> 9173 bytes .../halving/images/monsters-at-the-table.png | Bin 0 -> 285749 bytes .../wwwroot/games/halving/js/halving-10.js | 142 ++ .../wwwroot/games/halving/js/halving-2.js | 74 + .../wwwroot/games/halving/js/halving-4.js | 89 + .../wwwroot/games/halving/js/halving-6.js | 102 + .../wwwroot/games/halving/js/halving-8.js | 120 + .../wwwroot/games/images/stone.jpg | Bin 0 -> 41701 bytes .../wwwroot/games/images/tree.jpg | Bin 0 -> 42926 bytes .../wwwroot/games/js/drag-n-drop.js | 28 + .../AStar.Dev.Web/wwwroot/games/js/index.js | 16 + .../wwwroot/games/js/touch-events.js | 15 + .../AStar.Dev.Web/wwwroot/games/js/touch.js | 15 + .../wwwroot/games/match/css/grid-by-2.css | 20 + .../wwwroot/games/match/css/match.css | 25 + .../wwwroot/games/match/images/cottage.jpg | Bin 0 -> 23995 bytes .../games/match/images/detached-house.jpg | Bin 0 -> 17928 bytes .../wwwroot/games/match/images/fish.jpg | Bin 0 -> 2699 bytes .../wwwroot/games/match/images/kittens.jpg | Bin 0 -> 2385 bytes .../wwwroot/games/match/images/mouse.jpg | Bin 0 -> 4103 bytes .../wwwroot/games/match/images/puppy.jpg | Bin 0 -> 2446 bytes .../games/match/images/semi-detached.jpg | Bin 0 -> 20064 bytes .../games/match/images/terraced-house.jpg | Bin 0 -> 19721 bytes .../wwwroot/games/match/js/match-animals.js | 98 + .../wwwroot/games/match/js/match-houses.js | 107 + 1826 files changed, 120901 insertions(+), 37 deletions(-) create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/AStar.Dev.OneDrive.Client.Core.csproj create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/ConfigurationSettings/MsalConfigurationSettings.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Dtos/DeltaPage.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Dtos/LocalFileInfo.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Dtos/UploadSessionInfo.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Account.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/AccountEntity.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/AccountInfo.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DebugLog.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DebugLogEntity.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DebugLogEntry.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DeltaToken.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DriveItemRecord.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/SelectionState.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/SyncState.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/TransferStatus.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/TransferType.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileChangeEvent.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileMetadata.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileMetadataEntity.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileOperationLog.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileOperationLogEntity.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/LocalFileRecord.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConfigurationEntity.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConflict.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConflictEntity.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncSessionLog.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncSessionLogEntity.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncState.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/TransferLog.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/WindowPreferences.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/WindowPreferencesEntity.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/IAuthService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/IFileSystemAdapter.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/IGraphClient.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/ISyncRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/ConflictResolutionStrategy.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/FileChangeType.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/FileOperation.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/FileSyncStatus.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/SyncDirection.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/SyncStatus.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/SyncConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Utilities/Result.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Utilities/SyncSettings.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/AStar.Dev.OneDrive.Client.Infrastructure.csproj create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Auth/MsalAuthService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/AppDbContext.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/AccountConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/AccountEntityConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/DeltaTokenConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/DriveItemRecordConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/FileMetadataConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/LocalFileRecordConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/SyncConfigurationConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/SyncConflictConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/TransferLogConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/WindowPreferencesConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/DbInitializer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/ModelBuilderExtensions.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/AccountRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/DebugLogRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/EfSyncRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/FileMetadataRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/FileOperationLogRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IAccountRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IDebugLogRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IFileMetadataRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IFileOperationLogRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/ISyncConfigurationRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/ISyncConflictRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/ISyncSessionLogRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/SyncConfigurationRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/SyncConflictRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/SyncSessionLogRepository.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/SqliteTypeConverters.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/DependencyInjection/InfrastructureServiceCollectionExtensions.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/FileSystem/LocalFileSystemAdapter.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Graph/GraphClientWrapper.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Graph/GraphPathHelpers.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/HealthChecks/DatabaseHealthCheck.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/HealthChecks/GraphApiHealthCheck.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221025933_InitialCreation.Designer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221025933_InitialCreation.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221055438_AddBytesTransferredToTransferLogs.Designer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221055438_AddBytesTransferredToTransferLogs.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221085945_Test.Designer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221085945_Test.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116201123_AdditionalTablesForConsumption.Designer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116201123_AdditionalTablesForConsumption.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116223008_RenameTokenValueToToken.Designer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116223008_RenameTokenValueToToken.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260118103718_V3Tables.Designer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260118103718_V3Tables.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/AppDbContextModelSnapshot.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/AStar.Dev.OneDrive.Client.Services.csproj create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ChannelFactory.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/AppPathHelper.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/ApplicationSettings.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/EntraIdSettings.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/FileServices.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/UiSettings.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/UserPreferences.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/WindowSettings.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DeltaPageProcessor.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DownloadQueueConsumer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DownloadQueueProducer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/HealthCheckService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IChannelFactory.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IDeltaPageProcessor.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IDownloadQueueConsumer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IDownloadQueueProducer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ILocalFileScanner.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ISyncEngine.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ISyncErrorLogger.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ISyncProgressReporter.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ITransferService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IUploadQueueConsumer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IUploadQueueProducer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/LocalFileScanner.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncEngine.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncErrorLogger.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncExtensions.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncOperationType.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncProgress.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncProgressReporter.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncSettings.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/Syncronisation/ISyncronisationCoordinator.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/Syncronisation/SyncronisationCoordinator.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/TransferProgress.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/TransferService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/Untitled-1.sqlite3-query create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/UploadQueueConsumer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/UploadQueueProducer.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/AStar.Dev.OneDrive.Client.csproj create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/App.axaml create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/App.axaml.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ApplicationMetadata.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Assets/astar-logo.png create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Assets/astar.ico create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Assets/astar.png create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/AutoSaveService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/FileWriter.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/IAutoSaveService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/OneDriveClientConstants.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Converters/BooleanNegationConverter.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Converters/EnumToBooleanConverter.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/ApplicationName.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/DriveId.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/ItemId.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/LocalDriveId.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FodyWeavers.xml create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthenticationClient.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthenticationResult.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/IAuthService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/IAuthenticationClient.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/MsalAuthResult.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/AutoSyncCoordinator.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/AutoSyncSchedulerService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Configuration/AccountEntityConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DatabaseConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DebugLog.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DebugLogContext.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DebugLogger.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/FileWatcherService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IAutoSyncCoordinator.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IAutoSyncSchedulerService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IDebugLogger.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IFileWatcherService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IFolderTreeService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ILocalFileScanner.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IRemoteChangeDetector.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ISyncEngine.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ISyncSelectionService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IWindowPreferencesService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/LocalFileScanner.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/LogCleanupBackgroundService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/OneDriveServices/FolderTreeService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/OneDriveServices/GraphApiClient.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/OneDriveServices/IGraphApiClient.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ProgressReporterService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/RemoteChangeDetector.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ServiceConfiguration.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/SyncEngine.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/SyncSelectionService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/WindowPreferencesService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/HostExtensions.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Models/OneDriveFolderNode.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/OneDrive Notes and Issues.txt create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/OperationConstants.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Program.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/ISettingsAndPreferencesService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/SettingsAndPreferencesService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/UiSettings.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/WindowSettings.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Styles/Theme.axaml create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SyncConflicts/ConflictItemViewModel.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SyncConflicts/ConflictResolutionView.axaml create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SyncConflicts/ConflictResolutionView.axaml.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SyncConflicts/ConflictResolutionViewModel.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SyncConflicts/ConflictResolver.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SyncConflicts/IConflictResolver.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SyncConflicts/SyncConflict.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Theme/IThemeMapper.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Theme/IThemeSelectionHandler.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Theme/IThemeService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Theme/ThemeMapper.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Theme/ThemeSelectionHandler.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Theme/ThemeService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/AccountManagementViewModel.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/DashboardViewModel.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/DebugLogViewModel.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/IMainWindowCoordinator.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/ISyncCommandService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/ISyncStatusTarget.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/IWindowPositionValidator.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/IWindowPositionable.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/MainWindow.axaml create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/MainWindow.axaml.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/MainWindowCoordinator.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/MainWindowViewModel.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/SyncCommandService.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/SyncProgressViewModel.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/SyncTreeViewModel.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/UpdateAccountDetailsViewModel.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/ViewModelBase.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/ViewSyncHistoryViewModel.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/WindowPositionValidator.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ViewModels/WindowPositionable.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Views/AccountManagementView.axaml create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Views/AccountManagementView.axaml.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Views/DebugLogWindow.axaml create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Views/DebugLogWindow.axaml.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Views/SyncProgressView.axaml create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Views/SyncProgressView.axaml.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Views/SyncTreeView.axaml create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Views/SyncTreeView.axaml.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Views/UpdateAccountDetailsWindow.axaml create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Views/UpdateAccountDetailsWindow.axaml.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Views/ViewSyncHistoryWindow.axaml create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Views/ViewSyncHistoryWindow.axaml.cs create mode 100644 apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/appsettings.json create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Application/AStar.Dev.OneDrive.Sync.Client.Application.csproj create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Application/Interfaces/IMigrationService.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Application/Interfaces/ISyncService.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Application/Services/SyncService.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Domain/AStar.Dev.OneDrive.Sync.Client.Domain.csproj create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Domain/Entities/SyncFile.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Domain/Interfaces/ISyncFileRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/AStar.Dev.OneDrive.Sync.Client.Infrastructure.csproj create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/AstarOneDriveDbContext.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/AstarOneDriveDbContextFactory.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Configurations/AccountConfiguration.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Configurations/SettingConfiguration.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Configurations/SyncFileConfiguration.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Contracts/AccountState.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Contracts/FolderNodeState.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Contracts/SettingsState.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/DatabasePathResolver.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Entities/AccountEntity.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Entities/SettingEntity.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Entities/SyncFileEntity.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Migrations/20260223150000_InitialCreate.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Migrations/AstarOneDriveDbContextModelSnapshot.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Repositories/SqliteAccountsRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Repositories/SqliteFolderTreeRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Repositories/SqliteSettingsRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/SqliteDatabaseMigrator.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/OneDriveSyncFileRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure/ServiceCollectionExtensions.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/AStar.Dev.OneDrive.Sync.Client.UI.csproj create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/AccountManagement/AccountDialogView.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/AccountManagement/AccountDialogView.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/AccountManagement/AccountDialogViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/AccountManagement/AccountInfo.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/AccountManagement/AccountListView.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/AccountManagement/AccountListView.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/AccountManagement/AccountListViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/App.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/App.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/ApplicationMetadata.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Assets/astar.png create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Common/ErrorDialog.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Common/ErrorDialog.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Common/ErrorHandler.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Common/LayoutType.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Common/RelayCommand.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Common/ViewModelBase.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Composition/CompositionRoot.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/ExceptionBootstrap.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/FolderTrees/FolderNode.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/FolderTrees/FolderTreeView.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/FolderTrees/FolderTreeView.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/FolderTrees/FolderTreeViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Home/MainWindow.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Home/MainWindow.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Home/MainWindowViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/InMemoryLogSink.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Layouts/DashboardLayoutView.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Layouts/DashboardLayoutView.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Layouts/DashboardLayoutViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Layouts/ExplorerLayoutView.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Layouts/ExplorerLayoutView.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Layouts/ExplorerLayoutView.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Layouts/TerminalLayoutView.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Layouts/TerminalLayoutView.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Layouts/TerminalLayoutView.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Locales/en-GB.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Locales/en-US.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Localization/LocalizationManager.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/LoggingBootstrap.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Metrics.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Program.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/SerilogTraceListener.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Settings/SettingsView.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Settings/SettingsView.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Settings/SettingsViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/SyncStatus/SyncActivityEntry.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/SyncStatus/SyncStatusView.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/SyncStatus/SyncStatusView.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/SyncStatus/SyncStatusViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/ThemeManager/ThemeManager.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Themes/Base.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Themes/Colorful.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Themes/Dark.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Themes/Hacker.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Themes/HighContrast.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Themes/Light.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/Themes/Professional.axaml create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/ViewLocator.cs create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI/app.manifest create mode 100644 apps/astar-dev-onedrive-sync-client-v2/AstarOneDrive.Infrastructure/Data/DatabasePathResolver.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/AStar.Dev.OneDrive.Sync.Client.Core.csproj create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/AccountIdHasher.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/AdminAccountMetadata.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/ApplicationMetadata.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Data/DatabaseConfiguration.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Data/Entities/AccountEntity.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Data/Entities/DebugLogEntity.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Data/Entities/DriveItemEntity.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Data/Entities/FileOperationLogEntity.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Data/Entities/SyncConflictEntity.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Data/Entities/SyncSessionLogEntity.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Data/Entities/WindowPreferencesEntity.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/DebugLogMetadata.DeltaProcessingService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/DebugLogMetadata.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/AccountInfo.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/AccountInfoExtensions.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/DebugLogEntry.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/DeltaPage.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/DeltaToken.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/Enums/ConflictResolutionStrategy.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/Enums/FileChangeType.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/Enums/FileOperation.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/Enums/FileSyncStatus.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/Enums/SyncDirection.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/Enums/SyncStatus.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/Enums/ThemePreference.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/FileChangeEvent.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/FileMetadata.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/FileOperationLog.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/HashedAccountId.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/MsalConfigurationSettings.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/OneDrive/FileSystemInfo.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/OneDrive/Hashes.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/OneDrive/Image.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/OneDrive/OneDriveFile.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/OneDrive/OneDriveFolder.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/OneDrive/OneDriveResponse.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/OneDrive/ParentReference.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/OneDrive/SpecialFolder.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/OneDrive/User.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/OneDrive/Value.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/OneDrive/View.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/OneDriveFolderNode.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/SelectionState.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/SyncConfiguration.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/SyncConflict.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/SyncSessionLog.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/SyncState.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core/Models/WindowPreferences.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/AStar.Dev.OneDrive.Sync.Client.Infrastructure.csproj create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Configuration/AccountEntityConfiguration.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Configuration/DebugLogEntityConfiguration.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Configuration/DeltaTokenConfiguration.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Configuration/DriveItemRecordConfiguration.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Configuration/FileOperationLogEntityConfiguration.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Configuration/SyncConflictEntityConfiguration.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Configuration/SyncSessionLogEntityConfiguration.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Configuration/WindowPreferencesEntityConfiguration.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Migrations/20260217130717_InitialCreation3.Designer.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Migrations/20260217130717_InitialCreation3.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Migrations/20260218165504_SessionIdToGuid.Designer.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Migrations/20260218165504_SessionIdToGuid.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/Migrations/SyncDbContextModelSnapshot.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/ModelBuilderExtensions.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/SqliteTypeConverters.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/SyncDbContext.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Data/SyncDbContextFactory.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/AccountRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/DebugLogRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/DriveItemsRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/EfSyncRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/FileOperationLogRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/FolderSelectionExtensions.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/IAccountRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/IDebugLogRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/IDriveItemsRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/IFileOperationLogRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/ISyncConfigurationRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/ISyncConflictRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/ISyncRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/ISyncSessionLogRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/SyncConfigurationRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/SyncConflictRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Repositories/SyncSessionLogRepository.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/SerilogLogParser.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/Authentication/AuthConfiguration.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/Authentication/AuthService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/Authentication/AuthenticationClient.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/Authentication/AuthenticationResult.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/Authentication/AuthenticationResultExtensions.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/Authentication/IAuthService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/Authentication/IAuthenticationClient.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/Authentication/MsalAuthResult.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/AutoSyncCoordinator.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/AutoSyncSchedulerService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/ConflictDetectionService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/DebugLog.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/DebugLogContext.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/DebugLoggerService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/DeletionSyncService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/DeltaPageProcessor.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/DeltaProcessingService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/FileWatcherService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/GraphApiClient.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/GraphPathHelpers.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/IAutoSyncCoordinator.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/IAutoSyncSchedulerService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/IConflictDetectionService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/IDebugLogger.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/IDeletionSyncService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/IDeltaPageProcessor.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/IDeltaProcessingService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/IFileWatcherService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/IGraphApiClient.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/ILocalFileScanner.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/IRemoteChangeDetector.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/ISyncEngine.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/ISyncSelectionService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/ISyncStateCoordinator.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/IThemeService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/IThemeStartupCoordinator.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/IWindowPreferencesService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/LocalFileScanner.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/LogCleanupBackgroundService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/OneDriveServices/FileTransferService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/OneDriveServices/FolderTreeService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/OneDriveServices/IFileTransferService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/OneDriveServices/IFolderTreeService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/ProgressReporterService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/RemoteChangeDetector.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/SyncEngine.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/SyncError.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/SyncSelectionService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/SyncStateCoordinator.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/ThemeService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/ThemeStartupCoordinator.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure/Services/WindowPreferencesService.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/AStar.Dev.OneDrive.Sync.Client.csproj create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Accounts/AccountManagementView.axaml create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Accounts/AccountManagementView.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Accounts/AccountManagementViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Accounts/UpdateAccountDetailsViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Accounts/UpdateAccountDetailsWindow.axaml create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Accounts/UpdateAccountDetailsWindow.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/App.axaml create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/App.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/AppHost.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/ApplicationMetadata.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/ApplicationName.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/ApplicationServiceExtensions.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Assets/astar.png create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/AuthenticationExtensions.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/ConfigurationSettings/AppPathHelper.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/ConfigurationSettings/ApplicationSettings.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/ConfigurationSettings/EntraIdSettings.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Converters/BoolToStatusColorConverter.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Converters/BoolToStatusTextConverter.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Converters/BooleanNegationConverter.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Converters/EnumToBooleanConverter.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Converters/InitialsConverter.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Converters/ThemePreferenceToDisplayNameConverter.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/DatabaseServiceExtensions.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/DebugLogs/DebugLogViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/DebugLogs/DebugLogWindow.axaml create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/DebugLogs/DebugLogWindow.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/HttpClientExtension.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/MainWindow/MainWindow.axaml create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/MainWindow/MainWindow.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/MainWindow/MainWindowViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Program.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Settings/SettingsViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Settings/SettingsWindow.axaml create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Settings/SettingsWindow.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Syncronisation/SyncProgressView.axaml create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Syncronisation/SyncProgressView.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Syncronisation/SyncProgressViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Syncronisation/SyncTreeView.axaml create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Syncronisation/SyncTreeView.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Syncronisation/SyncTreeViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Syncronisation/ViewSyncHistoryViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Syncronisation/ViewSyncHistoryWindow.axaml create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Syncronisation/ViewSyncHistoryWindow.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/SyncronisationConflicts/ConflictItemViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/SyncronisationConflicts/ConflictResolutionView.axaml create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/SyncronisationConflicts/ConflictResolutionView.axaml.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/SyncronisationConflicts/ConflictResolutionViewModel.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/SyncronisationConflicts/ConflictResolver.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/SyncronisationConflicts/IConflictResolver.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Themes/ColourfulTheme.axaml create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Themes/ProfessionalTheme.axaml create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/Themes/TerminalTheme.axaml create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/ViewModelExtensions.cs create mode 100644 apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client/appsettings.json create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/AStar.Dev.Admin.Api.Client.Sdk.csproj create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/AStar.png create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/AdminApi/AdminApiClient.cs create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/AdminApi/AdminApiConfiguration.cs create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/Constants.cs create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/LICENSE create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/Models/ModelToIgnore.cs create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/Models/ScrapeDirectories.cs create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/Models/SearchCategory.cs create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/Models/SearchConfiguration.cs create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/Models/SiteConfiguration.cs create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/Models/TagToIgnore.cs create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/Readme.md create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/ServiceCollectionExtensions.cs create mode 100644 libs/AStar.Dev.Admin.Api.Client.Sdk/astar.ico create mode 100644 libs/AStar.Dev.Api.Client.Sdk.Shared/AStar.Dev.Api.Client.Sdk.Shared.csproj create mode 100644 libs/AStar.Dev.Api.Client.Sdk.Shared/AStar.png create mode 100644 libs/AStar.Dev.Api.Client.Sdk.Shared/AddApiHttpClient.cs create mode 100644 libs/AStar.Dev.Api.Client.Sdk.Shared/IApiConfiguration.cs create mode 100644 libs/AStar.Dev.Api.Client.Sdk.Shared/LICENSE create mode 100644 libs/AStar.Dev.Api.Client.Sdk.Shared/Readme.md create mode 100644 libs/AStar.Dev.Api.Client.Sdk.Shared/astar.ico create mode 100644 libs/AStar.Dev.Api.HealthChecks/AStar.Dev.Api.HealthChecks.csproj create mode 100644 libs/AStar.Dev.Api.HealthChecks/AStar.png create mode 100644 libs/AStar.Dev.Api.HealthChecks/HealthCheckExtensions.cs create mode 100644 libs/AStar.Dev.Api.HealthChecks/HealthStatusResponse.cs create mode 100644 libs/AStar.Dev.Api.HealthChecks/IApiClient.cs create mode 100644 libs/AStar.Dev.Api.HealthChecks/LICENSE create mode 100644 libs/AStar.Dev.Api.HealthChecks/Program.cs create mode 100644 libs/AStar.Dev.Api.HealthChecks/Properties/launchSettings.json create mode 100644 libs/AStar.Dev.Api.HealthChecks/Readme.md create mode 100644 libs/AStar.Dev.Api.HealthChecks/astar.ico create mode 100644 libs/AStar.Dev.Api.Usage.Sdk/AStar.Dev.Api.Usage.Sdk.csproj create mode 100644 libs/AStar.Dev.Api.Usage.Sdk/AStar.png create mode 100644 libs/AStar.Dev.Api.Usage.Sdk/ApiUsageConfiguration.cs create mode 100644 libs/AStar.Dev.Api.Usage.Sdk/ApiUsageEvent.cs create mode 100644 libs/AStar.Dev.Api.Usage.Sdk/LICENSE create mode 100644 libs/AStar.Dev.Api.Usage.Sdk/Metrics/IEndpointName.cs create mode 100644 libs/AStar.Dev.Api.Usage.Sdk/Metrics/UsageMetricHandler.cs create mode 100644 libs/AStar.Dev.Api.Usage.Sdk/Metrics/UsageMetricHandlerExtensions.cs create mode 100644 libs/AStar.Dev.Api.Usage.Sdk/Readme.md create mode 100644 libs/AStar.Dev.Api.Usage.Sdk/Send.cs create mode 100644 libs/AStar.Dev.Api.Usage.Sdk/ServiceCollectionExtensions.cs create mode 100644 libs/AStar.Dev.Api.Usage.Sdk/astar.ico create mode 100644 libs/AStar.Dev.AspNet.Extensions/AStar.Dev.AspNet.Extensions.csproj create mode 100644 libs/AStar.Dev.AspNet.Extensions/AStar.png create mode 100644 libs/AStar.Dev.AspNet.Extensions/ApiConfiguration.cs create mode 100644 libs/AStar.Dev.AspNet.Extensions/ConfigurationManagerExtensions/ConfigurationManagerExtensions.cs create mode 100644 libs/AStar.Dev.AspNet.Extensions/Handlers/GlobalExceptionHandler.cs create mode 100644 libs/AStar.Dev.AspNet.Extensions/LICENSE create mode 100644 libs/AStar.Dev.AspNet.Extensions/PipelineExtensions/PipelineExtensions.cs create mode 100644 libs/AStar.Dev.AspNet.Extensions/Readme.md create mode 100644 libs/AStar.Dev.AspNet.Extensions/RootEndpoint/RootEndpointConfiguration.cs create mode 100644 libs/AStar.Dev.AspNet.Extensions/ServiceCollectionExtensions/ServiceCollectionExtensions.cs create mode 100644 libs/AStar.Dev.AspNet.Extensions/WebApplicationBuilderExtensions/WebApplicationBuilderExtensions.cs create mode 100644 libs/AStar.Dev.AspNet.Extensions/astar.ico create mode 100644 libs/AStar.Dev.Auth.Extensions/AStar.Dev.Auth.Extensions.csproj create mode 100644 libs/AStar.Dev.Auth.Extensions/JwtEvents.cs create mode 100644 libs/AStar.Dev.Auth.Extensions/astar.ico create mode 100644 libs/AStar.Dev.Auth.Extensions/astar.png create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/AStar.Dev.Files.Api.Client.SDK.csproj create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/AStar.png create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Constants.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/FilesApi/FilesApiClient.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/FilesApi/FilesApiConfiguration.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/LICENSE create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Models/DirectoryChangeRequest.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Models/DuplicateGroup.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Models/Duplicates.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Models/ExcludedViewSettings.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Models/FileAccessDetail.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Models/FileDetail.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Models/FileDetailClassification.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Models/FileDetailClassifications.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Models/FileDimensionsWithSize.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Models/GetDuplicatesCountQueryResponse.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Models/SearchParameters.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Models/SearchType.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Models/SortOrder.cs create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/Readme.md create mode 100644 libs/AStar.Dev.Files.Api.Client.SDK/astar.ico create mode 100644 libs/AStar.Dev.Fluent.Assignments/AStar.Dev.Fluent.Assignments.csproj create mode 100644 libs/AStar.Dev.Fluent.Assignments/AStar.png create mode 100644 libs/AStar.Dev.Fluent.Assignments/FluentAssignmentsExtensions.cs create mode 100644 libs/AStar.Dev.Fluent.Assignments/LICENSE create mode 100644 libs/AStar.Dev.Fluent.Assignments/Readme.md create mode 100644 libs/AStar.Dev.Fluent.Assignments/astar.ico create mode 100644 libs/AStar.Dev.Functional.Extensions/AStar.Dev.Functional.Extensions.Blog.md create mode 100644 libs/AStar.Dev.Functional.Extensions/AStar.Dev.Functional.Extensions.csproj create mode 100644 libs/AStar.Dev.Functional.Extensions/AStar.Dev.Functional.Extensions.xml create mode 100644 libs/AStar.Dev.Functional.Extensions/CollectionAndStatusExtensions.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/ConvenienceResultExtensions.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/EnumerableExtensions.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/ErrorResponse.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/LICENSE create mode 100644 libs/AStar.Dev.Functional.Extensions/Option.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/OptionExtensions.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/OptionLinqExtensions.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/Option{T}.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/Pattern.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/Readme.md create mode 100644 libs/AStar.Dev.Functional.Extensions/Result.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/ResultExtensions.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/Try.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/TryExtensions.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/Unit.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/UnreachableException.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/ViewModelResultExtensions.cs create mode 100644 libs/AStar.Dev.Functional.Extensions/astar.png create mode 100644 libs/AStar.Dev.Guard.Clauses/AStar.Dev.Guard.Clauses.csproj create mode 100644 libs/AStar.Dev.Guard.Clauses/AStar.png create mode 100644 libs/AStar.Dev.Guard.Clauses/GuardAgainst.cs create mode 100644 libs/AStar.Dev.Guard.Clauses/LICENSE create mode 100644 libs/AStar.Dev.Guard.Clauses/Readme.md create mode 100644 libs/AStar.Dev.Guard.Clauses/astar.ico create mode 100644 libs/AStar.Dev.Images.Api.Client.SDK/AStar.Dev.Images.Api.Client.SDK.csproj create mode 100644 libs/AStar.Dev.Images.Api.Client.SDK/AStar.png create mode 100644 libs/AStar.Dev.Images.Api.Client.SDK/Constants.cs create mode 100644 libs/AStar.Dev.Images.Api.Client.SDK/ImagesApi/ImagesApiClient.cs create mode 100644 libs/AStar.Dev.Images.Api.Client.SDK/ImagesApi/ImagesApiConfiguration.cs create mode 100644 libs/AStar.Dev.Images.Api.Client.SDK/LICENSE create mode 100644 libs/AStar.Dev.Images.Api.Client.SDK/Models/NotFound.cs create mode 100644 libs/AStar.Dev.Images.Api.Client.SDK/Readme.md create mode 100644 libs/AStar.Dev.Images.Api.Client.SDK/astar.ico create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/.config/dotnet-tools.json create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/AStar.Dev.Infrastructure.AdminDb.csproj create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/AStar.png create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/AdminContext.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/Configurations/ScrapeDirectoryConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/Configurations/SearchCategoryConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/Configurations/SearchConfigurationConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/Configurations/SiteConfigurationConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/Create-Db-User.sql create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/LICENSE create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/Migrations/20250326164249_InitialCreation.Designer.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/Migrations/20250326164249_InitialCreation.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/Migrations/AdminContextModelSnapshot.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/Models/ScrapeDirectory.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/Models/SearchCategory.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/Models/SearchConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/Models/SiteConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/Readme.md create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/SeedData/ScrapeDirectoryData.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/SeedData/SearchCategoryData.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/SeedData/SearchConfigurationData.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/SeedData/SiteConfigurationData.cs create mode 100644 libs/AStar.Dev.Infrastructure.AdminDb/astar.ico create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/AStar.Dev.Infrastructure.FilesDb.csproj create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/AStar.png create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Configurations/ComplexPropertyBuilderConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Configurations/DeletionStatusConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Configurations/DirectoryNameConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Configurations/EventConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Configurations/EventTypeConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Configurations/FileAccessDetailConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Configurations/FileClassificationConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Configurations/FileDetailConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Configurations/FileNameConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Configurations/FileNamePartConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Configurations/IComplexPropertyConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Configurations/ImageDetailConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Configurations/ModelToIgnoreConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Configurations/TagToIgnoreConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Constants.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Data/FilesContext.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Data/FilesContextExtensions.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/EnumerableExtensions.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/LICENSE create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Migrations/20250919204057_InitialCreation.Designer.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Migrations/20250919204057_InitialCreation.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Migrations/20250919211728_RestructureDeletionAndImageDetails.Designer.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Migrations/20250919211728_RestructureDeletionAndImageDetails.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Migrations/FilesContextModelSnapshot.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/DeletionStatus.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/DirectoryName.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/DuplicateDetail.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/Event.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/EventType.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/FileAccessDetail.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/FileClassification.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/FileDetail.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/FileHandle.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/FileId.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/FileName.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/FileNamePart.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/FileSize.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/FileSizeEqualityComparer.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/ImageDetail.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/ModelToIgnore.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/SearchType.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/SortOrder.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Models/TagToIgnore.cs create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/Readme.md create mode 100644 libs/AStar.Dev.Infrastructure.FilesDb/astar.ico create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/AStar.Dev.Infrastructure.UsageDb.csproj create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/AStar.png create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/Configurations/SiteConfigurationConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/Data/ApiUsageContext.cs create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/LICENSE create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/Migrations/20250311203727_InitialConfiguration.Designer.cs create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/Migrations/20250311203727_InitialConfiguration.cs create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/Migrations/20250314122139_UseDateTime.Designer.cs create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/Migrations/20250314122139_UseDateTime.cs create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/Migrations/20250314123039_ClusterOnTimestamp.Designer.cs create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/Migrations/20250314123039_ClusterOnTimestamp.cs create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/Migrations/20250327113904_AddHttpMethod.Designer.cs create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/Migrations/20250327113904_AddHttpMethod.cs create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/Migrations/ApiUsageContextModelSnapshot.cs create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/Readme.md create mode 100644 libs/AStar.Dev.Infrastructure.UsageDb/astar.ico create mode 100644 libs/AStar.Dev.Infrastructure/AStar.Dev.Infrastructure.csproj create mode 100644 libs/AStar.Dev.Infrastructure/AStar.png create mode 100644 libs/AStar.Dev.Infrastructure/Data/AStarDbContextOptions.cs create mode 100644 libs/AStar.Dev.Infrastructure/Data/AuditableEntity.cs create mode 100644 libs/AStar.Dev.Infrastructure/Data/ConnectionString.cs create mode 100644 libs/AStar.Dev.Infrastructure/LICENSE create mode 100644 libs/AStar.Dev.Infrastructure/Readme.md create mode 100644 libs/AStar.Dev.Infrastructure/astar.ico create mode 100644 libs/AStar.Dev.Logging.Extensions/AStar.Dev.Logging.Extensions.csproj create mode 100644 libs/AStar.Dev.Logging.Extensions/AStar.Dev.Logging.Extensions.xml create mode 100644 libs/AStar.Dev.Logging.Extensions/AStar.png create mode 100644 libs/AStar.Dev.Logging.Extensions/AStarEventIds.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/AStarLogger.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/CloudRoleNameTelemetryInitializer.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Configuration.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/EventIds/AStarEventIds.Application.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/EventIds/AStarEventIds.OneDriveSync.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/EventIds/AStarEventIds.Website.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/EventIds/AStarEventIds.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/ILoggerAstar.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/ITelemetryClient.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/LICENSE create mode 100644 libs/AStar.Dev.Logging.Extensions/LogExtensions.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/LoggingExtensions.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Messages/AStarLog.Application.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Messages/AStarLog.OneDriveSync.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Messages/AStarLog.Web.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Messages/AStarLog.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Models/ApplicationInsights.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Models/Args.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Models/Console.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Models/FormatterOptions.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Models/JsonWriterOptions.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Models/Logging.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Models/Loglevel.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Models/Minimumlevel.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Models/Override.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Models/Serilog.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Models/SerilogConfig.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Models/WriteTo.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Properties/AssemblyInfo.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/Readme.md create mode 100644 libs/AStar.Dev.Logging.Extensions/SerilogConfigure.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/SerilogExtensions.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/SerilogLogFileLocator.cs create mode 100644 libs/AStar.Dev.Logging.Extensions/astar-logging-settings.json create mode 100644 libs/AStar.Dev.Logging.Extensions/astar.ico create mode 100644 libs/AStar.Dev.Logging.Extensions/astar.png create mode 100644 libs/AStar.Dev.Minimal.Api.Extensions/AStar.Dev.Minimal.Api.Extensions.csproj create mode 100644 libs/AStar.Dev.Minimal.Api.Extensions/ApiVersion.cs create mode 100644 libs/AStar.Dev.Minimal.Api.Extensions/RouteHandlerBuilderExtensions.cs create mode 100644 libs/AStar.Dev.Minimal.Api.Extensions/astar.ico create mode 100644 libs/AStar.Dev.Source.Analyzers/AStar.Dev.Source.Analyzers.csproj create mode 100644 libs/AStar.Dev.Source.Analyzers/AnalyzerReleases.Shipped.md create mode 100644 libs/AStar.Dev.Source.Analyzers/AnalyzerReleases.Unshipped.md create mode 100644 libs/AStar.Dev.Source.Analyzers/AutoRegisterOptionsPartialAnalyzer.cs create mode 100644 libs/AStar.Dev.Source.Generators.Attributes/AStar.Dev.Source.Generators.Attributes.csproj create mode 100644 libs/AStar.Dev.Source.Generators.Attributes/AutoRegisterEndpointAttribute.cs create mode 100644 libs/AStar.Dev.Source.Generators.Attributes/AutoRegisterOptions.cs create mode 100644 libs/AStar.Dev.Source.Generators.Attributes/AutoRegisterServiceAttribute.cs create mode 100644 libs/AStar.Dev.Source.Generators.Attributes/ServiceLifetime.cs create mode 100644 libs/AStar.Dev.Source.Generators.Attributes/StrongIdAttribute.cs create mode 100644 libs/AStar.Dev.Source.Generators.Sample/AStar.Dev.Source.Generators.Sample.csproj create mode 100644 libs/AStar.Dev.Source.Generators.Sample/SampleEntities.cs create mode 100644 libs/AStar.Dev.Source.Generators/AStar.Dev.Source.Generators.csproj create mode 100644 libs/AStar.Dev.Source.Generators/AttributeConstants.cs create mode 100644 libs/AStar.Dev.Source.Generators/OptionsBindingGeneration/OptionBindingGenerator.cs create mode 100644 libs/AStar.Dev.Source.Generators/OptionsBindingGeneration/OptionsBindingCodeGenerator.cs create mode 100644 libs/AStar.Dev.Source.Generators/OptionsBindingGeneration/OptionsTypeInfo.cs create mode 100644 libs/AStar.Dev.Source.Generators/Properties/launchSettings.json create mode 100644 libs/AStar.Dev.Source.Generators/Readme.md create mode 100644 libs/AStar.Dev.Source.Generators/ServiceRegistrationGeneration/ServiceCollectionCodeGenerator.cs create mode 100644 libs/AStar.Dev.Source.Generators/ServiceRegistrationGeneration/ServiceModel.cs create mode 100644 libs/AStar.Dev.Source.Generators/ServiceRegistrationGeneration/ServiceRegistrationGenerator.cs create mode 100644 libs/AStar.Dev.Source.Generators/StrongIdCodeGeneration/StrongIdCodeGenerator.cs create mode 100644 libs/AStar.Dev.Source.Generators/StrongIdCodeGeneration/StrongIdGenerator.cs create mode 100644 libs/AStar.Dev.Source.Generators/StrongIdCodeGeneration/StrongIdModel.cs create mode 100644 libs/AStar.Dev.Source.Generators/StrongIdCodeGeneration/StrongIdModelExtensions.cs create mode 100644 libs/AStar.Dev.Source.Generators/astar.png create mode 100644 libs/AStar.Dev.Source.Generators/build/AStar.Dev.Source.Generators.props create mode 100644 libs/AStar.Dev.Technical.Debt.Reporting/AStar.Dev.Technical.Debt.Reporting.csproj create mode 100644 libs/AStar.Dev.Technical.Debt.Reporting/AStar.png create mode 100644 libs/AStar.Dev.Technical.Debt.Reporting/LICENSE create mode 100644 libs/AStar.Dev.Technical.Debt.Reporting/Readme.md create mode 100644 libs/AStar.Dev.Technical.Debt.Reporting/RefactorAttribute.cs create mode 100644 libs/AStar.Dev.Technical.Debt.Reporting/astar.ico create mode 100644 libs/AStar.Dev.Test.Helpers.EndToEnd/AStar.Dev.Test.Helpers.EndToEnd.csproj create mode 100644 libs/AStar.Dev.Test.Helpers.EndToEnd/AStar.png create mode 100644 libs/AStar.Dev.Test.Helpers.EndToEnd/LICENSE create mode 100644 libs/AStar.Dev.Test.Helpers.EndToEnd/Readme.md create mode 100644 libs/AStar.Dev.Test.Helpers.EndToEnd/astar.ico create mode 100644 libs/AStar.Dev.Test.Helpers.Integration/AStar.Dev.Test.Helpers.Integration.csproj create mode 100644 libs/AStar.Dev.Test.Helpers.Integration/AStar.png create mode 100644 libs/AStar.Dev.Test.Helpers.Integration/LICENSE create mode 100644 libs/AStar.Dev.Test.Helpers.Integration/Readme.md create mode 100644 libs/AStar.Dev.Test.Helpers.Integration/astar.ico create mode 100644 libs/AStar.Dev.Test.Helpers.Unit/AStar.Dev.Test.Helpers.Unit.csproj create mode 100644 libs/AStar.Dev.Test.Helpers.Unit/AStar.png create mode 100644 libs/AStar.Dev.Test.Helpers.Unit/LICENSE create mode 100644 libs/AStar.Dev.Test.Helpers.Unit/Readme.md create mode 100644 libs/AStar.Dev.Test.Helpers.Unit/astar.ico create mode 100644 libs/AStar.Dev.Test.Helpers/AStar.Dev.Test.Helpers.csproj create mode 100644 libs/AStar.Dev.Test.Helpers/AStar.png create mode 100644 libs/AStar.Dev.Test.Helpers/LICENSE create mode 100644 libs/AStar.Dev.Test.Helpers/Readme.md create mode 100644 libs/AStar.Dev.Test.Helpers/astar.ico create mode 100644 libs/AStar.Dev.Usage.Api.Client.SDK/AStar.Dev.Usage.Api.Client.SDK.csproj create mode 100644 libs/AStar.Dev.Usage.Api.Client.SDK/AStar.png create mode 100644 libs/AStar.Dev.Usage.Api.Client.SDK/Constants.cs create mode 100644 libs/AStar.Dev.Usage.Api.Client.SDK/LICENSE create mode 100644 libs/AStar.Dev.Usage.Api.Client.SDK/Readme.md create mode 100644 libs/AStar.Dev.Usage.Api.Client.SDK/UsageApi/ImagesApiConfiguration.cs create mode 100644 libs/AStar.Dev.Usage.Api.Client.SDK/UsageApi/UsageApiClient.cs create mode 100644 libs/AStar.Dev.Usage.Api.Client.SDK/astar.ico create mode 100644 libs/AStar.Dev.Utilities/AStar.Dev.Utilities.csproj create mode 100644 libs/AStar.Dev.Utilities/AStar.Dev.Utilities.xml create mode 100644 libs/AStar.Dev.Utilities/BLOG.md create mode 100644 libs/AStar.Dev.Utilities/Constants.cs create mode 100644 libs/AStar.Dev.Utilities/EncryptionExtensions.cs create mode 100644 libs/AStar.Dev.Utilities/EnumExtensions.cs create mode 100644 libs/AStar.Dev.Utilities/LICENSE create mode 100644 libs/AStar.Dev.Utilities/LinqExtensions.cs create mode 100644 libs/AStar.Dev.Utilities/ObjectExtensions.cs create mode 100644 libs/AStar.Dev.Utilities/Readme.md create mode 100644 libs/AStar.Dev.Utilities/RegexExtensions.cs create mode 100644 libs/AStar.Dev.Utilities/StringExtensions.cs create mode 100644 libs/AStar.Dev.Utilities/astar.ico create mode 100644 libs/AStar.Dev.Utilities/astar.png delete mode 100644 libs/ExampleLib/ExampleLib.csproj delete mode 100644 libs/ExampleLib/MyClass.cs delete mode 100644 libs/libs.sln create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/AStar.Dev.OneDrive.Client.Core.Tests.Unit.csproj create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/ConfigurationSettings/MsalConfigurationSettingsShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Dtos/DeltaPageShould.ContainTheExpectedProperties.approved.txt create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Dtos/DeltaPageShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Dtos/LocalFileInfoShould.ContainTheExpectedProperties.approved.txt create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Dtos/LocalFileInfoShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Dtos/UploadSessionInfoShould.ContainTheExpectedProperties.approved.txt create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Dtos/UploadSessionInfoShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Entities/DeltaTokenShould.ContainTheExpectedProperties.approved.txt create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Entities/DeltaTokenShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Entities/DriveItemRecord.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Entities/DriveItemRecordShould.ContainTheExpectedProperties.approved.txt create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Entities/LocalFileRecord.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Entities/LocalFileRecordShould.ContainTheExpectedProperties.approved.txt create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Entities/TransferLog.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Entities/TransferLogShould.ContainTheExpectedProperties.approved.txt create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Models/FileOperationLogShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Models/OneDriveFolderNodeShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Models/SyncConfigurationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Models/SyncConflictShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Models/SyncSessionLogShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Repositories/AccountRepositoryShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Repositories/DebugLogRepositoryShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Repositories/FileMetadataRepositoryShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/Repositories/SyncConfigurationRepositoryShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core.Tests.Unit/xunit.runner.json create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Integration/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Integration.csproj create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Integration/Data/AppDbContextShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Integration/Data/DbInitializerShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Integration/Data/Repositories/EfSyncRepositoryShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Integration/FileSystem/LocalFileSystemAdapterShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Integration/xunit.runner.json create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Unit/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Unit.csproj create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Unit/Data/ModelBuilderExtensionsShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Unit/Data/SqliteTypeConvertersShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Unit/DatabaseHealthCheckShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Unit/Graph/GraphPathHelpersShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Unit/GraphApiHealthCheckShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Unit/GraphClientWrapperShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure.Tests.Unit/xunit.runner.json create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Integration/AStar.Dev.OneDrive.Client.Services.Tests.Integration.csproj create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Integration/SyncEngineShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Integration/TransferServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Integration/xunit.runner.json create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/AStar.Dev.OneDrive.Client.Services.Tests.Unit.csproj create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/ChannelFactoryShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/ConfigurationSettings/AppPathHelperShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/ConfigurationSettings/ApplicationSettingsShould.ContainExpectedPropertiesWithExpectedValues.approved.txt create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/ConfigurationSettings/ApplicationSettingsShould.ContainTheExpectedPropertiesWithTheExpectedValues.approved.txt create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/ConfigurationSettings/ApplicationSettingsShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/ConfigurationSettings/AutoRegisteredOptionsShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/ConfigurationSettings/EntraIdSettingsShould.ContainExpectedPropertiesWithExpectedValues.approved.txt create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/ConfigurationSettings/EntraIdSettingsShould.ContainTheExpectedPropertiesWithTheExpectedValues.approved.txt create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/ConfigurationSettings/EntraIdSettingsShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/ConfigurationSettings/FileServicesShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/ConfigurationSettings/UiSettingsShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/ConfigurationSettings/UserPreferencesShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/ConfigurationSettings/WindowSettingsShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/DownloadQueueConsumerShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/DownloadQueueProducerShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/HealthCheckService/ApplicationHealthCheckServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/SyncEngineShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/SyncProgressReporterShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/SyncProgressShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/SyncSettingsShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/TransferServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/UploadQueueConsumerShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/UploadQueueProducerShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services.Tests.Unit/xunit.runner.json create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/AStar.Dev.OneDrive.Client.Tests.Unit.csproj create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/Common/AutoSaveServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/Data/ApplicationNameShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/Data/DriveIdShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/Data/ItemIdShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/Data/LocalDriveIdShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/Authentication/AuthServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/Integration/ServiceConfigurationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/Services/FileWatcherServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/Services/LocalFileScannerShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/Services/LogCleanupBackgroundServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/Services/OneDriveServices/FolderTreeServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/Services/RemoteChangeDetectorShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/Services/Sync/ConflictResolverShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/Services/SyncEngineFormatScanningFolderForDisplayShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/Services/SyncEngineShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/Services/SyncSelectionServicePersistenceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/Services/SyncSelectionServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/Services/WindowPreferencesServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/ViewModels/AccountManagementIntegrationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/ViewModels/AccountManagementViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/ViewModels/ConflictItemViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/ViewModels/ConflictResolutionViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/ViewModels/DebugLogViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/ViewModels/MainWindowViewModelIntegrationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/ViewModels/MainWindowViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/ViewModels/SyncProgressViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/ViewModels/SyncTreeViewModelPersistenceIntegrationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/ViewModels/SyncTreeViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/ViewModels/UpdateAccountDetailsViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/FromV3/ViewModels/ViewSyncHistoryViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/SettingsAndPreferences/SettingsAndPreferencesServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/Theme/ThemeMapperShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/Theme/ThemeSelectionHandlerShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/Theme/ThemeServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/UnitTest1.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/ViewModels/DashboardViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/ViewModels/MainWindowCoordinatorShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/ViewModels/MainWindowViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/ViewModels/MainWindowViewModelSyncStatusTargetShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/ViewModels/SyncCommandServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/ViewModels/WindowPositionValidatorShould.cs create mode 100644 tests/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Tests.Unit/xunit.runner.json create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Application.Tests/AStar.Dev.OneDrive.Sync.Client.Application.Tests.csproj create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Application.Tests/GlobalSuppressions.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Application.Tests/Services/SyncServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Domain.Tests/AStar.Dev.OneDrive.Sync.Client.Domain.Tests.csproj create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Domain.Tests/Entities/SyncFileShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Domain.Tests/GlobalSuppressions.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.csproj create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests/Data/SqlitePersistenceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests/GlobalSuppressions.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests/ServiceCollectionExtensionsShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/AStar.Dev.OneDrive.Sync.Client.UI.Tests.csproj create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/AccountManagement/AccountDialogViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/AccountManagement/AccountListViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/AssemblyInfo.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Common/ErrorHandlerShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Common/RelayCommandShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Composition/AppCompositionWiringShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Composition/CompositionRootShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/FolderTrees/FolderTreeViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/GlobalSuppressions.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Home/MainWindowViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Integration/SettingsIntegrationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Integration/SyncIntegrationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Layouts/ExplorerLayoutIntegrationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Layouts/LayoutEdgeCasesShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Layouts/LayoutSharedViewModelBindingShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Layouts/LayoutViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Layouts/LayoutsShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Layouts/SyncStatusDataContextBindingShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Localization/LocalizationManagerShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Logging/LoggingBootstrapShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/Settings/SettingsViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/SyncStatus/SyncStatusViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/ThemeManager/ThemeManagerShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AStar.Dev.OneDrive.Sync.Client.UI.Tests/ThemeManager/ThemeManagerTestCollection.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AstarOneDrive.Infrastructure.Tests/AstarOneDrive.Infrastructure.Tests.csproj create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AstarOneDrive.Infrastructure.Tests/Data/SqlitePersistenceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client-v2/AstarOneDrive.Infrastructure.Tests/GlobalSuppressions.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit.csproj create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Data/DatabaseConfigurationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Data/Entities/AccountEntityShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Data/Entities/DebugLogEntityShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Data/Entities/DriveItemEntitySelectionUpdateShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Data/Entities/DriveItemEntityShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Data/Entities/FileOperationLogEntityShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Data/Entities/SyncConflictEntityShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Data/Entities/SyncSessionLogEntityShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Data/Entities/WindowPreferencesEntityShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Models/FileOperationLogShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Models/HashedAccountIdShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Models/OneDriveFolderNodeShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Models/SyncConfigurationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Models/SyncConflictShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Models/SyncSessionLogShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/Models/SyncStateShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Core.Tests.Unit/xunit.runner.json create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit.csproj create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Repositories/AccountRepositoryShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Repositories/DebugLogRepositoryShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Repositories/DriveItemsRepositoryShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Repositories/FolderSelectionExtensionsTests.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/ConflictDetectionServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/DeletionSyncServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/DeltaProcessingServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/FileWatcherServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/LocalFileScannerShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/OneDriveServices/FileTransferServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/OneDriveServices/FolderTreeServiceIntegrationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/OneDriveServices/FolderTreeServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/RemoteChangeDetectorShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/SyncEngineFormatScanningFolderForDisplayShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/SyncEngineResultPatternShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/SyncSelectionServicePersistenceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/SyncSelectionServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/SyncStateCoordinatorShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/ThemeServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/ThemeStartupCoordinatorShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/WindowPreferencesServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/Services/WindowPreferencesServiceShould_ThemePreference.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Infrastructure.Tests.Unit/xunit.runner.json create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Integration/AStar.Dev.OneDrive.Sync.Client.Tests.Integration.csproj create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Integration/MainWindow/MainWindowViewModelIntegrationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Integration/Services/OneDriveServices/FolderTreeServiceIntegrationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Integration/ThemePersistence/ThemePersistenceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Integration/ThemeResourceDictionariesShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Integration/appsettings.json create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Integration/xunit.runner.json create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/AStar.Dev.OneDrive.Sync.Client.Tests.Unit.csproj create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Accounts/AccountManagementIntegrationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Accounts/AccountManagementViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Accounts/UpdateAccountDetailsViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Authentication/AuthServiceShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/ConfigurationSettings/AppPathHelperShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/ConfigurationSettings/ApplicationSettingsShould.HaveExpectedDefaultValues.approved.txt create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/ConfigurationSettings/ApplicationSettingsShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/ConfigurationSettings/EntraIdSettingsShould.HaveExpectedDefaultValues.approved.txt create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/ConfigurationSettings/EntraIdSettingsShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Converters/BoolToStatusColorConverterShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Converters/BoolToStatusTextConverterShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Converters/BooleanNegationConverterShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Converters/EnumToBooleanConverterShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Converters/InitialsConverterShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Converters/ThemePreferenceToDisplayNameConverterShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/DebugLogs/DebugLogViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/GlobalUsings.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Integration/ServiceConfigurationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/MainWindow/MainWindowViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/SampleTest.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Settings/SettingsViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Syncronisation/SyncProgressViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Syncronisation/SyncTreeViewModelPersistenceIntegrationShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Syncronisation/SyncTreeViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/Syncronisation/ViewSyncHistoryViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/SyncronisationConflicts/ConflictItemViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/SyncronisationConflicts/ConflictResolutionViewModelShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/SyncronisationConflicts/ConflictResolverShould.cs create mode 100644 tests/apps/astar-dev-onedrive-sync-client/AStar.Dev.OneDrive.Sync.Client.Tests.Unit/appsettings.json create mode 100644 tests/libs/AStar.Dev.Admin.Api.Client.Sdk.Tests.Unit/AStar.Dev.Admin.Api.Client.Sdk.Tests.Unit.csproj create mode 100644 tests/libs/AStar.Dev.Api.HealthChecks.Tests.Unit/AStar.Dev.Api.HealthChecks.Tests.Unit.csproj create mode 100644 tests/libs/AStar.Dev.AspNet.Extensions.Tests.Unit/AStar.Dev.AspNet.Extensions.Tests.Unit.csproj create mode 100644 tests/libs/AStar.Dev.AspNet.Extensions.Tests.Unit/ApiConfigurationTest.cs create mode 100644 tests/libs/AStar.Dev.AspNet.Extensions.Tests.Unit/ConfigurationManagerExtensions/ConfigurationManagerExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.AspNet.Extensions.Tests.Unit/Handlers/GlobalExceptionHandlerShould.cs create mode 100644 tests/libs/AStar.Dev.AspNet.Extensions.Tests.Unit/PipelineExtensions/PipelineExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.AspNet.Extensions.Tests.Unit/ServiceCollectionExtensions/ServiceCollectionExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.AspNet.Extensions.Tests.Unit/TestData/appsettings.json create mode 100644 tests/libs/AStar.Dev.AspNet.Extensions.Tests.Unit/WebApplicationBuilderExtensions/WebApplicationBuilderExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit.csproj create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/FilesApi/FilesApiClientShould.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/FilesApi/FilesApiConfigurationShould.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Helpers/FilesApiClientFactory.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/MockMessageHandlers/MockDeletionSuccessHttpMessageHandler.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/MockMessageHandlers/MockHttpRequestExceptionErrorHttpMessageHandler.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/MockMessageHandlers/MockInternalServerErrorHttpMessageHandler.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/MockMessageHandlers/MockSuccessHttpMessageHandler.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/MockMessageHandlers/MockSuccessMessageWithValue0HttpMessageHandler.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/DirectoryChangeRequestShould.ReturnTheExpectedToString.approved.txt create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/DirectoryChangeRequestShould.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/DuplicateGroupShould.ReturnTheExpectedToString.approved.txt create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/DuplicateGroupShould.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/ExcludedViewSettingsShould.ReturnTheExpectedToString.approved.txt create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/ExcludedViewSettingsShould.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/FileAccessDetailShould.ReturnTheExpectedToString.approved.txt create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/FileAccessDetailShould.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/FileDetailShould.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/FileDimensionsWithSizeShould.ReturnTheExpectedToString.approved.txt create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/FileDimensionsWithSizeShould.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/HealthStatusResponseShould.ReturnTheExpectedToString.approved.txt create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/HealthStatusResponseShould.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/SearchParametersShould.ReturnTheExpectedToQueryString.approved.txt create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/SearchParametersShould.ReturnTheExpectedToString.approved.txt create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/SearchParametersShould.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/SearchTypeShould.cs create mode 100644 tests/libs/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/SortOrderShould.cs create mode 100644 tests/libs/AStar.Dev.Fluent.Assignments.Tests.Unit/AStar.Dev.Fluent.Assignments.Tests.Unit.csproj create mode 100644 tests/libs/AStar.Dev.Fluent.Assignments.Tests.Unit/FluentAssignmentsExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/AStar.Dev.Functional.Extensions.Tests.Unit.csproj create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/CollectionAndStatusExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/ConvenienceResultExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/CustomTestException.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/EnumerableExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/ErrorResponseShould.ContainTheExpectedProperties.approved.txt create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/ErrorResponseShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/OptionLinqExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/OptionShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/OptionToResultTests.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/PatternTests.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/ResultExtensionBindShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/ResultExtensionMapShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/ResultExtensionMatchAsyncShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/ResultExtensionTapShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/ResultImplicitConversionShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/ResultLinqExtensionsTests.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/ResultShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/TryExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/TryShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/UnreachableExceptionShould.cs create mode 100644 tests/libs/AStar.Dev.Functional.Extensions.Tests.Unit/ViewModelResultExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/AStar.Dev.Logging.Extensions.Tests.Unit.csproj create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/AStarEventIdsShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/AStarLoggerTest.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/CloudRoleNameTelemetryInitializerTest.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/ConfigurationShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/ConfigurationTest.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Helpers/serilog.config create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/LogShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/LoggingExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/LoggingExtensionsTest.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/ApplicationInsightsShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/ArgsShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/ConsoleShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/ConsoleTest.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/FormatterOptionsShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/JsonWriterOptionsShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/LogLevelShould.ToString_ShouldListAllProperties.approved.txt create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/LogLevelShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/LogLevelTest.ToString_ShouldListAllProperties.approved.txt create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/LoggingShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/LoggingTest.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/MinimumLevelShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/OverrideShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/SerilogConfigShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/SerilogShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Models/WriteToShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/Properties/launchSettings.json create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/SerilogConfigShould.ContainTheExpectedProperties.approved.txt create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/SerilogConfigShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/SerilogConfigureShould.ConfigureTheLoggerWhenParametersAreValid.approved.txt create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/SerilogConfigureShould.Configure_ShouldConfigureLogger_WithValidParameters.approved.txt create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/SerilogConfigureShould.cs create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/SerilogConfigureTest.Configure_ShouldConfigureLogger_WithValidParameters.approved.txt create mode 100644 tests/libs/AStar.Dev.Logging.Extensions.Tests.Unit/SerilogExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Source.Analyzers.Tests.Unit/AStar.Dev.Source.Analyzers.Tests.Unit.csproj create mode 100644 tests/libs/AStar.Dev.Source.Analyzers.Tests.Unit/AutoRegisterOptionsPartialAnalyzerShould.cs create mode 100644 tests/libs/AStar.Dev.Source.Analyzers.Tests.Unit/Testing/AnalyzerVerifier.cs create mode 100644 tests/libs/AStar.Dev.Source.Generators.Attributes.Tests.Unit/AStar.Dev.Source.Generators.Attributes.Tests.Unit.csproj create mode 100644 tests/libs/AStar.Dev.Source.Generators.Attributes.Tests.Unit/AutoRegisterEndpointAttributeShould.cs create mode 100644 tests/libs/AStar.Dev.Source.Generators.Attributes.Tests.Unit/AutoRegisterOptionsAttributeShould.cs create mode 100644 tests/libs/AStar.Dev.Source.Generators.Attributes.Tests.Unit/AutoRegisterServiceAttributeShould.cs create mode 100644 tests/libs/AStar.Dev.Source.Generators.Attributes.Tests.Unit/StrongIdAttributeShould.cs create mode 100644 tests/libs/AStar.Dev.Source.Generators.Attributes.Tests.Unit/xunit.runner.json create mode 100644 tests/libs/AStar.Dev.Source.Generators.Tests.Unit/AStar.Dev.Source.Generators.Tests.Unit.csproj create mode 100644 tests/libs/AStar.Dev.Source.Generators.Tests.Unit/OptionsBindingGeneration/OptionsBindingGeneratorShould.cs create mode 100644 tests/libs/AStar.Dev.Source.Generators.Tests.Unit/ServiceRegistrationGeneration/ServiceRegistrationGeneratorShould.cs create mode 100644 tests/libs/AStar.Dev.Source.Generators.Tests.Unit/StrongIdCodeGeneration/StrongIdGeneratorShould.cs create mode 100644 tests/libs/AStar.Dev.Source.Generators.Tests.Unit/StrongIdCodeGeneration/TestStrongIds.cs create mode 100644 tests/libs/AStar.Dev.Source.Generators.Tests.Unit/Utilitites/CompilationHelpers.cs create mode 100644 tests/libs/AStar.Dev.Source.Generators.Tests.Unit/Utils/TestAdditionalFile.cs create mode 100644 tests/libs/AStar.Dev.Utilities.Tests.Unit/AStar.Dev.Utilities.Tests.Unit.csproj create mode 100644 tests/libs/AStar.Dev.Utilities.Tests.Unit/AnyClass.cs create mode 100644 tests/libs/AStar.Dev.Utilities.Tests.Unit/AnyEnum.cs create mode 100644 tests/libs/AStar.Dev.Utilities.Tests.Unit/ConstantsShould.ContainTheExpectedWebDeserialisationSettingsSetting.approved.txt create mode 100644 tests/libs/AStar.Dev.Utilities.Tests.Unit/ConstantsShould.cs create mode 100644 tests/libs/AStar.Dev.Utilities.Tests.Unit/EncryptionExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Utilities.Tests.Unit/EnumExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Utilities.Tests.Unit/LinqExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Utilities.Tests.Unit/ObjectExtensionsShould.ContainTheToJsonMethodWhichReturnsTheExpectedString.approved.txt create mode 100644 tests/libs/AStar.Dev.Utilities.Tests.Unit/ObjectExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Utilities.Tests.Unit/RegexExtensionsShould.cs create mode 100644 tests/libs/AStar.Dev.Utilities.Tests.Unit/StringExtensionsShould.cs rename tests/{ => web}/AStar.Dev.Web.Tests.Unit/AStar.Dev.Web.Tests.Unit.csproj (100%) rename tests/{ => web}/AStar.Dev.Web.Tests.Unit/UnitTest1.cs (100%) rename tests/{ => web}/AStar.Dev.Web.Tests.Unit/xunit.runner.json (100%) create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.EndToEnd/AStar.Dev.Admin.Api.Tests.EndToEnd.csproj create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Integration/AStar.Dev.Admin.Api.Tests.Integration.csproj create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Integration/BasicTests.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Integration/CustomWebApplicationFactory.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/AStar.Dev.Admin.Api.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/AddEndpointsShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/EndpointConstantsShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/Properties/launchSettings.json create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/ScrapeDirectories/GetAll/V1/GetAllScrapeDirectoriesQueryShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/ScrapeDirectories/GetAll/V1/GetAllScrapeDirectoriesQueryShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/ScrapeDirectories/GetAll/V1/GetAllScrapeDirectoriesRequestHandlerShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/ScrapeDirectories/GetAll/V1/GetScrapeDirectoriesResponseShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/ScrapeDirectories/GetAll/V1/GetScrapeDirectoriesResponseShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/ScrapeDirectories/Update/V1/UpdateScrapeDirectoriesCommandForDbShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/ScrapeDirectories/Update/V1/UpdateScrapeDirectoriesCommandForDbShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/ScrapeDirectories/Update/V1/UpdateScrapeDirectoriesCommandShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/ScrapeDirectories/Update/V1/UpdateScrapeDirectoriesCommandShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/ScrapeDirectories/Update/V1/UpdateScrapeDirectoriesRequestHandlerShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/ScrapeDirectories/Update/V1/UpdateScrapeDirectoriesResponseShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/ScrapeDirectories/Update/V1/UpdateScrapeDirectoriesResponseShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/GetAll/V1/GetAllSearchCategoriesRequestHandlerShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/GetAll/V1/GetAllSearchCategoriesssQueryTest.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/GetAll/V1/GetAllSiteConfigurationsQueryShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/GetAll/V1/GetSearchCategoriesResponseShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/GetAll/V1/GetSiteConfigurationResponseShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/GetById/V1/GetSearchCategoriesByIdQueryShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/GetById/V1/GetSearchCategoriesByIdQueryShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/GetById/V1/GetSearchCategoriesRequestHandlerShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/GetById/V1/GetSearchCategoriesResponseShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/GetById/V1/GetSearchCategoriesResponseShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/Update/V1/BACKUP DATABASE FilesDb.sql create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/Update/V1/UpdateSearchCategoriesRequestHandlerShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/Update/V1/UpdateSearchCategoriesResponseShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/Update/V1/UpdateSearchCategoriesResponseShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/Update/V1/UpdateSearchCategoryCommandForDbShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/Update/V1/UpdateSearchCategoryCommandForDbShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/Update/V1/UpdateSearchCategoryCommandShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchCategories/Update/V1/UpdateSearchCategoryCommandShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/GetAll/V1/GetAllSearchConfigurationRequestHandlerShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/GetAll/V1/GetAllSearchConfigurationResponseShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/GetAll/V1/GetAllSearchConfigurationResponseShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/GetAll/V1/GetAllSearchConfigurationsQueryShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/GetAll/V1/GetAllSearchConfigurationsQueryShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/GetBySlug/V1/GetSearchConfigurationQueryShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/GetBySlug/V1/GetSearchConfigurationQueryShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/GetBySlug/V1/GetSearchConfigurationRequestHandlerShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/GetBySlug/V1/GetSearchConfigurationResponseShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/GetBySlug/V1/GetSearchConfigurationResponseShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/Update/V1/UpdateSearchConfigurationCommandForDbShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/Update/V1/UpdateSearchConfigurationCommandForDbShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/Update/V1/UpdateSearchConfigurationCommandShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/Update/V1/UpdateSearchConfigurationCommandShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/Update/V1/UpdateSearchConfigurationRequestHandlerShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/Update/V1/UpdateSearchConfigurationResponseShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SearchConfiguration/Update/V1/UpdateSearchConfigurationResponseShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/GetAll/V1/GetAllSiteConfigurationsQueryShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/GetAll/V1/GetAllSiteConfigurationsQueryTest.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/GetAll/V1/GetAllSiteConfigurationsRequestHandlerShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/GetAll/V1/GetSiteConfigurationResponseShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/GetBySlug/V1/GetSiteConfigurationBySlugQueryTest.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/GetBySlug/V1/GetSiteConfigurationQueryShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/GetBySlug/V1/GetSiteConfigurationQueryTest.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/GetBySlug/V1/GetSiteConfigurationRequestHandlerShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/GetBySlug/V1/GetSiteConfigurationResponseShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/SiteConfigurationExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/Update/V1/UpdateSiteConfigurationCommandForDbShould.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/Update/V1/UpdateSiteConfigurationCommandForDbShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/Update/V1/UpdateSiteConfigurationCommandShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/Update/V1/UpdateSiteConfigurationCommandTest.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/Update/V1/UpdateSiteConfigurationRequestHandlerShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/Update/V1/UpdateSiteConfigurationResponseShould.cs create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api.Tests.Unit/SiteConfiguration/Update/V1/UpdateSiteConfigurationResponseTest.ContainTheExpectedValues.approved.txt create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Files.Api.Tests.EndToEnd/AStar.Dev.Files.Api.Tests.EndToEnd.csproj create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Files.Api.Tests.Integration/AStar.Dev.Files.Api.Tests.Integration.csproj create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Files.Api.Tests.Unit/AStar.Dev.Files.Api.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Images.Api.Tests.EndToEnd/AStar.Dev.Images.Api.Tests.EndToEnd.csproj create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Images.Api.Tests.Integration/AStar.Dev.Images.Api.Tests.Integration.csproj create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Images.Api.Tests.Unit/AStar.Dev.Images.Api.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/modules/apis/AStar.Dev.Images.Api.Tests.Unit/Models/DimensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Admin.Api.Client.Sdk.Tests.Unit/AStar.Dev.Admin.Api.Client.Sdk.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Api.HealthChecks.Tests.Unit/AStar.Dev.Api.HealthChecks.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.AspNet.Extensions.Tests.Unit/AStar.Dev.AspNet.Extensions.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.AspNet.Extensions.Tests.Unit/ApiConfigurationTest.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.AspNet.Extensions.Tests.Unit/ConfigurationManagerExtensions/ConfigurationManagerExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.AspNet.Extensions.Tests.Unit/Handlers/GlobalExceptionHandlerShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.AspNet.Extensions.Tests.Unit/PipelineExtensions/PipelineExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.AspNet.Extensions.Tests.Unit/ServiceCollectionExtensions/ServiceCollectionExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.AspNet.Extensions.Tests.Unit/TestData/appsettings.json create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.AspNet.Extensions.Tests.Unit/WebApplicationBuilderExtensions/WebApplicationBuilderExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/FilesApi/FilesApiClientShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/FilesApi/FilesApiConfigurationShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Helpers/FilesApiClientFactory.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/MockMessageHandlers/MockDeletionSuccessHttpMessageHandler.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/MockMessageHandlers/MockHttpRequestExceptionErrorHttpMessageHandler.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/MockMessageHandlers/MockInternalServerErrorHttpMessageHandler.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/MockMessageHandlers/MockSuccessHttpMessageHandler.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/MockMessageHandlers/MockSuccessMessageWithValue0HttpMessageHandler.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/DirectoryChangeRequestShould.ReturnTheExpectedToString.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/DirectoryChangeRequestShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/DuplicateGroupShould.ReturnTheExpectedToString.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/DuplicateGroupShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/ExcludedViewSettingsShould.ReturnTheExpectedToString.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/ExcludedViewSettingsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/FileAccessDetailShould.ReturnTheExpectedToString.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/FileAccessDetailShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/FileDetailShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/FileDimensionsWithSizeShould.ReturnTheExpectedToString.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/FileDimensionsWithSizeShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/HealthStatusResponseShould.ReturnTheExpectedToString.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/HealthStatusResponseShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/SearchParametersShould.ReturnTheExpectedToQueryString.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/SearchParametersShould.ReturnTheExpectedToString.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/SearchParametersShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/SearchTypeShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Files.Api.Client.Sdk.Tests.Unit/Models/SortOrderShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Fluent.Assignments.Tests.Unit/AStar.Dev.Fluent.Assignments.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Fluent.Assignments.Tests.Unit/FluentAssignmentsExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/AStar.Dev.Functional.Extensions.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/CollectionAndStatusExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/ConvenienceResultExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/CustomTestException.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/EnumerableExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/ErrorResponseShould.ContainTheExpectedProperties.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/ErrorResponseShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/OptionLinqExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/OptionShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/OptionToResultTests.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/PatternTests.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/ResultAsyncLinqExtensionsTests.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/ResultExtensionBindShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/ResultExtensionMapShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/ResultExtensionTapShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/ResultLinqExtensionsTests.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/ResultShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/TryExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/TryShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Functional.Extensions.Tests.Unit/ViewModelResultExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Guard.Clauses.Tests.Unit/AStar.Dev.Guard.Clauses.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Images.Api.Client.Sdk.Tests.Unit/AStar.Dev.Images.Api.Client.Sdk.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Images.Api.Client.Sdk.Tests.Unit/ImagesApiClientShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Images.Api.Client.Sdk.Tests.Unit/ImagesApiConfigurationShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Images.Api.Client.Sdk.Tests.Unit/MockMessageHandlers/MockDeletionSuccessHttpMessageHandler.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Images.Api.Client.Sdk.Tests.Unit/MockMessageHandlers/MockHttpRequestExceptionErrorHttpMessageHandler.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Images.Api.Client.Sdk.Tests.Unit/MockMessageHandlers/MockInternalServerErrorHttpMessageHandler1.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Images.Api.Client.Sdk.Tests.Unit/MockMessageHandlers/MockSuccessHttpMessageHandler.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.AdminDb.Tests.Unit/AStar.Dev.Infrastructure.AdminDb.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.AdminDb.Tests.Unit/UserConfigurationShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/AStar.Dev.Infrastructure.FilesDb.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/Data/FilesContextExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/EnumerableExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/Fixtures/FilesContextFixture.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/Fixtures/MockFilesContext.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/Models/FileDetailShould.ReturnTheExpectedToStringRepresentation.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/Models/FileDetailShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/Models/FileSizeEqualityComparerShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/Models/FileSizeShould.ReturnTheExpectedToStringOutput.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/Models/FileSizeShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/Models/TagToIgnoreCompletelyShould.ReturnTheExpectedToStringOutput.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/Models/TagToIgnoreCompletelyShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/Models/TagToIgnoreShould.ReturnTheExpectedToStringOutput.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/Models/TagToIgnoreShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.FilesDb.Tests.Unit/TestFiles/files.json create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Infrastructure.Tests.Unit/AStar.Dev.Infrastructure.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Logging.Extensions.Tests.Unit/AStar.Dev.Logging.Extensions.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Logging.Extensions.Tests.Unit/Helpers/serilog.config create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Logging.Extensions.Tests.Unit/LoggingExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Logging.Extensions.Tests.Unit/SerilogConfigShould.ContainTheExpectedProperties.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Logging.Extensions.Tests.Unit/SerilogConfigShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Technical.Debt.Reporting.Tests.Unit/AStar.Dev.Technical.Debt.Reporting.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Test.Helpers.EndToEnd.Tests.Unit/AStar.Dev.Test.Helpers.EndToEnd.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Test.Helpers.Integration.Tests.Unit/AStar.Dev.Test.Helpers.Integration.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Test.Helpers.Tests.Unit.Unit/AStar.Dev.Test.Helpers.Tests.Unit.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Test.Helpers.Tests.Unit/AStar.Dev.Test.Helpers.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Utilities.Tests.Unit/AStar.Dev.Utilities.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Utilities.Tests.Unit/AnyClass.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Utilities.Tests.Unit/AnyEnum.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Utilities.Tests.Unit/ConstantsShould.ContainTheExpectedWebDeserialisationSettingsSetting.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Utilities.Tests.Unit/ConstantsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Utilities.Tests.Unit/EncryptionExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Utilities.Tests.Unit/EnumExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Utilities.Tests.Unit/LinqExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Utilities.Tests.Unit/ObjectExtensionsShould.ContainTheToJsonMethodWhichReturnsTheExpectedString.approved.txt create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Utilities.Tests.Unit/ObjectExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Utilities.Tests.Unit/RegexExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/nuget-packages/AStar.Dev.Utilities.Tests.Unit/StringExtensionsShould.cs create mode 100644 tests/web/astar-dev-old/services/AStar.Dev.FilesDb.MigrationService.Tests.Unit/AStar.Dev.FilesDb.MigrationService.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/services/AStar.Dev.FilesDb.MigrationService.Tests.Unit/UnitTest1.cs create mode 100644 tests/web/astar-dev-old/services/AStar.Dev.FilesDb.MigrationService.Tests.Unit/xunit.runner.json create mode 100644 tests/web/astar-dev-old/services/AStar.Dev.Usage.Logger.Tests.Unit/AStar.Dev.Usage.Logger.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/services/AStar.Dev.Usage.Logger.Tests.Unit/UnitTest1.cs create mode 100644 tests/web/astar-dev-old/services/AStar.Dev.Usage.Logger.Tests.Unit/xunit.runner.json create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Integration/AStar.Dev.Web.Tests.Integration.csproj create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Integration/CustomWebApplicationFactory.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Integration/PlaceHolderTests.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Integration/WebTests.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/AStar.Dev.Web.Tests.Unit.csproj create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/Components/ApiStatusCheckShould.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/Components/Pages/Shared/SearchShould.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/MockApiClient.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/Pages/Admin/AddFilesToDatabaseShould.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/Pages/Admin/ApiUsageShould.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/Pages/Admin/AuthenticationCheckShould.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/Pages/Admin/FileClassificationsShould.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/Pages/Admin/ModelsToIgnoreShould.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/Pages/Admin/ScrapeDirectoriesShould.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/Pages/Admin/SiteConfigurationShould.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/Pages/Admin/TagsToIgnoreShould.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/Pages/HomeShould.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/Shared/NavMenuShould.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/StartupConfiguration/AddApiHttpClientShould.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/StartupConfiguration/ServicesShould.cs create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/StartupConfiguration/appSettings.json create mode 100644 tests/web/astar-dev-old/uis/AStar.Dev.Web.Tests.Unit/xunit.runner.json create mode 100644 web/astar-dev-old/aspire/AStar.Dev.Web.AppHost/AStar.Dev.Web.AppHost.csproj create mode 100644 web/astar-dev-old/aspire/AStar.Dev.Web.AppHost/AppHost.cs create mode 100644 web/astar-dev-old/aspire/AStar.Dev.Web.AppHost/Properties/launchSettings.json create mode 100644 web/astar-dev-old/aspire/AStar.Dev.Web.AppHost/appsettings.json create mode 100644 web/astar-dev-old/aspire/AStar.Dev.Web.Aspire.Common/AStar.Dev.Web.Aspire.Common.csproj create mode 100644 web/astar-dev-old/aspire/AStar.Dev.Web.Aspire.Common/AspireConstants.cs create mode 100644 web/astar-dev-old/aspire/AStar.Dev.Web.ServiceDefaults/AStar.Dev.Web.ServiceDefaults.csproj create mode 100644 web/astar-dev-old/aspire/AStar.Dev.Web.ServiceDefaults/Extensions.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/AStar.Dev.Admin.Api.csproj create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/AStar.png create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/AddEndpoints.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/Create-User.sql create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/Dockerfile create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/EndpointConstants.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/IAssemblyMarker.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/IEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/Program.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/Properties/launchSettings.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/ScrapeDirectories/GetAll/V1/GetAllScrapeDirectoriesEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/ScrapeDirectories/GetAll/V1/GetAllScrapeDirectoriesQuery.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/ScrapeDirectories/GetAll/V1/GetAllScrapeDirectoriesQueryResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/ScrapeDirectories/GetById/V1/GetScrapeDirectoriesByIdEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/ScrapeDirectories/GetById/V1/GetScrapeDirectoriesByIdQuery.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/ScrapeDirectories/GetById/V1/GetScrapeDirectoriesByIdQueryRequestHandler.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/ScrapeDirectories/GetById/V1/GetScrapeDirectoriesByIdResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/ScrapeDirectories/Update/V1/UpdateScrapeDirectoriesCommand.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/ScrapeDirectories/Update/V1/UpdateScrapeDirectoriesCommandForDb.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/ScrapeDirectories/Update/V1/UpdateScrapeDirectoriesEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/ScrapeDirectories/Update/V1/UpdateScrapeDirectoriesResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchCategories/GetAll/V1/GetAllSearchCategoriesEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchCategories/GetAll/V1/GetAllSearchCategoriesQuery.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchCategories/GetAll/V1/GetAllSearchCategoriesResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchCategories/GetById/V1/GetSearchCategoriesByIdEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchCategories/GetById/V1/GetSearchCategoriesByIdQuery.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchCategories/GetById/V1/GetSearchCategoriesByIdResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchCategories/Update/V1/UpdateSearchCategoryCommand.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchCategories/Update/V1/UpdateSearchCategoryCommandForDb.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchCategories/Update/V1/UpdateSearchCategoryEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchCategories/Update/V1/UpdateSearchCategoryResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchConfiguration/GetAll/V1/GetAllSearchConfigurationsEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchConfiguration/GetAll/V1/GetAllSearchConfigurationsQuery.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchConfiguration/GetAll/V1/GetAllSearchConfigurationsResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchConfiguration/GetBySlug/V1/GetSearchConfigurationBySlugEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchConfiguration/GetBySlug/V1/GetSearchConfigurationBySlugQuery.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchConfiguration/GetBySlug/V1/GetSearchConfigurationBySlugResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchConfiguration/Update/V1/UpdateSearchConfigurationCommand.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchConfiguration/Update/V1/UpdateSearchConfigurationCommandForDb.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchConfiguration/Update/V1/UpdateSearchConfigurationEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SearchConfiguration/Update/V1/UpdateSearchConfigurationResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SiteConfigurations/GetAll/V1/GetAllSiteConfigurationsEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SiteConfigurations/GetAll/V1/GetAllSiteConfigurationsQuery.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SiteConfigurations/GetAll/V1/GetAllSiteConfigurationsResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SiteConfigurations/GetBySlug/V1/GetSiteConfigurationBySlugEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SiteConfigurations/GetBySlug/V1/GetSiteConfigurationBySlugQuery.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SiteConfigurations/GetBySlug/V1/GetSiteConfigurationBySlugResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SiteConfigurations/SiteConfigurationExtensions.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SiteConfigurations/Update/V1/UpdateSiteConfigurationCommand.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SiteConfigurations/Update/V1/UpdateSiteConfigurationCommandForDb.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SiteConfigurations/Update/V1/UpdateSiteConfigurationEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/SiteConfigurations/Update/V1/UpdateSiteConfigurationResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/appsettings.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/astar-logging-settings.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/astar.ico create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Admin.Api/wwwroot/swagger-ui/SwaggerDark.css create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/AStar.Dev.Files.Api.csproj create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/AStar.png create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/AddEndpoints.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/AllFiles/V1/GetAllFilesEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/AllFiles/V1/GetAllFilesQuery.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/AllFiles/V1/GetAllFilesQueryResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Classifications/GetAll/V1/GetAllClassificationsEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Classifications/GetAll/V1/GetAllClassificationsResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Classifications/GetAllForFile/V1/GetAllClassificationsForFileEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Classifications/GetAllForFile/V1/GetAllClassificationsForFileQuery.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Classifications/GetAllForFile/V1/GetAllClassificationsForFileResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Config/SearchType.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Directories/GetAll/V1/GetAllDirectoriesEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Directories/GetAll/V1/GetAllDirectoriesQuery.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Directories/GetAll/V1/GetAllDirectoriesQueryResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Dockerfile create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Duplicates/Count/V1/FileDetailsQueryableExtensions.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Duplicates/Count/V1/GetDuplicatesCountEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Duplicates/Count/V1/GetDuplicatesCountQuery.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Duplicates/Count/V1/GetDuplicatesCountQueryResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Duplicates/Count/V1/GetDuplicatesCountQueryWithUser.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Duplicates/FileGrouping.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Duplicates/Get/V1/FileDetailsQueryableExtensions.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Duplicates/Get/V1/GetDuplicatesEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Duplicates/Get/V1/GetDuplicatesQuery.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Duplicates/Get/V1/GetDuplicatesQueryResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Duplicates/Get/V1/GetDuplicatesQueryWithUser.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/EndpointConstants.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/BaseSearchParameters.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/Count.CountSearchParameters.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/Count.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/CountDuplicates.CountDuplicatesSearchParameters.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/CountDuplicates.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/FileAccessDetail.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/FileDetail.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/FilesDatabaseUpdateEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/List.ListSearchParameters.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/List.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/ListDuplicates.DuplicatesResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/ListDuplicates.FileSizeDto.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/ListDuplicates.ListDuplicatesSearchParameters.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/ListDuplicates.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/MarkForHardDeletion.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/MarkForMoving.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/MarkForSoftDeletion.Request.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/MarkForSoftDeletion.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/UndoMarkForHardDeletion.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/UndoMarkForMoving.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/UndoMarkForSoftDeletion.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/Update.DirectoryChangeRequest.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Endpoints/Update.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/FileInfoDtoExtensions.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/HardDelete/V1/MarkFileForHardDeletionCommand.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/HardDelete/V1/MarkFileForHardDeletionCommandResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/HardDelete/V1/MarkFileForHardDeletionEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/HardDelete/V1/UnMarkFileForHardDeletionCommand.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/HardDelete/V1/UnMarkFileForHardDeletionCommandResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/HardDelete/V1/UnMarkFileForHardDeletionEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/IAssemblyMarker.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/IEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/MarkForMoving/V1/MarkFileForMovingCommand.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/MarkForMoving/V1/MarkFileForMovingCommandResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/MarkForMoving/V1/MarkFileForMovingEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/MarkForMoving/V1/UnMarkFileForMovingCommand.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/MarkForMoving/V1/UnMarkFileForMovingCommandResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/MarkForMoving/V1/UnMarkFileForMovingEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Models/FileAccessDetailDto.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Models/FileInfoDto.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Program.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/Properties/launchSettings.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/SoftDelete/V1/MarkFileForSoftDeletionCommand.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/SoftDelete/V1/MarkFileForSoftDeletionCommandResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/SoftDelete/V1/MarkFileForSoftDeletionEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/SoftDelete/V1/UnMarkFileForSoftDeletionCommand.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/SoftDelete/V1/UnMarkFileForSoftDeletionCommandResponse.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/SoftDelete/V1/UnMarkFileForSoftDeletionEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/StartupConfiguration/Services.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/appsettings.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/astar-logging-settings.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/astar.ico create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Api/wwwroot/swagger-ui/SwaggerDark.css create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Classifications.Api/AStar.Dev.Files.Classifications.Api.csproj create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Classifications.Api/AStar.Dev.Files.Classifications.Api.http create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Classifications.Api/FileClassification.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Classifications.Api/Program.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Classifications.Api/Properties/launchSettings.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Classifications.Api/appsettings.Development.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Files.Classifications.Api/appsettings.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/AStar.Dev.Images.Api.csproj create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/AStar.png create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/AddEndpoints.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/Dockerfile create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/EndpointConstants.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/Extensions/FileInfoDtoExtensions.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/Extensions/FileInfoExtensions.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/Extensions/StringExtensions.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/FileSizeEqualityComparer.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/IAssemblyMarker.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/IEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/Images/GetImageEndpoint.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/Models/Dimensions.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/Models/FileInfoDto.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/Models/FileSize.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/Program.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/Properties/launchSettings.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/Services/IImageService.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/Services/ImageService.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/StartupConfiguration/Services.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/appsettings.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/astar-logging-settings.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/astar.ico create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.Images.Api/wwwroot/swagger-ui/SwaggerDark.css create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.ToDo.Api/AStar.Dev.ToDo.Api.csproj create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.ToDo.Api/Controllers/ToDo.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.ToDo.Api/Controllers/ToDoListController.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.ToDo.Api/Dockerfile create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.ToDo.Api/Program.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.ToDo.Api/Properties/launchSettings.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.ToDo.Api/Properties/serviceDependencies.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.ToDo.Api/Properties/serviceDependencies.local.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.ToDo.Api/Startup.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.ToDo.Api/appsettings.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Dev.ToDo.Api/astar.ico create mode 100644 web/astar-dev-old/modules/apis/AStar.Web.ApiService/AStar.Web.ApiService.csproj create mode 100644 web/astar-dev-old/modules/apis/AStar.Web.ApiService/AStar.Web.ApiService.http create mode 100644 web/astar-dev-old/modules/apis/AStar.Web.ApiService/Program.cs create mode 100644 web/astar-dev-old/modules/apis/AStar.Web.ApiService/Properties/launchSettings.json create mode 100644 web/astar-dev-old/modules/apis/AStar.Web.ApiService/appsettings.json create mode 100644 web/astar-dev-old/services/AStar.Dev.FilesDb.MigrationService/AStar.Dev.FilesDb.MigrationService.csproj create mode 100644 web/astar-dev-old/services/AStar.Dev.FilesDb.MigrationService/Program.cs create mode 100644 web/astar-dev-old/services/AStar.Dev.FilesDb.MigrationService/Properties/launchSettings.json create mode 100644 web/astar-dev-old/services/AStar.Dev.FilesDb.MigrationService/Worker.cs create mode 100644 web/astar-dev-old/services/AStar.Dev.FilesDb.MigrationService/appsettings.json create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/AStar.Dev.Usage.Logger.csproj create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/AStar.Dev.Usage.Logger.http create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/AStar.png create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/AddEndpoints.cs create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/Dockerfile create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/EndpointConstants.cs create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/HalfSecondPeriodicTimer.cs create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/IAssemblyMarker.cs create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/IEndpoint.cs create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/IPeriodicTimer.cs create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/JsonSettings.cs create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/LICENSE create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/ProcessUsageEventsService.cs create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/Program.cs create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/Properties/launchSettings.json create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/Readme.md create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/Usage/GetAll/V1/ApiUsageEventDto.cs create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/Usage/GetAll/V1/GetAllApiUsageEventsEndpoint.cs create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/appsettings.json create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/astar-logging-settings.json create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/astar.ico create mode 100644 web/astar-dev-old/services/AStar.Dev.Usage.Logger/wwwroot/swagger-ui/SwaggerDark.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/.well-known/microsoft-identity-association.json create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/AStar.Dev.Web.csproj create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/App.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/App.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/LoginDisplay.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/LoginDisplay.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/MainLayout.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/MainLayout.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/MainLayout.razor.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/Menu/AdminMenuService.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/Menu/DirectoriesMenuService.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/Menu/FileMenuService.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/Menu/GamesMenuService.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/Menu/IMenuItemsService.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/Menu/ImagesMenuService.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/Menu/MenuItemsService.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/Menu/NuGetMenuService.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/NavMenu.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/NavMenu.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/NavMenu.razor.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/SettingsPanel.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Layout/SettingsPanel.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/AddFilesToDatabase.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/AddFilesToDatabase.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/ApiUsage.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/ApiUsage.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/AuthenticationCheck.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/AuthenticationCheck.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/FileClassifications.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/FileClassifications.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/ModelsToIgnore.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/ModelsToIgnore.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/ScrapeDirectories.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/ScrapeDirectories.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/SiteConfiguration.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/SiteConfiguration.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/TagsToIgnore.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Admin/TagsToIgnore.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Counter.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Counter.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Dashboard.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Dashboard.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Dashboard.razor.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Directories/MoveDirectories.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Directories/MoveDirectories.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Directories/RenameDirectories.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Directories/RenameDirectories.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Error.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Files/DuplicateFiles.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Files/DuplicateFiles.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Files/MoveFiles.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Files/MoveFiles.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Files/RenameFiles.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Files/RenameFiles.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Home.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Home.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Images/DuplicateImages.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Images/DuplicateImages.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Images/MoveImages.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Images/MoveImages.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Images/RandomImages.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Images/RandomImages.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Images/RenameImages.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Images/RenameImages.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Images/ScrapeImages.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Images/ScrapeImages.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/Games.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/Games.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/Halving.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/Halving.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/Matching.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/Matching.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/halving/Halving10.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/halving/Halving10.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/halving/Halving2.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/halving/Halving2.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/halving/Halving4.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/halving/Halving4.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/halving/Halving6.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/halving/Halving6.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/halving/Halving8.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/halving/Halving8.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/matching/MatchAnimals.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/matching/MatchAnimals.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/matching/MatchHouses.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/KidsGames/matching/MatchHouses.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/NuGetDocumentation/AStarDevFunctionalResults.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/NuGetDocumentation/AStarDevFunctionalResults.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/NuGetDocumentation/MarkdownView.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/NuGetDocumentation/MarkdownView.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/NuGetDocumentation/NuGetDocumentation.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/NuGetDocumentation/NuGetDocumentation.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Shared/Search.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Shared/Search.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Weather.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/Weather.razor.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Pages/_Imports.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/Routes.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Components/_Imports.razor create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/FilesApiOptions.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/IAssemblyMarker.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Models/FileClassification.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Models/SearchModel.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Models/SearchType.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Models/SortOrder.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Program.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Properties/launchSettings.json create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Services/FileClassificationsService.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/Services/IFileClassificationsService.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/WeatherApiClient.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/WebApplicationBuilderExtensions.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/WebApplicationExtensions.cs create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/app.http create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/appsettings.json create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/astar-logging-settings.json create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/astar-logo-large.png create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/astar.ico create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/astar.png create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/app.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/assets/astar-logo.png create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/assets/astar.ico create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/assets/astar.png create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/assets/fontawesome/LICENSE.txt create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/assets/fontawesome/css/all.min.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/assets/fontawesome/css/brands.min.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/assets/fontawesome/css/fontawesome.min.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/assets/fontawesome/webfonts/fa-brands-400.woff2 create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/assets/fontawesome/webfonts/fa-regular-400.woff2 create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/assets/fontawesome/webfonts/fa-solid-900.woff2 create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/assets/fontawesome/webfonts/fa-v4compatibility.woff2 create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/css/app.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/docs/FunctionalResult.md create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/favicon.ico create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/favicon.png create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/css/drag-n-drop.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/css/grid.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/css/index.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/css/menu.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/halving/css/halving.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/halving/images/burger.png create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/halving/images/cookie.png create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/halving/images/cupcake.png create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/halving/images/fries.png create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/halving/images/icecream.png create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/halving/images/monsters-at-the-table.png create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/halving/js/halving-10.js create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/halving/js/halving-2.js create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/halving/js/halving-4.js create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/halving/js/halving-6.js create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/halving/js/halving-8.js create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/images/stone.jpg create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/images/tree.jpg create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/js/drag-n-drop.js create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/js/index.js create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/js/touch-events.js create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/js/touch.js create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/match/css/grid-by-2.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/match/css/match.css create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/match/images/cottage.jpg create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/match/images/detached-house.jpg create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/match/images/fish.jpg create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/match/images/kittens.jpg create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/match/images/mouse.jpg create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/match/images/puppy.jpg create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/match/images/semi-detached.jpg create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/match/images/terraced-house.jpg create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/match/js/match-animals.js create mode 100644 web/astar-dev-old/uis/AStar.Dev.Web/wwwroot/games/match/js/match-houses.js diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/AStar.Dev.OneDrive.Client.Core.csproj b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/AStar.Dev.OneDrive.Client.Core.csproj new file mode 100644 index 0000000..4585cee --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/AStar.Dev.OneDrive.Client.Core.csproj @@ -0,0 +1,14 @@ + + + net10.0 + enable + enable + preview + + + + + + + + diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/ConfigurationSettings/MsalConfigurationSettings.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/ConfigurationSettings/MsalConfigurationSettings.cs new file mode 100644 index 0000000..653e3c1 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/ConfigurationSettings/MsalConfigurationSettings.cs @@ -0,0 +1,11 @@ + +namespace AStar.Dev.OneDrive.Client.Core.ConfigurationSettings; + +public record MsalConfigurationSettings +( + string ClientId, + string RedirectUri, + string GraphUri, + string[] Scopes, + string CachePrefix +); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Dtos/DeltaPage.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Dtos/DeltaPage.cs new file mode 100644 index 0000000..ebb0c1f --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Dtos/DeltaPage.cs @@ -0,0 +1,5 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Core.Dtos; + +public sealed record DeltaPage(IEnumerable Items, string? NextLink, string? DeltaLink); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Dtos/LocalFileInfo.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Dtos/LocalFileInfo.cs new file mode 100644 index 0000000..114e29d --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Dtos/LocalFileInfo.cs @@ -0,0 +1,3 @@ +namespace AStar.Dev.OneDrive.Client.Core.Dtos; + +public sealed record LocalFileInfo(string RelativePath, long Size, DateTimeOffset LastWriteUtc, string? Hash); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Dtos/UploadSessionInfo.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Dtos/UploadSessionInfo.cs new file mode 100644 index 0000000..6f83137 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Dtos/UploadSessionInfo.cs @@ -0,0 +1,3 @@ +namespace AStar.Dev.OneDrive.Client.Core.Dtos; + +public sealed record UploadSessionInfo(string UploadUrl, string SessionId, DateTimeOffset ExpiresAt); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Account.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Account.cs new file mode 100644 index 0000000..d3a929f --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Account.cs @@ -0,0 +1,19 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Entity for storing account information in the database. +/// +public sealed class Account +{ + public required string AccountId { get; set; } + public required string DisplayName { get; set; } + public required string LocalSyncPath { get; set; } + public bool IsAuthenticated { get; set; } + public DateTime? LastSyncUtc { get; set; } + public string? DeltaToken { get; set; } + public bool EnableDetailedSyncLogging { get; set; } + public bool EnableDebugLogging { get; set; } + public int MaxParallelUpDownloads { get; set; } + public int MaxItemsInBatch { get; set; } + public int? AutoSyncIntervalMinutes { get; set; } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/AccountEntity.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/AccountEntity.cs new file mode 100644 index 0000000..ac3c2a9 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/AccountEntity.cs @@ -0,0 +1,19 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Entity for storing account information in the database. +/// +public sealed class AccountEntity +{ + public required string AccountId { get; set; } + public required string DisplayName { get; set; } + public required string LocalSyncPath { get; set; } + public bool IsAuthenticated { get; set; } + public DateTime? LastSyncUtc { get; set; } + public string? DeltaToken { get; set; } + public bool EnableDetailedSyncLogging { get; set; } + public bool EnableDebugLogging { get; set; } + public int MaxParallelUpDownloads { get; set; } + public int MaxItemsInBatch { get; set; } + public int? AutoSyncIntervalMinutes { get; set; } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/AccountInfo.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/AccountInfo.cs new file mode 100644 index 0000000..fe96569 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/AccountInfo.cs @@ -0,0 +1,29 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Represents account information for a OneDrive account. +/// +/// Unique identifier for the account. +/// Display name of the account holder. +/// Local directory path for synchronization. +/// Indicates whether the account is currently authenticated. +/// Timestamp of the last successful synchronization. +/// Delta token for incremental synchronization. +/// Enables detailed logging of all file operations during sync. +/// Enables debug logging to database for historical review. +/// Maximum number of parallel upload/download operations (1-10). +/// Maximum number of items to process in a single batch (1-100). +/// Interval in minutes for automatic remote sync checks (null = disabled, 60-1440). +public sealed record AccountInfo( + string AccountId, + string DisplayName, + string LocalSyncPath, + bool IsAuthenticated, + DateTime? LastSyncUtc, + string? DeltaToken, + bool EnableDetailedSyncLogging, + bool EnableDebugLogging, + int MaxParallelUpDownloads, + int MaxItemsInBatch, + int? AutoSyncIntervalMinutes +); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DebugLog.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DebugLog.cs new file mode 100644 index 0000000..4bfdb09 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DebugLog.cs @@ -0,0 +1,15 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Entity for storing debug log entries in the database. +/// +public sealed class DebugLog +{ + public int Id { get; set; } + public required string AccountId { get; set; } + public DateTime TimestampUtc { get; set; } + public required string LogLevel { get; set; } + public required string Source { get; set; } + public required string Message { get; set; } + public string? Exception { get; set; } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DebugLogEntity.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DebugLogEntity.cs new file mode 100644 index 0000000..ca6bd49 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DebugLogEntity.cs @@ -0,0 +1,15 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Entity for storing debug log entries in the database. +/// +public sealed class DebugLogEntity +{ + public int Id { get; set; } + public required string AccountId { get; set; } + public DateTime TimestampUtc { get; set; } + public required string LogLevel { get; set; } + public required string Source { get; set; } + public required string Message { get; set; } + public string? Exception { get; set; } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DebugLogEntry.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DebugLogEntry.cs new file mode 100644 index 0000000..cb24927 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DebugLogEntry.cs @@ -0,0 +1,21 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Represents a debug log entry. +/// +/// Unique identifier for the log entry. +/// Account ID associated with the log entry. +/// When the log entry was created. +/// Severity level (Info, Error, Entry, Exit). +/// Source of the log (typically ClassName.MethodName). +/// Log message content. +/// Exception details if applicable. +public sealed record DebugLogEntry( + int Id, + string AccountId, + DateTime Timestamp, + string LogLevel, + string Source, + string Message, + string? Exception +); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DeltaToken.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DeltaToken.cs new file mode 100644 index 0000000..81606a5 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DeltaToken.cs @@ -0,0 +1,3 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +public sealed record DeltaToken(string AccountId, string Id, string Token, DateTimeOffset LastSyncedUtc); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DriveItemRecord.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DriveItemRecord.cs new file mode 100644 index 0000000..df70ccf --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/DriveItemRecord.cs @@ -0,0 +1,14 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +public sealed record DriveItemRecord( + string AccountId, + string Id, + string DriveItemId, + string RelativePath, + string? ETag, + string? CTag, + long Size, + DateTimeOffset LastModifiedUtc, + bool IsFolder, + bool IsDeleted +); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/SelectionState.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/SelectionState.cs new file mode 100644 index 0000000..870d069 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/SelectionState.cs @@ -0,0 +1,25 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities.Enums; + +/// +/// Represents the selection state of a folder in the sync tree. +/// +public enum SelectionState +{ + /// + /// The folder and all its children are not selected for sync. + /// + Unchecked = 0, + + /// + /// The folder and all its children are selected for sync. + /// + Checked = 1, + + /// + /// Some (but not all) of the folder's children are selected for sync. + /// + /// + /// This state is calculated based on child selections and cannot be directly set by the user. + /// + Indeterminate = 2 +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/SyncState.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/SyncState.cs new file mode 100644 index 0000000..a454111 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/SyncState.cs @@ -0,0 +1,12 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities.Enums; + +public enum SyncState +{ + Unknown, + PendingDownload, + Downloaded, + PendingUpload, + Uploaded, + Deleted, + Error +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/TransferStatus.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/TransferStatus.cs new file mode 100644 index 0000000..c307d05 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/TransferStatus.cs @@ -0,0 +1,3 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities.Enums; + +public enum TransferStatus { Pending, InProgress, Success, Failed } diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/TransferType.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/TransferType.cs new file mode 100644 index 0000000..2ed3c84 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/Enums/TransferType.cs @@ -0,0 +1,3 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities.Enums; + +public enum TransferType { Download, Upload, Delete } diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileChangeEvent.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileChangeEvent.cs new file mode 100644 index 0000000..7f5b596 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileChangeEvent.cs @@ -0,0 +1,19 @@ +using AStar.Dev.OneDrive.Client.Core.Models.Enums; + +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Represents a local file system change event detected by the file watcher. +/// +/// Account identifier. +/// Full local file system path. +/// Path relative to the sync root directory. +/// Type of file system change. +/// Timestamp when the change was detected. +public sealed record FileChangeEvent( + string AccountId, + string LocalPath, + string RelativePath, + FileChangeType ChangeType, + DateTime DetectedUtc +); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileMetadata.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileMetadata.cs new file mode 100644 index 0000000..59d6d6f --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileMetadata.cs @@ -0,0 +1,33 @@ +using AStar.Dev.OneDrive.Client.Core.Models.Enums; + +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Represents metadata for a synchronized file. +/// +/// Unique identifier (typically OneDrive item ID). +/// Account identifier. +/// File name. +/// OneDrive path to the file. +/// File size in bytes. +/// Last modification timestamp. +/// Local file system path. +/// OneDrive cTag for change tracking. +/// OneDrive eTag for versioning. +/// SHA256 hash of local file content. +/// Current synchronization status of the file. +/// Direction of last synchronization operation. +public sealed record FileMetadata( + string Id, + string AccountId, + string Name, + string Path, + long Size, + DateTime LastModifiedUtc, + string LocalPath, + string? CTag, + string? ETag, + string? LocalHash, + FileSyncStatus SyncStatus, + SyncDirection? LastSyncDirection +); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileMetadataEntity.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileMetadataEntity.cs new file mode 100644 index 0000000..4b218c6 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileMetadataEntity.cs @@ -0,0 +1,20 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Entity for tracking synced files and their metadata in the database. +/// +public sealed class FileMetadataEntity +{ + public required string Id { get; set; } + public required string AccountId { get; set; } + public required string Name { get; set; } + public required string Path { get; set; } + public long Size { get; set; } + public DateTime LastModifiedUtc { get; set; } + public required string LocalPath { get; set; } + public string? CTag { get; set; } + public string? ETag { get; set; } + public string? LocalHash { get; set; } + public int SyncStatus { get; set; } + public int? LastSyncDirection { get; set; } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileOperationLog.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileOperationLog.cs new file mode 100644 index 0000000..1fd72f0 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileOperationLog.cs @@ -0,0 +1,32 @@ +using AStar.Dev.OneDrive.Client.Core.Models.Enums; + +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Database entity for file operation log. +/// +public record FileOperationLog( + string Id, string SyncSessionId, string AccountId, DateTime Timestamp, FileOperation Operation, string FilePath, string LocalPath, + string? OneDriveId, long FileSize, string? LocalHash, string? RemoteHash, DateTime LastModifiedUtc, string Reason) +{ + public static FileOperationLog CreateSyncConflictLog(string syncSessionId, string accountId, string filePath, string localPath, + string oneDriveId, string? localHash, long fileSize, DateTime lastModifiedUtc, DateTime remoteFileLastModifiedUtc) => new( + Id: Guid.NewGuid().ToString(), SyncSessionId: syncSessionId, AccountId: accountId, Timestamp: DateTime.UtcNow, + Operation: FileOperation.ConflictDetected, FilePath: filePath, LocalPath: localPath, OneDriveId: oneDriveId, + FileSize: fileSize, LocalHash: localHash, RemoteHash: null, LastModifiedUtc: lastModifiedUtc, + Reason: $"Conflict: Both local and remote changed. Local modified: {lastModifiedUtc:yyyy-MM-dd HH:mm:ss}, Remote modified: {remoteFileLastModifiedUtc:yyyy-MM-dd HH:mm:ss}"); + + public static FileOperationLog CreateDownloadLog(string syncSessionId, string accountId, string filePath, string localPath, + string? oneDriveId, string? localHash, long fileSize, DateTime lastModifiedUtc, string reason) => new( + Id: Guid.NewGuid().ToString(), SyncSessionId: syncSessionId, AccountId: accountId, Timestamp: DateTime.UtcNow, + Operation: FileOperation.Download, FilePath: filePath, LocalPath: localPath, OneDriveId: oneDriveId, + FileSize: fileSize, LocalHash: localHash, RemoteHash: null, LastModifiedUtc: lastModifiedUtc, + Reason: reason); + + public static FileOperationLog CreateUploadLog(string syncSessionId, string accountId, string filePath, string localPath, + string? oneDriveId, string? localHash, long fileSize, DateTime lastModifiedUtc, string reason) => new( + Id: Guid.NewGuid().ToString(), SyncSessionId: syncSessionId, AccountId: accountId, Timestamp: DateTime.UtcNow, + Operation: FileOperation.Upload, FilePath: filePath, LocalPath: localPath, OneDriveId: oneDriveId, + FileSize: fileSize, LocalHash: localHash, RemoteHash: null, LastModifiedUtc: lastModifiedUtc, + Reason: reason); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileOperationLogEntity.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileOperationLogEntity.cs new file mode 100644 index 0000000..1ad67b8 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/FileOperationLogEntity.cs @@ -0,0 +1,21 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Database entity for file operation log. +/// +public class FileOperationLogEntity +{ + public string Id { get; set; } = string.Empty; + public string SyncSessionId { get; set; } = string.Empty; + public string AccountId { get; set; } = string.Empty; + public DateTime Timestamp { get; set; } + public int Operation { get; set; } + public string FilePath { get; set; } = string.Empty; + public string LocalPath { get; set; } = string.Empty; + public string? OneDriveId { get; set; } + public long FileSize { get; set; } + public string? LocalHash { get; set; } + public string? RemoteHash { get; set; } + public DateTime LastModifiedUtc { get; set; } + public string Reason { get; set; } = string.Empty; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/LocalFileRecord.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/LocalFileRecord.cs new file mode 100644 index 0000000..f638112 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/LocalFileRecord.cs @@ -0,0 +1,11 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +public sealed record LocalFileRecord( + string AccountId, + string Id, + string RelativePath, + string? Hash, + long Size, + DateTimeOffset LastWriteUtc, + Enums.SyncState SyncState +); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConfiguration.cs new file mode 100644 index 0000000..bbc7976 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConfiguration.cs @@ -0,0 +1,13 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Entity for storing sync configuration (folder selections) in the database. +/// +public sealed class SyncConfiguration(int id, string accountId, string folderPath, bool isSelected, DateTime lastModifiedUtc) +{ + public int Id { get; set; } = id; + public string AccountId { get; set; } = accountId; + public string FolderPath { get; set; } = folderPath; + public bool IsSelected { get; set; } = isSelected; + public DateTime LastModifiedUtc { get; set; } = lastModifiedUtc; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConfigurationEntity.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConfigurationEntity.cs new file mode 100644 index 0000000..7d8f673 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConfigurationEntity.cs @@ -0,0 +1,13 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Entity for storing sync configuration (folder selections) in the database. +/// +public sealed class SyncConfigurationEntity +{ + public int Id { get; set; } + public required string AccountId { get; set; } + public required string FolderPath { get; set; } + public bool IsSelected { get; set; } + public DateTime LastModifiedUtc { get; set; } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConflict.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConflict.cs new file mode 100644 index 0000000..d80ae70 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConflict.cs @@ -0,0 +1,38 @@ +using AStar.Dev.OneDrive.Client.Core.Models.Enums; + +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Entity representing a file synchronization conflict in the database. +/// +/// Unique identifier for the conflict. +/// Unique identifier for the account. +/// Path to the conflicted file. +/// Timestamp of the last local modification. +/// Timestamp of the last remote modification. +/// Size of the local file. +/// Size of the remote file. +/// Timestamp when the conflict was detected. +/// Resolution strategy for the conflict. +/// Indicates whether the conflict has been resolved. +public sealed record SyncConflict(string Id, string AccountId, string FilePath, DateTime LocalModifiedUtc, DateTime RemoteModifiedUtc, long LocalSize,long RemoteSize,DateTime DetectedUtc, ConflictResolutionStrategy ResolutionStrategy = ConflictResolutionStrategy.None, bool IsResolved=false) +{ + /// + /// Navigation property to the associated account. + /// + public Account? Account { get; set; } + + public static SyncConflict CreateUnresolvedConflict(string accountId, string filePath, DateTime localModifiedUtc, DateTime remoteModifiedUtc, long localSize, long remoteSize) => new + ( Guid.CreateVersion7().ToString(), + accountId, + filePath, + localModifiedUtc, + remoteModifiedUtc, + localSize, + remoteSize, + DateTime.UtcNow, + ConflictResolutionStrategy.None, + false + ); +} + diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConflictEntity.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConflictEntity.cs new file mode 100644 index 0000000..20c52cc --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncConflictEntity.cs @@ -0,0 +1,64 @@ +using AStar.Dev.OneDrive.Client.Core.Models.Enums; + +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Entity representing a file synchronization conflict in the database. +/// +public sealed class SyncConflictEntity +{ + /// + /// Gets or sets the unique identifier for the conflict. + /// + public string Id { get; set; } = string.Empty; + + /// + /// Gets or sets the account identifier. + /// + public string AccountId { get; set; } = string.Empty; + + /// + /// Gets or sets the path to the conflicted file. + /// + public string FilePath { get; set; } = string.Empty; + + /// + /// Gets or sets the local file modification timestamp. + /// + public DateTime LocalModifiedUtc { get; set; } + + /// + /// Gets or sets the OneDrive file modification timestamp. + /// + public DateTime RemoteModifiedUtc { get; set; } + + /// + /// Gets or sets the local file size in bytes. + /// + public long LocalSize { get; set; } + + /// + /// Gets or sets the OneDrive file size in bytes. + /// + public long RemoteSize { get; set; } + + /// + /// Gets or sets the timestamp when the conflict was detected. + /// + public DateTime DetectedUtc { get; set; } + + /// + /// Gets or sets the strategy chosen to resolve the conflict. + /// + public ConflictResolutionStrategy ResolutionStrategy { get; set; } + + /// + /// Gets or sets a value indicating whether the conflict has been resolved. + /// + public bool IsResolved { get; set; } + + /// + /// Navigation property to the associated account. + /// + public AccountEntity? Account { get; set; } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncSessionLog.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncSessionLog.cs new file mode 100644 index 0000000..363a640 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncSessionLog.cs @@ -0,0 +1,41 @@ +using AStar.Dev.OneDrive.Client.Core.Models.Enums; + +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Represents a summary of a sync session. +/// +/// Unique identifier for the sync session. +/// The account identifier. +/// When the sync started. +/// When the sync completed (null if still running). +/// Final status of the sync. +/// Number of files uploaded. +/// Number of files downloaded. +/// Number of files deleted. +/// Number of conflicts detected. +/// Total bytes transferred. +public record SyncSessionLog( + string Id, + string AccountId, + DateTime StartedUtc, + DateTime? CompletedUtc, + SyncStatus Status, + int FilesUploaded, + int FilesDownloaded, + int FilesDeleted, + int ConflictsDetected, + long TotalBytes) +{ + public static SyncSessionLog CreateInitialRunning(string accountId) => new( + Id: Guid.CreateVersion7().ToString(), + AccountId: accountId, + StartedUtc: DateTime.UtcNow, + CompletedUtc: null, + Status: SyncStatus.Running, + FilesUploaded: 0, + FilesDownloaded: 0, + FilesDeleted: 0, + ConflictsDetected: 0, + TotalBytes: 0L); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncSessionLogEntity.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncSessionLogEntity.cs new file mode 100644 index 0000000..cd4403c --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncSessionLogEntity.cs @@ -0,0 +1,18 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Database entity for sync session log. +/// +public class SyncSessionLogEntity +{ + public string Id { get; set; } = string.Empty; + public string AccountId { get; set; } = string.Empty; + public DateTime StartedUtc { get; set; } + public DateTime? CompletedUtc { get; set; } + public int Status { get; set; } + public int FilesUploaded { get; set; } + public int FilesDownloaded { get; set; } + public int FilesDeleted { get; set; } + public int ConflictsDetected { get; set; } + public long TotalBytes { get; set; } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncState.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncState.cs new file mode 100644 index 0000000..12eeeac --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/SyncState.cs @@ -0,0 +1,55 @@ +using AStar.Dev.OneDrive.Client.Core.Models.Enums; + +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Represents the current state of synchronization for an account. +/// +/// Account identifier. +/// Current synchronization status. +/// Total number of files to synchronize. +/// Number of files already synchronized. +/// Total bytes to synchronize. +/// Number of bytes already synchronized. +/// Number of files currently downloading. +/// Number of files currently uploading. +/// Number of files deleted during sync. +/// Number of conflicts detected. +/// Current transfer speed in MB/s. +/// Estimated seconds until completion. +/// The folder path currently being scanned (null when not scanning). +/// Timestamp of the last state update. +public sealed record SyncState( + string AccountId, + SyncStatus Status, + int TotalFiles, + int CompletedFiles, + long TotalBytes, + long CompletedBytes, + int FilesDownloading, + int FilesUploading, + int FilesDeleted, + int ConflictsDetected, + double MegabytesPerSecond, + int? EstimatedSecondsRemaining, + string? CurrentScanningFolder, + DateTime? LastUpdateUtc +) +{ + public static SyncState CreateInitial(string accountId) + => new( + AccountId: accountId, + Status: SyncStatus.Idle, + TotalFiles: 0, + CompletedFiles: 0, + TotalBytes: 0, + CompletedBytes: 0, + FilesDownloading: 0, + FilesUploading: 0, + FilesDeleted: 0, + ConflictsDetected: 0, + MegabytesPerSecond: 0, + EstimatedSecondsRemaining: null, + CurrentScanningFolder: null, + LastUpdateUtc: null); +}; diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/TransferLog.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/TransferLog.cs new file mode 100644 index 0000000..f99120c --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/TransferLog.cs @@ -0,0 +1,15 @@ +using AStar.Dev.OneDrive.Client.Core.Entities.Enums; + +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +public sealed record TransferLog( + string AccountId, + string Id, + TransferType Type, + string ItemId, + DateTimeOffset StartedUtc, + DateTimeOffset? CompletedUtc, + TransferStatus Status, + long? BytesTransferred, + string? Error +); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/WindowPreferences.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/WindowPreferences.cs new file mode 100644 index 0000000..a941bc5 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/WindowPreferences.cs @@ -0,0 +1,19 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Represents window position and size preferences. +/// +/// Unique identifier (should always be 1 for singleton). +/// Window X position (null if maximized or first run). +/// Window Y position (null if maximized or first run). +/// Window width in pixels. +/// Window height in pixels. +/// Indicates whether the window is maximized. +public sealed record WindowPreferences( + int Id, + double? X, + double? Y, + double Width, + double Height, + bool IsMaximized +); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/WindowPreferencesEntity.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/WindowPreferencesEntity.cs new file mode 100644 index 0000000..9e235cf --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Entities/WindowPreferencesEntity.cs @@ -0,0 +1,14 @@ +namespace AStar.Dev.OneDrive.Client.Core.Entities; + +/// +/// Entity for storing window position and size preferences in the database. +/// +public sealed class WindowPreferencesEntity +{ + public int Id { get; set; } + public double? X { get; set; } + public double? Y { get; set; } + public double Width { get; set; } + public double Height { get; set; } + public bool IsMaximized { get; set; } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/IAuthService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/IAuthService.cs new file mode 100644 index 0000000..d59cac8 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/IAuthService.cs @@ -0,0 +1,9 @@ +namespace AStar.Dev.OneDrive.Client.Core.Interfaces; + +public interface IAuthService +{ + Task SignInAsync(CancellationToken cancellationToken); + Task SignOutAsync(string accountId, CancellationToken cancellationToken); + Task GetAccessTokenAsync(string accountId, CancellationToken cancellationToken); + Task IsUserSignedInAsync(string accountId, CancellationToken cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/IFileSystemAdapter.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/IFileSystemAdapter.cs new file mode 100644 index 0000000..0f3afc7 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/IFileSystemAdapter.cs @@ -0,0 +1,14 @@ +using System.IO.Abstractions; +using AStar.Dev.OneDrive.Client.Core.Dtos; + +namespace AStar.Dev.OneDrive.Client.Core.Interfaces; + +public interface IFileSystemAdapter +{ + IFileInfo GetFileInfo(string relativePath); + Task WriteFileAsync(string relativePath, Stream content, CancellationToken cancellationToken); + Task OpenReadAsync(string relativePath, CancellationToken cancellationToken); + Task OpenWriteAsync(string relativePath, CancellationToken cancellationToken); + Task DeleteFileAsync(string relativePath, CancellationToken cancellationToken); + Task> EnumerateFilesAsync(CancellationToken cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/IGraphClient.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/IGraphClient.cs new file mode 100644 index 0000000..de4f70f --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/IGraphClient.cs @@ -0,0 +1,20 @@ +using AStar.Dev.OneDrive.Client.Core.Dtos; + +namespace AStar.Dev.OneDrive.Client.Core.Interfaces; + +public interface IGraphClient +{ + /// + /// If deltaOrNextLink is null, call /me/drive/root/delta to start full enumeration. + /// If it is a nextLink or deltaLink, GET that URL. + /// + Task GetDriveDeltaPageAsync(string accountId, string? deltaOrNextLink, CancellationToken cancellationToken); + + Task DownloadDriveItemContentAsync(string accountId, string driveItemId, CancellationToken cancellationToken); + + Task CreateUploadSessionAsync(string accountId, string parentPath, string fileName, CancellationToken cancellationToken); + + Task UploadChunkAsync(UploadSessionInfo session, Stream chunk, long rangeStart, long rangeEnd, CancellationToken cancellationToken); + + Task DeleteDriveItemAsync(string accountId, string driveItemId, CancellationToken cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/ISyncRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/ISyncRepository.cs new file mode 100644 index 0000000..59b262e --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Interfaces/ISyncRepository.cs @@ -0,0 +1,33 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using SyncState = AStar.Dev.OneDrive.Client.Core.Entities.Enums.SyncState; + +namespace AStar.Dev.OneDrive.Client.Core.Interfaces; + +public interface ISyncRepository +{ + Task GetDeltaTokenAsync(string accountId, CancellationToken cancellationToken); + Task SaveOrUpdateDeltaTokenAsync(string accountId, DeltaToken token, CancellationToken cancellationToken); + + /// + /// Apply a page of DriveItem metadata to the local DB. + /// Implementations should use a transaction and batch writes. + /// + Task ApplyDriveItemsAsync(string accountId, IEnumerable items, CancellationToken cancellationToken); + + Task> GetPendingDownloadsAsync(string accountId, int pageSize, int offset, CancellationToken cancellationToken); + Task MarkLocalFileStateAsync(string accountId, string driveItemId, SyncState state, CancellationToken cancellationToken); + Task AddOrUpdateLocalFileAsync(string accountId, LocalFileRecord file, CancellationToken cancellationToken); + Task> GetPendingUploadsAsync(string accountId, int limit, CancellationToken cancellationToken); + Task GetPendingDownloadCountAsync(string accountId, CancellationToken cancellationToken); + Task GetPendingUploadCountAsync(string accountId, CancellationToken cancellationToken); + Task GetLocalFileByPathAsync(string accountId, string relativePath, CancellationToken cancellationToken); + Task LogTransferAsync(string accountId, TransferLog log, CancellationToken cancellationToken); + /// + /// Gets a DriveItemRecord by its relative path, or null if not found. + /// + Task GetDriveItemByPathAsync(string accountId, string relativePath, CancellationToken cancellationToken); + /// + /// Gets all pending downloads (not just a page). + /// + Task> GetAllPendingDownloadsAsync(string accountId, CancellationToken cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/ConflictResolutionStrategy.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/ConflictResolutionStrategy.cs new file mode 100644 index 0000000..335a5f3 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/ConflictResolutionStrategy.cs @@ -0,0 +1,32 @@ +namespace AStar.Dev.OneDrive.Client.Core.Models.Enums; + +/// +/// Represents the strategy for resolving file synchronization conflicts. +/// +public enum ConflictResolutionStrategy +{ + /// + /// No action taken; conflict remains unresolved. + /// + None = 0, + + /// + /// Keep the local version and upload to OneDrive. + /// + KeepLocal = 1, + + /// + /// Keep the OneDrive version and download to local. + /// + KeepRemote = 2, + + /// + /// Keep both versions with different file names. + /// + KeepBoth = 3, + + /// + /// Keep the newer version based on modification time. + /// + KeepNewer = 4 +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/FileChangeType.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/FileChangeType.cs new file mode 100644 index 0000000..42853ab --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/FileChangeType.cs @@ -0,0 +1,27 @@ +namespace AStar.Dev.OneDrive.Client.Core.Models.Enums; + +/// +/// Types of file system changes that can be detected by the file watcher. +/// +public enum FileChangeType +{ + /// + /// A new file was created. + /// + Created, + + /// + /// An existing file was modified. + /// + Modified, + + /// + /// A file was deleted. + /// + Deleted, + + /// + /// A file was renamed. + /// + Renamed +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/FileOperation.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/FileOperation.cs new file mode 100644 index 0000000..5e5b0a4 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/FileOperation.cs @@ -0,0 +1,27 @@ +namespace AStar.Dev.OneDrive.Client.Core.Models.Enums; + +/// +/// Represents the type of file operation performed during sync. +/// +public enum FileOperation +{ + /// + /// File was uploaded to OneDrive. + /// + Upload = 0, + + /// + /// File was downloaded from OneDrive. + /// + Download = 1, + + /// + /// File was deleted locally or from OneDrive. + /// + Delete = 2, + + /// + /// Conflict was detected for this file. + /// + ConflictDetected = 3 +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/FileSyncStatus.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/FileSyncStatus.cs new file mode 100644 index 0000000..150cf61 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/FileSyncStatus.cs @@ -0,0 +1,47 @@ +namespace AStar.Dev.OneDrive.Client.Core.Models.Enums; + +/// +/// Represents the synchronization status of an individual file. +/// +public enum FileSyncStatus +{ + /// + /// File is in sync between local and OneDrive. + /// + Synced = 0, + + /// + /// File is pending upload to OneDrive. + /// + PendingUpload = 1, + + /// + /// File is pending download from OneDrive. + /// + PendingDownload = 2, + + /// + /// File is currently being uploaded. + /// + Uploading = 3, + + /// + /// File is currently being downloaded. + /// + Downloading = 4, + + /// + /// File has a conflict that needs resolution. + /// + Conflict = 5, + + /// + /// File synchronization failed. + /// + Failed = 6, + + /// + /// File has been deleted locally or remotely. + /// + Deleted = 7 +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/SyncDirection.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/SyncDirection.cs new file mode 100644 index 0000000..3143db7 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/SyncDirection.cs @@ -0,0 +1,17 @@ +namespace AStar.Dev.OneDrive.Client.Core.Models.Enums; + +/// +/// Represents the direction of file synchronization. +/// +public enum SyncDirection +{ + /// + /// File was uploaded to OneDrive. + /// + Upload = 0, + + /// + /// File was downloaded from OneDrive. + /// + Download = 1 +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/SyncStatus.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/SyncStatus.cs new file mode 100644 index 0000000..7fcc806 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/Enums/SyncStatus.cs @@ -0,0 +1,37 @@ +namespace AStar.Dev.OneDrive.Client.Core.Models.Enums; + +/// +/// Represents the current synchronization status. +/// +public enum SyncStatus +{ + /// + /// Synchronization is idle and not running. + /// + Idle = 0, + + /// + /// Synchronization is currently in progress. + /// + Running = 1, + + /// + /// Synchronization has been paused by the user. + /// + Paused = 2, + + /// + /// Synchronization completed successfully. + /// + Completed = 3, + + /// + /// Synchronization failed with errors. + /// + Failed = 4, + + /// + /// Synchronization is waiting to start. + /// + Queued = 5 +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/SyncConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/SyncConfiguration.cs new file mode 100644 index 0000000..42cf947 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Models/SyncConfiguration.cs @@ -0,0 +1,17 @@ +namespace AStar.Dev.OneDrive.Client.Core.Models; + +/// +/// Represents folder selection configuration for synchronization. +/// +/// Unique identifier for this configuration entry. +/// Account identifier this configuration belongs to. +/// OneDrive folder path. +/// Indicates whether this folder is selected for synchronization. +/// Timestamp when this configuration was last modified. +public sealed record SyncConfiguration( + int Id, + string AccountId, + string FolderPath, + bool IsSelected, + DateTime LastModifiedUtc +); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Utilities/Result.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Utilities/Result.cs new file mode 100644 index 0000000..9b75890 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Utilities/Result.cs @@ -0,0 +1,11 @@ +namespace AStar.Dev.OneDrive.Client.Core.Utilities; + +/// +/// Minimal Result type for simple success/failure flows. +/// Keep small and focused; expand in services if needed. +/// +public readonly record struct Result(bool IsSuccess, string? Error) +{ + public static Result Success => new(true, null); + public static Result Failure(string error) => new(false, error); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Utilities/SyncSettings.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Utilities/SyncSettings.cs new file mode 100644 index 0000000..dfe6036 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Core/Utilities/SyncSettings.cs @@ -0,0 +1,5 @@ +namespace AStar.Dev.OneDrive.Client.Core.Utilities; + +public sealed record SyncSettings(int ParallelDownloads = 4, int BatchSize = 50, ConflictPolicy ConflictPolicy = ConflictPolicy.LastWriteWins); + +public enum ConflictPolicy { LastWriteWins, KeepLocal, KeepRemote, Prompt } diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/AStar.Dev.OneDrive.Client.Infrastructure.csproj b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/AStar.Dev.OneDrive.Client.Infrastructure.csproj new file mode 100644 index 0000000..69864c0 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/AStar.Dev.OneDrive.Client.Infrastructure.csproj @@ -0,0 +1,33 @@ + + + net10.0 + enable + enable + preview + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Auth/MsalAuthService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Auth/MsalAuthService.cs new file mode 100644 index 0000000..a5fbb6d --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Auth/MsalAuthService.cs @@ -0,0 +1,89 @@ +using AStar.Dev.OneDrive.Client.Core.ConfigurationSettings; +using AStar.Dev.OneDrive.Client.Core.Interfaces; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensions.Msal; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Auth; + +public sealed class MsalAuthService(MsalConfigurationSettings msalConfigurationSettings) : IAuthService +{ + private readonly IPublicClientApplication _pca = PublicClientApplicationBuilder + .Create(msalConfigurationSettings.ClientId) + .WithAuthority(AzureCloudInstance.AzurePublic, "Common") + .WithRedirectUri(msalConfigurationSettings.RedirectUri) + .Build(); + private IAccount? _account; + private bool _initialized; + + public async Task SignInAsync(CancellationToken cancellationToken) + { + MsalCacheHelper cacheHelper = await MsalCacheHelper.CreateAsync( + new StorageCreationPropertiesBuilder( + $"{msalConfigurationSettings.CachePrefix}1.bin", + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)) + .WithUnprotectedFile() + .Build()); + cacheHelper.RegisterCache(_pca.UserTokenCache); + + _account = (await _pca.GetAccountsAsync()).FirstOrDefault(); + + try + { + AuthenticationResult result = await _pca + .AcquireTokenSilent(msalConfigurationSettings.Scopes, _account) + .ExecuteAsync(cancellationToken); + + _account = result.Account; + } + catch(MsalUiRequiredException) + { + AuthenticationResult result = await _pca + .AcquireTokenInteractive(msalConfigurationSettings.Scopes) + .ExecuteAsync(cancellationToken); + + _account = result.Account; + } + } + + public async Task SignOutAsync(string accountId, CancellationToken cancellationToken) + { + IEnumerable accounts = await _pca.GetAccountsAsync(); + + foreach(IAccount? account in accounts) await _pca.RemoveAsync(account); + + _account = null; + } + + public async Task GetAccessTokenAsync(string accountId, CancellationToken cancellationToken) + { + if(_account is null) + throw new InvalidOperationException("Not signed in"); + AuthenticationResult result = await _pca.AcquireTokenSilent(msalConfigurationSettings.Scopes, _account).ExecuteAsync(cancellationToken); + return result.AccessToken; + } + + public async Task IsUserSignedInAsync(string accountId, CancellationToken cancellationToken) + { + await InitializeAsync(); // 🔑 CRITICAL + + _account = (await _pca.GetAccountsAsync()).FirstOrDefault(); + return _account != null; + } + + private async Task InitializeAsync() + { + if(_initialized) + return; + + MsalCacheHelper cacheHelper = await MsalCacheHelper.CreateAsync( + new StorageCreationPropertiesBuilder( + $"{msalConfigurationSettings.CachePrefix}1.bin", + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)) + .WithUnprotectedFile() + .Build()); + + cacheHelper.RegisterCache(_pca.UserTokenCache); + + _initialized = true; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/AppDbContext.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/AppDbContext.cs new file mode 100644 index 0000000..1a68278 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/AppDbContext.cs @@ -0,0 +1,138 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Infrastructure.Data.Configurations; +using Microsoft.EntityFrameworkCore; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data; + +public class AppDbContext : DbContext +{ + /// + /// Gets or sets the accounts table. + /// + public DbSet Accounts { get; set; } = null!; + + /// + /// Gets or sets the sync configurations table. + /// + public DbSet SyncConfigurations { get; set; } = null!; + + /// + /// Gets or sets the file metadata table. + /// + public DbSet FileMetadata { get; set; } = null!; + + /// + /// Gets or sets the window preferences table. + /// + public DbSet WindowPreferences { get; set; } = null!; + + /// + /// Gets or sets the sync conflicts table. + /// + public DbSet SyncConflicts { get; set; } = null!; + + /// + /// Gets or sets the sync session logs table. + /// + public DbSet SyncSessionLogs { get; set; } = null!; + + /// + /// Gets or sets the file operation logs table. + /// + public DbSet FileOperationLogs { get; set; } = null!; + + /// + /// Gets or sets the debug logs table. + /// + public DbSet DebugLogs { get; set; } = null!; + + public AppDbContext(DbContextOptions opts) : base(opts) { } + + public AppDbContext() : base(new DbContextOptions()) { } + + public DbSet DriveItems { get; init; } = null!; + + public DbSet LocalFiles { get; init; } = null!; + + public DbSet DeltaTokens { get; init; } = null!; + + public DbSet TransferLogs { get; init; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + _ = modelBuilder.ApplyConfiguration(new AccountConfiguration()); + _ = modelBuilder.ApplyConfiguration(new AccountEntityConfiguration()); + _ = modelBuilder.ApplyConfiguration(new DriveItemRecordConfiguration()); + _ = modelBuilder.ApplyConfiguration(new LocalFileRecordConfiguration()); + _ = modelBuilder.ApplyConfiguration(new DeltaTokenConfiguration()); + _ = modelBuilder.ApplyConfiguration(new TransferLogConfiguration()); + _ = modelBuilder.ApplyConfiguration(new FileMetadataConfiguration()); + _ = modelBuilder.ApplyConfiguration(new SyncConfigurationConfiguration()); + _ = modelBuilder.ApplyConfiguration(new SyncConflictConfiguration()); + _ = modelBuilder.ApplyConfiguration(new WindowPreferencesConfiguration()); + + // Configure SyncConfigurationEntity + _ = modelBuilder.Entity(entity => + { + _ = entity.HasKey(e => e.Id); + _ = entity.Property(e => e.AccountId).IsRequired(); + _ = entity.Property(e => e.FolderPath).IsRequired(); + _ = entity.HasIndex(e => new { e.AccountId, e.FolderPath }); + + // Foreign key relationship with cascade delete + _ = entity.HasOne() + .WithMany() + .HasForeignKey(e => e.AccountId) + .OnDelete(DeleteBehavior.Cascade); + }); + + // Configure FileMetadataEntity + _ = modelBuilder.Entity(entity => + { + _ = entity.ToTable("FileMetadataEntity"); + _ = entity.HasKey(e => e.Id); + _ = entity.Property(e => e.Id).IsRequired(); + _ = entity.Property(e => e.AccountId).IsRequired(); + _ = entity.Property(e => e.Name).IsRequired(); + _ = entity.Property(e => e.Path).IsRequired(); + _ = entity.Property(e => e.LocalPath).IsRequired(); + + _ = entity.HasIndex(e => e.AccountId); + _ = entity.HasIndex(e => new { e.AccountId, e.Path }); + + // Foreign key relationship with cascade delete + _ = entity.HasOne() + .WithMany() + .HasForeignKey(e => e.AccountId) + .OnDelete(DeleteBehavior.Cascade); + }); + + // Configure WindowPreferencesEntity + _ = modelBuilder.Entity(entity => + { + _ = entity.ToTable("WindowPreferencesEntity"); + _ = entity.HasKey(e => e.Id); + _ = entity.Property(e => e.Width).HasDefaultValue(800); + _ = entity.Property(e => e.Height).HasDefaultValue(600); + }); + + // Configure SyncConflictEntity + _ = modelBuilder.Entity(entity => + { + _ = entity.HasKey(e => e.Id); + _ = entity.Property(e => e.Id).IsRequired(); + _ = entity.Property(e => e.AccountId).IsRequired(); + _ = entity.Property(e => e.FilePath).IsRequired(); + + _ = entity.HasIndex(e => e.AccountId); + _ = entity.HasIndex(e => new { e.AccountId, e.IsResolved }); + + // Foreign key relationship with cascade delete + _ = entity.HasOne(e => e.Account) + .WithMany() + .HasForeignKey(e => e.AccountId) + .OnDelete(DeleteBehavior.Cascade); + }); + modelBuilder.UseSqliteFriendlyConversions(); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/AccountConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/AccountConfiguration.cs new file mode 100644 index 0000000..2774349 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/AccountConfiguration.cs @@ -0,0 +1,17 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Configurations; + +public class AccountConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + _ = builder.HasKey(e => e.AccountId); + _ = builder.Property(e => e.AccountId).IsRequired(); + _ = builder.Property(e => e.DisplayName).IsRequired(); + _ = builder.Property(e => e.LocalSyncPath).IsRequired(); + _ = builder.HasIndex(e => e.LocalSyncPath).IsUnique(); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/AccountEntityConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/AccountEntityConfiguration.cs new file mode 100644 index 0000000..6dc6b80 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/AccountEntityConfiguration.cs @@ -0,0 +1,17 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Configurations; + +public class AccountEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + _ = builder.HasKey(e => e.AccountId); + _ = builder.Property(e => e.AccountId).IsRequired(); + _ = builder.Property(e => e.DisplayName).IsRequired(); + _ = builder.Property(e => e.LocalSyncPath).IsRequired(); + _ = builder.HasIndex(e => e.LocalSyncPath).IsUnique(); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/DeltaTokenConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/DeltaTokenConfiguration.cs new file mode 100644 index 0000000..0f12c42 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/DeltaTokenConfiguration.cs @@ -0,0 +1,21 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Configurations; + +public sealed class DeltaTokenConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + _ = builder.ToTable("DeltaTokens"); + _ = builder.HasKey(t => t.Id); + + _ = builder.Property(e => e.Token).HasColumnType("TEXT"); + + _ = builder.HasOne() + .WithMany() + .HasForeignKey(e => e.AccountId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/DriveItemRecordConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/DriveItemRecordConfiguration.cs new file mode 100644 index 0000000..9dcf9d3 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/DriveItemRecordConfiguration.cs @@ -0,0 +1,23 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Configurations; + +public sealed class DriveItemRecordConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + _ = builder.ToTable("DriveItems"); + _ = builder.HasKey(d => d.Id); + + _ = builder.Property("RelativePath").IsRequired(); + + _ = builder.HasIndex("DriveItemId"); + + _ = builder.HasOne() + .WithMany() + .HasForeignKey(e => e.AccountId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/FileMetadataConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/FileMetadataConfiguration.cs new file mode 100644 index 0000000..56b665c --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/FileMetadataConfiguration.cs @@ -0,0 +1,26 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Configurations; + +public class FileMetadataConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + _ = builder.HasKey(e => e.Id); + _ = builder.Property(e => e.Id).IsRequired(); + _ = builder.Property(e => e.AccountId).IsRequired(); + _ = builder.Property(e => e.Name).IsRequired(); + _ = builder.Property(e => e.Path).IsRequired(); + _ = builder.Property(e => e.LocalPath).IsRequired(); + + _ = builder.HasIndex(e => e.AccountId); + _ = builder.HasIndex(e => new { e.AccountId, e.Path }); + + _ = builder.HasOne() + .WithMany() + .HasForeignKey(e => e.AccountId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/LocalFileRecordConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/LocalFileRecordConfiguration.cs new file mode 100644 index 0000000..36fa729 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/LocalFileRecordConfiguration.cs @@ -0,0 +1,23 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Configurations; + +public sealed class LocalFileRecordConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + _ = builder.ToTable("LocalFiles"); + _ = builder.HasKey(l => l.Id); + + _ = builder.Property("RelativePath").IsRequired(); + + _ = builder.HasIndex("RelativePath"); + + _ = builder.HasOne() + .WithMany() + .HasForeignKey(e => e.AccountId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/SyncConfigurationConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/SyncConfigurationConfiguration.cs new file mode 100644 index 0000000..4aae81b --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/SyncConfigurationConfiguration.cs @@ -0,0 +1,21 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Configurations; + +public class SyncConfigurationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + _ = builder.HasKey(e => e.Id); + _ = builder.Property(e => e.AccountId).IsRequired(); + _ = builder.Property(e => e.FolderPath).IsRequired(); + _ = builder.HasIndex(e => new { e.AccountId, e.FolderPath }); + + _ = builder.HasOne() + .WithMany() + .HasForeignKey(e => e.AccountId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/SyncConflictConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/SyncConflictConfiguration.cs new file mode 100644 index 0000000..ee64bdf --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/SyncConflictConfiguration.cs @@ -0,0 +1,24 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Configurations; + +public class SyncConflictConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + _ = builder.HasKey(e => e.Id); + _ = builder.Property(e => e.Id).IsRequired(); + _ = builder.Property(e => e.AccountId).IsRequired(); + _ = builder.Property(e => e.FilePath).IsRequired(); + + _ = builder.HasIndex(e => e.AccountId); + _ = builder.HasIndex(e => new { e.AccountId, e.IsResolved }); + + _ = builder.HasOne(e => e.Account) + .WithMany() + .HasForeignKey(e => e.AccountId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/TransferLogConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/TransferLogConfiguration.cs new file mode 100644 index 0000000..63ba89f --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/TransferLogConfiguration.cs @@ -0,0 +1,23 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Configurations; + +public sealed class TransferLogConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + _ = builder.ToTable("TransferLogs"); + _ = builder.HasKey(t => t.Id); + + _ = builder.Property("Status").HasColumnType("TEXT"); + + _ = builder.Property("BytesTransferred").HasColumnType("INTEGER"); + + _ = builder.HasOne() + .WithMany() + .HasForeignKey(e => e.AccountId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/WindowPreferencesConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/WindowPreferencesConfiguration.cs new file mode 100644 index 0000000..48dab87 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Configurations/WindowPreferencesConfiguration.cs @@ -0,0 +1,15 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Configurations; + +public class WindowPreferencesConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + _ = builder.HasKey(e => e.Id); + _ = builder.Property(e => e.Width).HasDefaultValue(800); + _ = builder.Property(e => e.Height).HasDefaultValue(600); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/DbInitializer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/DbInitializer.cs new file mode 100644 index 0000000..8d82c22 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/DbInitializer.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data; + +public static class DbInitializer +{ + public static void EnsureDatabaseCreatedAndConfigured(AppDbContext db) + { + // Ensure DB created and enable WAL for better concurrency + db.Database.OpenConnection(); + _ = db.Database.ExecuteSqlRaw("PRAGMA journal_mode=WAL;"); + db.Database.Migrate(); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/ModelBuilderExtensions.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/ModelBuilderExtensions.cs new file mode 100644 index 0000000..c367f26 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/ModelBuilderExtensions.cs @@ -0,0 +1,72 @@ +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data; + +public static class ModelBuilderExtensions +{ + public static void UseSqliteFriendlyConversions(this ModelBuilder mb) + { + Type[] targetEntities = + [ + typeof(Core.Entities.DriveItemRecord), + typeof(Core.Entities.LocalFileRecord), + typeof(Core.Entities.DeltaToken), + typeof(Core.Entities.TransferLog) + ]; + + foreach(IMutableEntityType? et in mb.Model.GetEntityTypes().Where(e => targetEntities.Contains(e.ClrType))) + ApplyConversionsForEntity(mb, et); + } + + static void ApplyConversionsForEntity(ModelBuilder mb, IMutableEntityType et) + { + EntityTypeBuilder eb = mb.Entity(et.ClrType); + + foreach(PropertyInfo propInfo in et.ClrType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + Type pType = propInfo.PropertyType; + + // DateTimeOffset + if(pType == typeof(DateTimeOffset)) + _ = eb.Property(propInfo.Name).HasConversion(SqliteTypeConverters.DateTimeOffsetToTicks).HasColumnType("INTEGER").HasColumnName(propInfo.Name + "_Ticks"); + else if(Nullable.GetUnderlyingType(pType) == typeof(DateTimeOffset)) + _ = eb.Property(propInfo.Name).HasConversion(SqliteTypeConverters.NullableDateTimeOffsetToTicks).HasColumnType("INTEGER").HasColumnName(propInfo.Name + "_Ticks"); + + // TimeSpan + else if(pType == typeof(TimeSpan)) + _ = eb.Property(propInfo.Name).HasConversion(SqliteTypeConverters.TimeSpanToTicks).HasColumnType("INTEGER"); + else if(Nullable.GetUnderlyingType(pType) == typeof(TimeSpan)) + _ = eb.Property(propInfo.Name).HasConversion(SqliteTypeConverters.NullableTimeSpanToTicks).HasColumnType("INTEGER"); + + // Guid + else if(pType == typeof(Guid)) + _ = eb.Property(propInfo.Name).HasConversion(SqliteTypeConverters.GuidToBytes).HasColumnType("BLOB"); + else if(Nullable.GetUnderlyingType(pType) == typeof(Guid)) + _ = eb.Property(propInfo.Name).HasConversion(SqliteTypeConverters.NullableGuidToBytes).HasColumnType("BLOB"); + + // decimal + else if(pType == typeof(decimal)) + _ = eb.Property(propInfo.Name).HasConversion(SqliteTypeConverters.DecimalToCents).HasColumnType("INTEGER"); + else if(Nullable.GetUnderlyingType(pType) == typeof(decimal)) + _ = eb.Property(propInfo.Name).HasConversion(SqliteTypeConverters.NullableDecimalToCents).HasColumnType("INTEGER"); + + // enums -> integer + else if(pType.IsEnum) + _ = eb.Property(propInfo.Name).HasConversion().HasColumnType("INTEGER"); + else if(Nullable.GetUnderlyingType(pType)?.IsEnum == true) + { + Type? enumType = Nullable.GetUnderlyingType(pType); + if(enumType != null) + { + Type converterType = typeof(EnumToNumberConverter<,>).MakeGenericType(enumType, typeof(int)); + var converter = (ValueConverter)Activator.CreateInstance(converterType)!; + _ = eb.Property(propInfo.Name).HasConversion(converter).HasColumnType("INTEGER"); + } + } + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/AccountRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/AccountRepository.cs new file mode 100644 index 0000000..dad30a7 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/AccountRepository.cs @@ -0,0 +1,117 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +/// +/// Repository implementation for managing account data. +/// +public sealed class AccountRepository : IAccountRepository +{ + private readonly AppDbContext _context; + + public AccountRepository(AppDbContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + } + + /// + public async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + List entities = await _context.Accounts.ToListAsync(cancellationToken); + return [.. entities.Select(MapToModel)]; + } + + /// + public async Task GetByIdAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + AccountEntity? entity = await _context.Accounts.FindAsync([accountId], cancellationToken); + return entity is null ? null : MapToModel(entity); + } + + /// + public async Task AddAsync(AccountInfo account, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(account); + + AccountEntity entity = MapToEntity(account); + _ = _context.Accounts.Add(entity); + _ = await _context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task UpdateAsync(AccountInfo account, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(account); + + AccountEntity entity = await _context.Accounts.FindAsync([account.AccountId], cancellationToken) ?? throw new InvalidOperationException($"Account with ID '{account.AccountId}' not found."); + + entity.DisplayName = account.DisplayName; + entity.LocalSyncPath = account.LocalSyncPath; + entity.IsAuthenticated = account.IsAuthenticated; + entity.LastSyncUtc = account.LastSyncUtc; + entity.DeltaToken = account.DeltaToken; + entity.EnableDetailedSyncLogging = account.EnableDetailedSyncLogging; + entity.EnableDebugLogging = account.EnableDebugLogging; + entity.MaxParallelUpDownloads = account.MaxParallelUpDownloads; + entity.MaxItemsInBatch = account.MaxItemsInBatch; + entity.AutoSyncIntervalMinutes = account.AutoSyncIntervalMinutes; + + _ = await _context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task DeleteAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + AccountEntity? entity = await _context.Accounts.FindAsync([accountId], cancellationToken); + if(entity is not null) + { + _ = _context.Accounts.Remove(entity); + _ = await _context.SaveChangesAsync(cancellationToken); + } + } + + /// + public async Task ExistsAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + return await _context.Accounts.AnyAsync(a => a.AccountId == accountId, cancellationToken); + } + + private static AccountInfo MapToModel(AccountEntity entity) + => new( + entity.AccountId, + entity.DisplayName, + entity.LocalSyncPath, + entity.IsAuthenticated, + entity.LastSyncUtc, + entity.DeltaToken, + entity.EnableDetailedSyncLogging, + entity.EnableDebugLogging, + entity.MaxParallelUpDownloads, + entity.MaxItemsInBatch, + entity.AutoSyncIntervalMinutes + ); + + private static AccountEntity MapToEntity(AccountInfo model) + => new() + { + AccountId = model.AccountId, + DisplayName = model.DisplayName, + LocalSyncPath = model.LocalSyncPath, + IsAuthenticated = model.IsAuthenticated, + LastSyncUtc = model.LastSyncUtc, + DeltaToken = model.DeltaToken, + EnableDetailedSyncLogging = model.EnableDetailedSyncLogging, + EnableDebugLogging = model.EnableDebugLogging, + MaxParallelUpDownloads = model.MaxParallelUpDownloads, + MaxItemsInBatch = model.MaxItemsInBatch, + AutoSyncIntervalMinutes = model.AutoSyncIntervalMinutes + }; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/DebugLogRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/DebugLogRepository.cs new file mode 100644 index 0000000..8350bd6 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/DebugLogRepository.cs @@ -0,0 +1,92 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +/// +/// Repository implementation for accessing debug log entries. +/// +public sealed class DebugLogRepository : IDebugLogRepository +{ + private readonly AppDbContext _context; + + public DebugLogRepository(AppDbContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + } + + /// + public async Task> GetByAccountIdAsync(string accountId, int pageSize, int skip, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + List entities = await _context.DebugLogs + .Where(log => log.AccountId == accountId) + .OrderByDescending(log => log.TimestampUtc) + .Skip(skip) + .Take(pageSize) + .ToListAsync(cancellationToken); + + return [.. entities.Select(entity => new DebugLogEntry( + entity.Id, + entity.AccountId, + entity.TimestampUtc, + entity.LogLevel, + entity.Source, + entity.Message, + entity.Exception + ))]; + } + + /// + public async Task> GetByAccountIdAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + List entities = await _context.DebugLogs + .Where(log => log.AccountId == accountId) + .OrderByDescending(log => log.TimestampUtc) + .ToListAsync(cancellationToken); + + return [.. entities.Select(entity => new DebugLogEntry( + entity.Id, + entity.AccountId, + entity.TimestampUtc, + entity.LogLevel, + entity.Source, + entity.Message, + entity.Exception + ))]; + } + + /// + public async Task DeleteByAccountIdAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + List entities = await _context.DebugLogs + .Where(log => log.AccountId == accountId) + .ToListAsync(cancellationToken); + + _context.DebugLogs.RemoveRange(entities); + _ = await _context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task DeleteOlderThanAsync(DateTime olderThan, CancellationToken cancellationToken = default) + { + List entities = await _context.DebugLogs + .Where(log => log.TimestampUtc < olderThan) + .ToListAsync(cancellationToken); + + _context.DebugLogs.RemoveRange(entities); + _ = await _context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task GetDebugLogCountByAccountIdAsync(string accountId, CancellationToken cancellationToken = default) + => await _context.DebugLogs + .Where(log => log.AccountId == accountId) + .CountAsync(cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/EfSyncRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/EfSyncRepository.cs new file mode 100644 index 0000000..1ccda32 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/EfSyncRepository.cs @@ -0,0 +1,188 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Entities.Enums; +using AStar.Dev.OneDrive.Client.Core.Interfaces; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.Logging; +using SyncState = AStar.Dev.OneDrive.Client.Core.Entities.Enums.SyncState; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +public sealed class EfSyncRepository : ISyncRepository +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly ILogger _logger; + + public EfSyncRepository(IDbContextFactory dbContextFactory, ILogger logger) + { + _dbContextFactory = dbContextFactory; + _logger = logger; + } + + public async Task GetDeltaTokenAsync(string accountId, CancellationToken cancellationToken) + { + await using AppDbContext db = _dbContextFactory.CreateDbContext(); + + return await db.DeltaTokens.OrderByDescending(t => t.LastSyncedUtc).FirstOrDefaultAsync(cancellationToken); + } + + public async Task SaveOrUpdateDeltaTokenAsync(string accountId, DeltaToken token, CancellationToken cancellationToken) + { + await using AppDbContext db = _dbContextFactory.CreateDbContext(); + cancellationToken.ThrowIfCancellationRequested(); + DeltaToken? existing = await db.DeltaTokens.FindAsync([token.Id], cancellationToken); + if(existing is null) + _ = db.DeltaTokens.Add(token); + else + db.Entry(existing).CurrentValues.SetValues(token); + _ = await db.SaveChangesAsync(cancellationToken); + } + + public async Task ApplyDriveItemsAsync(string accountId, IEnumerable items, CancellationToken cancellationToken) + { + await using AppDbContext db = _dbContextFactory.CreateDbContext(); + await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync(cancellationToken); + foreach(DriveItemRecord item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + DriveItemRecord? existing = await db.DriveItems.FindAsync([item.Id], cancellationToken); + if(existing is null) + _ = db.DriveItems.Add(item); + else + db.Entry(existing).CurrentValues.SetValues(item); + } + + _ = await db.SaveChangesAsync(cancellationToken); + await tx.CommitAsync(cancellationToken); + } + + public async Task> GetPendingDownloadsAsync(string accountId, int pageSize, int offset, CancellationToken cancellationToken) + { + await using AppDbContext db = _dbContextFactory.CreateDbContext(); + cancellationToken.ThrowIfCancellationRequested(); + var totalDriveItems = await db.DriveItems.CountAsync(cancellationToken); + var totalFiles = await db.DriveItems.Where(d => !d.IsFolder && !d.IsDeleted).CountAsync(cancellationToken); + var downloadedFiles = await db.LocalFiles.Where(l => l.SyncState == SyncState.Downloaded || l.SyncState == SyncState.Uploaded).CountAsync(cancellationToken); + + _logger.LogDebug("Repository stats: {TotalItems} total items, {TotalFiles} files, {Downloaded} already downloaded", + totalDriveItems, totalFiles, downloadedFiles); + var stats = string.Format("Repository stats: {0} total items, {1} files, {2} already downloaded", + totalDriveItems, totalFiles, downloadedFiles); + var log = new TransferLog(accountId, Guid.CreateVersion7().ToString(), TransferType.Download, "Stats", DateTimeOffset.UtcNow, null, TransferStatus.InProgress, 0, stats); + + _ = db.TransferLogs.Add(log); + + IQueryable driveItems = db.DriveItems + .Where(d => !d.IsFolder && !d.IsDeleted) + .Where(d => !db.LocalFiles.Any(l => l.Id == d.Id && (l.SyncState == SyncState.Downloaded || l.SyncState == SyncState.Uploaded))) + .OrderBy(d => d.LastModifiedUtc) + .Skip(offset*pageSize) + .Take(pageSize); + + List results = await driveItems.ToListAsync(cancellationToken); + + _logger.LogDebug("GetPendingDownloadsAsync(pageSize={PageSize}, offset={Offset}): returning {Count} items", + pageSize, offset, results.Count); + + var stats2 = string.Format("GetPendingDownloadsAsync(pageSize={0}, offset={1}): returning {2} items", + pageSize, offset, results.Count); + var log2 = new TransferLog(accountId, Guid.CreateVersion7().ToString(), TransferType.Download, "Stats2", DateTimeOffset.UtcNow, null, TransferStatus.InProgress, 0, stats2); + + _ = db.TransferLogs.Add(log2); + _ = await db.SaveChangesAsync(cancellationToken); + + return results; + } + public async Task> GetAllPendingDownloadsAsync(string accountId, CancellationToken cancellationToken) + { + await using AppDbContext db = _dbContextFactory.CreateDbContext(); + cancellationToken.ThrowIfCancellationRequested(); + List driveItems = await db.DriveItems + .Where(d => !d.IsFolder && !d.IsDeleted) + .Where(d => !db.LocalFiles.Any(l => l.Id == d.Id && (l.SyncState == SyncState.Downloaded || l.SyncState == SyncState.Uploaded))) + .OrderBy(d => d.LastModifiedUtc) + .ToListAsync(cancellationToken); + return driveItems; + } + + public async Task GetPendingDownloadCountAsync(string accountId, CancellationToken cancellationToken) + { + await using AppDbContext db = _dbContextFactory.CreateDbContext(); + var count = await db.DriveItems + .Where(d => !d.IsFolder && !d.IsDeleted) + .Where(d => !db.LocalFiles.Any(l => l.Id == d.Id && (l.SyncState == SyncState.Downloaded || l.SyncState == SyncState.Uploaded))) + .CountAsync(cancellationToken); + + _logger.LogDebug("GetPendingDownloadCountAsync: {Count} pending downloads", count); + + return count; + } + + public async Task MarkLocalFileStateAsync(string accountId, string driveItemId, SyncState state, CancellationToken cancellationToken) + { + await using AppDbContext db = _dbContextFactory.CreateDbContext(); + cancellationToken.ThrowIfCancellationRequested(); + DriveItemRecord? drive = await db.DriveItems.FindAsync(accountId, driveItemId, cancellationToken); + if(drive is null) + return; + + LocalFileRecord? local = await db.LocalFiles.FindAsync(accountId, driveItemId, cancellationToken); + if(local is null) + _ = db.LocalFiles.Add(new LocalFileRecord(accountId, driveItemId, drive.RelativePath, null, drive.Size, drive.LastModifiedUtc, state)); + else + db.Entry(local).CurrentValues.SetValues(local with { SyncState = state, LastWriteUtc = drive.LastModifiedUtc, Size = drive.Size }); + + _ = await db.SaveChangesAsync(cancellationToken); + } + + public async Task AddOrUpdateLocalFileAsync(string accountId, LocalFileRecord file, CancellationToken cancellationToken) + { + await using AppDbContext db = _dbContextFactory.CreateDbContext(); + cancellationToken.ThrowIfCancellationRequested(); + LocalFileRecord? existing = await db.LocalFiles.FindAsync(accountId, file.Id, cancellationToken); + if(existing is null) + _ = db.LocalFiles.Add(file); + else + db.Entry(existing).CurrentValues.SetValues(file); + _ = await db.SaveChangesAsync(cancellationToken); + } + + public async Task> GetPendingUploadsAsync(string accountId, int limit, CancellationToken cancellationToken) + { + await using AppDbContext db = _dbContextFactory.CreateDbContext(); + return await db.LocalFiles.Where(l => l.SyncState == SyncState.PendingUpload).Take(limit).ToListAsync(cancellationToken); + } + + public async Task GetPendingUploadCountAsync(string accountId, CancellationToken cancellationToken) + { + await using AppDbContext db = _dbContextFactory.CreateDbContext(); + return await db.LocalFiles.Where(l => l.SyncState == SyncState.PendingUpload).CountAsync(cancellationToken); + } + + public async Task GetDriveItemByPathAsync(string accountId, string relativePath, CancellationToken cancellationToken) + { + await using AppDbContext db = _dbContextFactory.CreateDbContext(); + return await db.DriveItems.FirstOrDefaultAsync(d => d.RelativePath == relativePath && !d.IsDeleted, cancellationToken); + } + + public async Task GetLocalFileByPathAsync(string accountId, string relativePath, CancellationToken cancellationToken) + { + await using AppDbContext db = _dbContextFactory.CreateDbContext(); + return await db.LocalFiles.FirstOrDefaultAsync(l => l.RelativePath == relativePath, cancellationToken); + } + + public async Task LogTransferAsync(string accountId, TransferLog log, CancellationToken cancellationToken) + { + await using AppDbContext db = _dbContextFactory.CreateDbContext(); + TransferLog? existing = await db.TransferLogs.FindAsync( log.Id, cancellationToken); + if(existing is not null) + { + db.Entry(existing).CurrentValues.SetValues(log); + _ = await db.SaveChangesAsync(cancellationToken); + return; + } + + _ = db.TransferLogs.Add(log); + _ = await db.SaveChangesAsync(cancellationToken); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/FileMetadataRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/FileMetadataRepository.cs new file mode 100644 index 0000000..8f3aa88 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/FileMetadataRepository.cs @@ -0,0 +1,181 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Models.Enums; +using Microsoft.EntityFrameworkCore; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +/// +/// Repository implementation for managing file metadata. +/// +public sealed class FileMetadataRepository : IFileMetadataRepository +{ + private readonly AppDbContext _context; + + public FileMetadataRepository(AppDbContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + } + + /// + public async Task> GetByAccountIdAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + List entities = await _context.FileMetadata + .AsNoTracking() + .Where(fm => fm.AccountId == accountId) + .ToListAsync(cancellationToken); + + return [.. entities.Select(MapToModel)]; + } + + /// + public async Task GetByIdAsync(string id, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(id); + + FileMetadataEntity? entity = await _context.FileMetadata.FindAsync([id], cancellationToken); + return entity is null ? null : MapToModel(entity); + } + + /// + public async Task GetByPathAsync(string accountId, string path, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + ArgumentNullException.ThrowIfNull(path); + + FileMetadataEntity? entity = await _context.FileMetadata + .FirstOrDefaultAsync(fm => fm.AccountId == accountId && fm.Path == path, cancellationToken); + + return entity is null ? null : MapToModel(entity); + } + + /// + public async Task> GetByStatusAsync(string accountId, FileSyncStatus status, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + List entities = await _context.FileMetadata + .Where(fm => fm.AccountId == accountId && fm.SyncStatus == (int)status) + .ToListAsync(cancellationToken); + + return [.. entities.Select(MapToModel)]; + } + + /// + public async Task AddAsync(FileMetadata fileMetadata, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(fileMetadata); + + FileMetadataEntity entity = MapToEntity(fileMetadata); + if(_context.FileMetadata.Any(fm => fm.Id == entity.Id)) + { + await UpdateAsync(fileMetadata, cancellationToken); + return; + } + + _ = _context.FileMetadata.Add(entity); + _ = await _context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task UpdateAsync(FileMetadata fileMetadata, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(fileMetadata); + + FileMetadataEntity entity = await _context.FileMetadata.FindAsync([fileMetadata.Id], cancellationToken) ?? throw new InvalidOperationException($"File metadata with ID '{fileMetadata.Id}' not found."); + + entity.AccountId = fileMetadata.AccountId; + entity.Name = fileMetadata.Name; + entity.Path = fileMetadata.Path; + entity.Size = fileMetadata.Size; + entity.LastModifiedUtc = fileMetadata.LastModifiedUtc; + entity.LocalPath = fileMetadata.LocalPath; + entity.CTag = fileMetadata.CTag; + entity.ETag = fileMetadata.ETag; + entity.LocalHash = fileMetadata.LocalHash; + entity.SyncStatus = (int)fileMetadata.SyncStatus; + entity.LastSyncDirection = fileMetadata.LastSyncDirection.HasValue ? (int)fileMetadata.LastSyncDirection.Value : null; + + _ = await _context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task DeleteAsync(string id, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(id); + + FileMetadataEntity? entity = await _context.FileMetadata.FindAsync([id], cancellationToken); + if(entity is not null) + { + _ = _context.FileMetadata.Remove(entity); + _ = await _context.SaveChangesAsync(cancellationToken); + } + } + + /// + public async Task DeleteByAccountIdAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + List entities = await _context.FileMetadata + .Where(fm => fm.AccountId == accountId) + .ToListAsync(cancellationToken); + + _context.FileMetadata.RemoveRange(entities); + _ = await _context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task SaveBatchAsync(IEnumerable fileMetadataList, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(fileMetadataList); + + var entities = fileMetadataList.Select(MapToEntity).ToList(); + + foreach(FileMetadataEntity? entity in entities) + { + FileMetadataEntity? existing = await _context.FileMetadata.FindAsync([entity.Id], cancellationToken); + if(existing is null) + _ = _context.FileMetadata.Add(entity); + else + _context.Entry(existing).CurrentValues.SetValues(entity); + } + + _ = await _context.SaveChangesAsync(cancellationToken); + } + + private static FileMetadata MapToModel(FileMetadataEntity entity) + => new( + entity.Id, + entity.AccountId, + entity.Name, + entity.Path, + entity.Size, + entity.LastModifiedUtc, + entity.LocalPath, + entity.CTag, + entity.ETag, + entity.LocalHash, + (FileSyncStatus)entity.SyncStatus, + entity.LastSyncDirection.HasValue ? (SyncDirection)entity.LastSyncDirection.Value : null + ); + + private static FileMetadataEntity MapToEntity(FileMetadata model) + => new() + { + Id = model.Id, + AccountId = model.AccountId, + Name = model.Name, + Path = model.Path, + Size = model.Size, + LastModifiedUtc = model.LastModifiedUtc, + LocalPath = model.LocalPath, + CTag = model.CTag, + ETag = model.ETag, + LocalHash = model.LocalHash, + SyncStatus = (int)model.SyncStatus, + LastSyncDirection = model.LastSyncDirection.HasValue ? (int)model.LastSyncDirection.Value : null + }; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/FileOperationLogRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/FileOperationLogRepository.cs new file mode 100644 index 0000000..67ecdfb --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/FileOperationLogRepository.cs @@ -0,0 +1,113 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Models.Enums; +using Microsoft.EntityFrameworkCore; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +/// +/// Repository implementation for managing file operation logs. +/// +public sealed class FileOperationLogRepository : IFileOperationLogRepository +{ + private readonly AppDbContext _context; + + public FileOperationLogRepository(AppDbContext context) => _context = context ?? throw new ArgumentNullException(nameof(context)); + + /// + public async Task> GetBySessionIdAsync(string syncSessionId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(syncSessionId); + + List entities = await _context.FileOperationLogs + .AsNoTracking() + .Where(f => f.SyncSessionId == syncSessionId) + .OrderBy(f => f.Timestamp) + .ToListAsync(cancellationToken); + + return [.. entities.Select(MapToModel)]; + } + + /// + public async Task> GetByAccountIdAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + List entities = await _context.FileOperationLogs + .AsNoTracking() + .Where(f => f.AccountId == accountId) + .OrderByDescending(f => f.Timestamp) + .ToListAsync(cancellationToken); + + return [.. entities.Select(MapToModel)]; + } + + /// + public async Task> GetByAccountIdAsync(string accountId, int pageSize, int skip, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + List entities = await _context.FileOperationLogs + .AsNoTracking() + .Where(f => f.AccountId == accountId) + .OrderByDescending(f => f.Timestamp) + .Skip(skip) + .Take(pageSize) + .ToListAsync(cancellationToken); + + return [.. entities.Select(MapToModel)]; + } + + /// + public async Task AddAsync(FileOperationLog operationLog, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(operationLog); + + FileOperationLogEntity entity = MapToEntity(operationLog); + _ = _context.FileOperationLogs.Add(entity); + _ = await _context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task DeleteOldOperationsAsync(string accountId, DateTime olderThan, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + _ = await _context.FileOperationLogs + .Where(f => f.AccountId == accountId && f.Timestamp < olderThan) + .ExecuteDeleteAsync(cancellationToken); + } + + private static FileOperationLog MapToModel(FileOperationLogEntity entity) + => new( + entity.Id, + entity.SyncSessionId, + entity.AccountId, + entity.Timestamp, + (FileOperation)entity.Operation, + entity.FilePath, + entity.LocalPath, + entity.OneDriveId, + entity.FileSize, + entity.LocalHash, + entity.RemoteHash, + entity.LastModifiedUtc, + entity.Reason); + + private static FileOperationLogEntity MapToEntity(FileOperationLog model) + => new() + { + Id = model.Id, + SyncSessionId = model.SyncSessionId, + AccountId = model.AccountId, + Timestamp = model.Timestamp, + Operation = (int)model.Operation, + FilePath = model.FilePath, + LocalPath = model.LocalPath, + OneDriveId = model.OneDriveId, + FileSize = model.FileSize, + LocalHash = model.LocalHash, + RemoteHash = model.RemoteHash, + LastModifiedUtc = model.LastModifiedUtc, + Reason = model.Reason + }; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IAccountRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IAccountRepository.cs new file mode 100644 index 0000000..1d344af --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IAccountRepository.cs @@ -0,0 +1,53 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +/// +/// Repository for managing account data. +/// +public interface IAccountRepository +{ + /// + /// Gets all accounts. + /// + /// Cancellation token. + /// List of all accounts. + Task> GetAllAsync(CancellationToken cancellationToken = default); + + /// + /// Gets an account by its ID. + /// + /// The account identifier. + /// Cancellation token. + /// The account if found, otherwise null. + Task GetByIdAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Adds a new account. + /// + /// The account to add. + /// Cancellation token. + Task AddAsync(AccountInfo account, CancellationToken cancellationToken = default); + + /// + /// Updates an existing account. + /// + /// The account to update. + /// Cancellation token. + Task UpdateAsync(AccountInfo account, CancellationToken cancellationToken = default); + + /// + /// Deletes an account by its ID. + /// + /// The account identifier. + /// Cancellation token. + Task DeleteAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Checks if an account with the specified ID exists. + /// + /// The account identifier. + /// Cancellation token. + /// True if the account exists, otherwise false. + Task ExistsAsync(string accountId, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IDebugLogRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IDebugLogRepository.cs new file mode 100644 index 0000000..9841303 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IDebugLogRepository.cs @@ -0,0 +1,49 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +/// +/// Repository for accessing debug log entries. +/// +public interface IDebugLogRepository +{ + /// + /// Gets debug log entries for a specific account with paging support. + /// + /// The account ID to filter by. + /// Number of records to retrieve. + /// Number of records to skip. + /// Cancellation token. + /// List of debug log entries ordered by timestamp descending (newest first). + Task> GetByAccountIdAsync(string accountId, int pageSize, int skip, CancellationToken cancellationToken = default); + + /// + /// Gets all debug log entries for a specific account. + /// + /// The account ID to filter by. + /// Cancellation token. + /// List of all debug log entries for the account. + Task> GetByAccountIdAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Gets the count of debug log entries for a specific account. + /// + /// The account ID to filter by. + /// Cancellation token. + /// The count of debug log entries for the account. + Task GetDebugLogCountByAccountIdAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Deletes all debug log entries for a specific account. + /// + /// The account ID to filter by. + /// Cancellation token. + Task DeleteByAccountIdAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Deletes debug log entries older than the specified date. + /// + /// Delete entries older than this date. + /// Cancellation token. + Task DeleteOlderThanAsync(DateTime olderThan, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IFileMetadataRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IFileMetadataRepository.cs new file mode 100644 index 0000000..4e983d5 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IFileMetadataRepository.cs @@ -0,0 +1,79 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Models.Enums; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +/// +/// Repository for managing file metadata. +/// +public interface IFileMetadataRepository +{ + /// + /// Gets all file metadata for a specific account. + /// + /// The account identifier. + /// Cancellation token. + /// List of file metadata for the account. + Task> GetByAccountIdAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Gets file metadata by its ID. + /// + /// The file identifier. + /// Cancellation token. + /// The file metadata if found, otherwise null. + Task GetByIdAsync(string id, CancellationToken cancellationToken = default); + + /// + /// Gets file metadata by account ID and path. + /// + /// The account identifier. + /// The file path. + /// Cancellation token. + /// The file metadata if found, otherwise null. + Task GetByPathAsync(string accountId, string path, CancellationToken cancellationToken = default); + + /// + /// Gets all files with a specific sync status for an account. + /// + /// The account identifier. + /// The sync status to filter by. + /// Cancellation token. + /// List of file metadata matching the status. + Task> GetByStatusAsync(string accountId, FileSyncStatus status, CancellationToken cancellationToken = default); + + /// + /// Adds new file metadata. + /// + /// The file metadata to add. + /// Cancellation token. + Task AddAsync(FileMetadata fileMetadata, CancellationToken cancellationToken = default); + + /// + /// Updates existing file metadata. + /// + /// The file metadata to update. + /// Cancellation token. + Task UpdateAsync(FileMetadata fileMetadata, CancellationToken cancellationToken = default); + + /// + /// Deletes file metadata by its ID. + /// + /// The file identifier. + /// Cancellation token. + Task DeleteAsync(string id, CancellationToken cancellationToken = default); + + /// + /// Deletes all file metadata for a specific account. + /// + /// The account identifier. + /// Cancellation token. + Task DeleteByAccountIdAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Saves multiple file metadata entries in a batch operation. + /// + /// The file metadata entries to save. + /// Cancellation token. + Task SaveBatchAsync(IEnumerable fileMetadataList, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IFileOperationLogRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IFileOperationLogRepository.cs new file mode 100644 index 0000000..7cffc63 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/IFileOperationLogRepository.cs @@ -0,0 +1,50 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +/// +/// Repository for managing file operation logs. +/// +public interface IFileOperationLogRepository +{ + /// + /// Gets all file operations for a sync session. + /// + /// The sync session identifier. + /// Cancellation token. + /// List of file operations. + Task> GetBySessionIdAsync(string syncSessionId, CancellationToken cancellationToken = default); + + /// + /// Gets all file operations for an account. + /// + /// The account identifier. + /// Cancellation token. + /// List of file operations. + Task> GetByAccountIdAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Gets file operations for an account with paging support. + /// + /// The account identifier. + /// Number of records to return. + /// Number of records to skip. + /// Cancellation token. + /// List of file operations. + Task> GetByAccountIdAsync(string accountId, int pageSize, int skip, CancellationToken cancellationToken = default); + + /// + /// Adds a new file operation log. + /// + /// The operation log to add. + /// Cancellation token. + Task AddAsync(FileOperationLog operationLog, CancellationToken cancellationToken = default); + + /// + /// Deletes old file operation logs for an account. + /// + /// The account identifier. + /// Delete operations older than this date. + /// Cancellation token. + Task DeleteOldOperationsAsync(string accountId, DateTime olderThan, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/ISyncConfigurationRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/ISyncConfigurationRepository.cs new file mode 100644 index 0000000..eade2ea --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/ISyncConfigurationRepository.cs @@ -0,0 +1,66 @@ +using AStar.Dev.Functional.Extensions; +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +/// +/// Repository for managing sync configuration data. +/// +public interface ISyncConfigurationRepository +{ + /// + /// Gets all sync configurations for a specific account. + /// + /// The account identifier. + /// Cancellation token. + /// List of sync configurations for the account. + Task> GetByAccountIdAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Gets all selected folder paths for a specific account. + /// + /// The account identifier. + /// Cancellation token. + /// List of selected folder paths. + Task> GetSelectedFoldersAsync(string accountId, CancellationToken cancellationToken = default); + + Task, ErrorResponse>> GetSelectedFolders2Async(string accountId, CancellationToken cancellationToken = default); + + /// + /// Adds a new sync configuration. + /// + /// The configuration to add. + /// Cancellation token. + /// The updated sync configuration. + Task AddAsync(SyncConfiguration configuration, CancellationToken cancellationToken = default); + + /// + /// Updates an existing sync configuration. + /// + /// The configuration to update. + /// Cancellation token. + /// The updated sync configuration. + Task UpdateAsync(SyncConfiguration configuration, CancellationToken cancellationToken = default); + + /// + /// Deletes a sync configuration by its ID. + /// + /// The configuration identifier. + /// Cancellation token. + Task DeleteAsync(int id, CancellationToken cancellationToken = default); + + /// + /// Deletes all sync configurations for a specific account. + /// + /// The account identifier. + /// Cancellation token. + Task DeleteByAccountIdAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Saves multiple sync configurations for an account in a batch operation. + /// + /// The account identifier. + /// The configurations to save. + /// Cancellation token. + Task SaveBatchAsync(string accountId, IEnumerable configurations, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/ISyncConflictRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/ISyncConflictRepository.cs new file mode 100644 index 0000000..49b7a15 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/ISyncConflictRepository.cs @@ -0,0 +1,70 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +/// +/// Repository for managing sync conflicts. +/// +public interface ISyncConflictRepository +{ + /// + /// Gets all conflicts for a specific account. + /// + /// The account identifier. + /// Cancellation token. + /// List of conflicts for the account. + Task> GetByAccountIdAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Gets unresolved conflicts for a specific account. + /// + /// The account identifier. + /// Cancellation token. + /// List of unresolved conflicts for the account. + Task> GetUnresolvedByAccountIdAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Gets a conflict by its ID. + /// + /// The conflict identifier. + /// Cancellation token. + /// The conflict if found, otherwise null. + Task GetByIdAsync(string id, CancellationToken cancellationToken = default); + + /// + /// Gets a conflict by account ID and file path. + /// + /// The account identifier. + /// The file path. + /// Cancellation token. + /// The conflict if found, otherwise null. + Task GetByFilePathAsync(string accountId, string filePath, CancellationToken cancellationToken = default); + + /// + /// Adds a new conflict. + /// + /// The conflict to add. + /// Cancellation token. + Task AddAsync(SyncConflict conflict, CancellationToken cancellationToken = default); + + /// + /// Updates an existing conflict. + /// + /// The conflict to update. + /// Cancellation token. + Task UpdateAsync(SyncConflict conflict, CancellationToken cancellationToken = default); + + /// + /// Deletes a conflict by its ID. + /// + /// The conflict identifier. + /// Cancellation token. + Task DeleteAsync(string id, CancellationToken cancellationToken = default); + + /// + /// Deletes all conflicts for a specific account. + /// + /// The account identifier. + /// Cancellation token. + Task DeleteByAccountIdAsync(string accountId, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/ISyncSessionLogRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/ISyncSessionLogRepository.cs new file mode 100644 index 0000000..dbf7f54 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/ISyncSessionLogRepository.cs @@ -0,0 +1,47 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +/// +/// Repository for managing sync session logs. +/// +public interface ISyncSessionLogRepository +{ + /// + /// Gets all sync sessions for an account. + /// + /// The account identifier. + /// Cancellation token. + /// List of sync sessions. + Task> GetByAccountIdAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Gets a sync session by ID. + /// + /// The session identifier. + /// Cancellation token. + /// The sync session if found, otherwise null. + Task GetByIdAsync(string id, CancellationToken cancellationToken = default); + + /// + /// Adds a new sync session log. + /// + /// The session log to add. + /// Cancellation token. + Task AddAsync(SyncSessionLog sessionLog, CancellationToken cancellationToken = default); + + /// + /// Updates an existing sync session log. + /// + /// The session log to update. + /// Cancellation token. + Task UpdateAsync(SyncSessionLog sessionLog, CancellationToken cancellationToken = default); + + /// + /// Deletes old sync session logs for an account. + /// + /// The account identifier. + /// Delete sessions older than this date. + /// Cancellation token. + Task DeleteOldSessionsAsync(string accountId, DateTime olderThan, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/SyncConfigurationRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/SyncConfigurationRepository.cs new file mode 100644 index 0000000..f3a16de --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/SyncConfigurationRepository.cs @@ -0,0 +1,175 @@ +using AStar.Dev.Functional.Extensions; +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +/// +/// Repository implementation for managing sync configuration data. +/// +public sealed class SyncConfigurationRepository : ISyncConfigurationRepository +{ + private readonly AppDbContext _context; + + public SyncConfigurationRepository(AppDbContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + } + + /// + public async Task> GetByAccountIdAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + List entities = await _context.SyncConfigurations + .Where(sc => sc.AccountId == accountId) + .ToListAsync(cancellationToken); + + return [.. entities.Select(MapToModel)]; + } + + /// + public async Task> GetSelectedFoldersAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + return await _context.SyncConfigurations + .Where(sc => sc.AccountId == accountId && sc.IsSelected) + .Select(sc => CleanUpPath(sc.FolderPath)) + .Distinct() + .ToListAsync(cancellationToken); + } + + /// + public async Task, ErrorResponse>> GetSelectedFolders2Async(string accountId, CancellationToken cancellationToken = default) + => await _context.SyncConfigurations + .Where(sc => sc.AccountId == accountId && sc.IsSelected) + .Select(sc => CleanUpPath(sc.FolderPath)) + .Distinct() + .ToListAsync(cancellationToken); + + private static string CleanUpPath(string localFolderPath) + { + var indexOfDrives = localFolderPath.IndexOf("drives", StringComparison.OrdinalIgnoreCase); + if(indexOfDrives >= 0) + { + var indexOfColon = localFolderPath.IndexOf(":/", StringComparison.OrdinalIgnoreCase); + if(indexOfColon > 0) + { + var part1 = localFolderPath[..indexOfDrives]; + var part2 = localFolderPath[(indexOfColon + 2)..]; + localFolderPath = part1 + part2; + } + } + + return localFolderPath; + } + + /// + public async Task AddAsync(SyncConfiguration configuration, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(configuration); + SyncConfigurationEntity? existingEntity = await _context.SyncConfigurations + .FirstOrDefaultAsync(sc => sc.AccountId == configuration.AccountId && sc.FolderPath == configuration.FolderPath, cancellationToken); + + if(existingEntity is not null) return configuration; + + var lastIndexOf = configuration.FolderPath.LastIndexOf('/'); + if(lastIndexOf > 0) + { + var parentPath = configuration.FolderPath[..lastIndexOf]; + var test = ""; // SyncEngine.FormatScanningFolderForDisplay(parentPath)!.Replace("OneDrive: ", string.Empty); + SyncConfigurationEntity? parentEntity = await _context.SyncConfigurations + .FirstOrDefaultAsync(sc => sc.AccountId == configuration.AccountId && (sc.FolderPath == parentPath || sc.FolderPath == test), cancellationToken); + + if(parentEntity is not null) + { + // var updatedPath = ""; // SyncEngine.FormatScanningFolderForDisplay(configuration.FolderPath)!.Replace("OneDrive: ", string.Empty); + // configuration = configuration with { FolderPath = updatedPath, IsSelected = parentEntity.IsSelected }; + } + } + + SyncConfigurationEntity entity = MapToEntity(configuration); + _ = _context.SyncConfigurations.Add(entity); + _ = await _context.SaveChangesAsync(cancellationToken); + + return configuration; + } + + /// + public async Task UpdateAsync(SyncConfiguration configuration, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(configuration); + + SyncConfigurationEntity entity = await _context.SyncConfigurations.FindAsync([configuration.Id], cancellationToken) ?? throw new InvalidOperationException($"Sync configuration with ID '{configuration.Id}' not found."); + + entity.AccountId = configuration.AccountId; + entity.FolderPath = configuration.FolderPath; + entity.IsSelected = configuration.IsSelected; + entity.LastModifiedUtc = configuration.LastModifiedUtc; + + _ = await _context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task DeleteAsync(int id, CancellationToken cancellationToken = default) + { + SyncConfigurationEntity? entity = await _context.SyncConfigurations.FindAsync([id], cancellationToken); + if(entity is not null) + { + _ = _context.SyncConfigurations.Remove(entity); + _ = await _context.SaveChangesAsync(cancellationToken); + } + } + + /// + public async Task DeleteByAccountIdAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + List entities = await _context.SyncConfigurations + .Where(sc => sc.AccountId == accountId) + .ToListAsync(cancellationToken); + + _context.SyncConfigurations.RemoveRange(entities); + _ = await _context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task SaveBatchAsync(string accountId, IEnumerable configurations, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + ArgumentNullException.ThrowIfNull(configurations); + + List existingEntities = await _context.SyncConfigurations + .Where(sc => sc.AccountId == accountId) + .ToListAsync(cancellationToken); + + _context.SyncConfigurations.RemoveRange(existingEntities); + + var newEntities = configurations.Select(MapToEntity).ToList(); + _context.SyncConfigurations.AddRange(newEntities); + + _ = await _context.SaveChangesAsync(cancellationToken); + } + + private static SyncConfiguration MapToModel(SyncConfigurationEntity entity) + => new( + entity.Id, + entity.AccountId, + entity.FolderPath, + entity.IsSelected, + entity.LastModifiedUtc + ); + + private static SyncConfigurationEntity MapToEntity(SyncConfiguration model) + => new() + { + Id = model.Id, + AccountId = model.AccountId, + FolderPath = model.FolderPath, + IsSelected = model.IsSelected, + LastModifiedUtc = model.LastModifiedUtc + }; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/SyncConflictRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/SyncConflictRepository.cs new file mode 100644 index 0000000..90728c2 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/SyncConflictRepository.cs @@ -0,0 +1,122 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +/// +/// Repository for managing sync conflicts in the database. +/// +public sealed class SyncConflictRepository : ISyncConflictRepository +{ + private readonly AppDbContext _context; + + public SyncConflictRepository(AppDbContext context) => _context = context ?? throw new ArgumentNullException(nameof(context)); + + /// + public async Task> GetByAccountIdAsync(string accountId, CancellationToken cancellationToken = default) + { + List entities = await _context.SyncConflicts + .Where(c => c.AccountId == accountId) + .OrderByDescending(c => c.DetectedUtc) + .ToListAsync(cancellationToken); + + return [.. entities.Select(MapToDomain)]; + } + + /// + public async Task> GetUnresolvedByAccountIdAsync(string accountId, CancellationToken cancellationToken = default) + { + List entities = await _context.SyncConflicts + .Where(c => c.AccountId == accountId && !c.IsResolved) + .OrderByDescending(c => c.DetectedUtc) + .ToListAsync(cancellationToken); + + return [.. entities.Select(MapToDomain)]; + } + + /// + public async Task GetByIdAsync(string id, CancellationToken cancellationToken = default) + { + SyncConflictEntity? entity = await _context.SyncConflicts + .FirstOrDefaultAsync(c => c.Id == id, cancellationToken); + + return entity is not null ? MapToDomain(entity) : null; + } + + /// + public async Task GetByFilePathAsync(string accountId, string filePath, CancellationToken cancellationToken = default) + { + SyncConflictEntity? entity = await _context.SyncConflicts + .Where(c => c.AccountId == accountId && c.FilePath == filePath && !c.IsResolved) + .OrderByDescending(c => c.DetectedUtc) + .FirstOrDefaultAsync(cancellationToken); + + return entity is not null ? MapToDomain(entity) : null; + } + + /// + public async Task AddAsync(SyncConflict conflict, CancellationToken cancellationToken = default) + { + SyncConflictEntity entity = MapToEntity(conflict); + _ = await _context.SyncConflicts.AddAsync(entity, cancellationToken); + _ = await _context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task UpdateAsync(SyncConflict conflict, CancellationToken cancellationToken = default) + { + SyncConflictEntity existingEntity = await _context.SyncConflicts + .FirstOrDefaultAsync(c => c.Id == conflict.Id, cancellationToken) ?? throw new InvalidOperationException($"Conflict not found: {conflict.Id}"); + + existingEntity.ResolutionStrategy = conflict.ResolutionStrategy; + existingEntity.IsResolved = conflict.IsResolved; + + _ = await _context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task DeleteAsync(string id, CancellationToken cancellationToken = default) + { + SyncConflictEntity? entity = await _context.SyncConflicts + .FirstOrDefaultAsync(c => c.Id == id, cancellationToken); + + if(entity is not null) + { + _ = _context.SyncConflicts.Remove(entity); + _ = await _context.SaveChangesAsync(cancellationToken); + } + } + + /// + public async Task DeleteByAccountIdAsync(string accountId, CancellationToken cancellationToken = default) => _ = await _context.SyncConflicts + .Where(c => c.AccountId == accountId) + .ExecuteDeleteAsync(cancellationToken); + + private static SyncConflict MapToDomain(SyncConflictEntity entity) + => new SyncConflict( + entity.Id, + entity.AccountId, + entity.FilePath, + entity.LocalModifiedUtc, + entity.RemoteModifiedUtc, + entity.LocalSize, + entity.RemoteSize, + entity.DetectedUtc, + entity.ResolutionStrategy, + entity.IsResolved); + + private static SyncConflictEntity MapToEntity(SyncConflict conflict) + => new() + { + Id = conflict.Id, + AccountId = conflict.AccountId, + FilePath = conflict.FilePath, + LocalModifiedUtc = conflict.LocalModifiedUtc, + RemoteModifiedUtc = conflict.RemoteModifiedUtc, + LocalSize = conflict.LocalSize, + RemoteSize = conflict.RemoteSize, + DetectedUtc = conflict.DetectedUtc, + ResolutionStrategy = conflict.ResolutionStrategy, + IsResolved = conflict.IsResolved + }; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/SyncSessionLogRepository.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/SyncSessionLogRepository.cs new file mode 100644 index 0000000..dc08a3a --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/Repositories/SyncSessionLogRepository.cs @@ -0,0 +1,107 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Models.Enums; +using Microsoft.EntityFrameworkCore; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +/// +/// Repository implementation for managing sync session logs. +/// +public sealed class SyncSessionLogRepository : ISyncSessionLogRepository +{ + private readonly AppDbContext _context; + + public SyncSessionLogRepository(AppDbContext context) => _context = context ?? throw new ArgumentNullException(nameof(context)); + + /// + public async Task> GetByAccountIdAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + List entities = await _context.SyncSessionLogs + .AsNoTracking() + .Where(s => s.AccountId == accountId) + .OrderByDescending(s => s.StartedUtc) + .ToListAsync(cancellationToken); + + return [.. entities.Select(MapToModel)]; + } + + /// + public async Task GetByIdAsync(string id, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(id); + + SyncSessionLogEntity? entity = await _context.SyncSessionLogs + .AsNoTracking() + .FirstOrDefaultAsync(s => s.Id == id, cancellationToken); + + return entity is not null ? MapToModel(entity) : null; + } + + /// + public async Task AddAsync(SyncSessionLog sessionLog, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(sessionLog); + + SyncSessionLogEntity entity = MapToEntity(sessionLog); + _ = _context.SyncSessionLogs.Add(entity); + _ = await _context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task UpdateAsync(SyncSessionLog sessionLog, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(sessionLog); + + SyncSessionLogEntity entity = await _context.SyncSessionLogs.FindAsync([sessionLog.Id], cancellationToken) ?? throw new InvalidOperationException($"Sync session log with ID '{sessionLog.Id}' not found."); + + entity.CompletedUtc = sessionLog.CompletedUtc; + entity.Status = (int)sessionLog.Status; + entity.FilesUploaded = sessionLog.FilesUploaded; + entity.FilesDownloaded = sessionLog.FilesDownloaded; + entity.FilesDeleted = sessionLog.FilesDeleted; + entity.ConflictsDetected = sessionLog.ConflictsDetected; + entity.TotalBytes = sessionLog.TotalBytes; + + _ = await _context.SaveChangesAsync(cancellationToken); + } + + /// + public async Task DeleteOldSessionsAsync(string accountId, DateTime olderThan, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + _ = await _context.SyncSessionLogs + .Where(s => s.AccountId == accountId && s.StartedUtc < olderThan) + .ExecuteDeleteAsync(cancellationToken); + } + + private static SyncSessionLog MapToModel(SyncSessionLogEntity entity) + => new( + entity.Id, + entity.AccountId, + entity.StartedUtc, + entity.CompletedUtc, + (SyncStatus)entity.Status, + entity.FilesUploaded, + entity.FilesDownloaded, + entity.FilesDeleted, + entity.ConflictsDetected, + entity.TotalBytes); + + private static SyncSessionLogEntity MapToEntity(SyncSessionLog model) + => new() + { + Id = model.Id, + AccountId = model.AccountId, + StartedUtc = model.StartedUtc, + CompletedUtc = model.CompletedUtc, + Status = (int)model.Status, + FilesUploaded = model.FilesUploaded, + FilesDownloaded = model.FilesDownloaded, + FilesDeleted = model.FilesDeleted, + ConflictsDetected = model.ConflictsDetected, + TotalBytes = model.TotalBytes + }; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/SqliteTypeConverters.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/SqliteTypeConverters.cs new file mode 100644 index 0000000..82bc5b5 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Data/SqliteTypeConverters.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Data; + +public static class SqliteTypeConverters +{ + public static ValueConverter DateTimeOffsetToTicks { get; } = + new(dto => dto.ToUniversalTime().UtcTicks, ticks => new DateTimeOffset(ticks, TimeSpan.Zero)); + + public static ValueConverter NullableDateTimeOffsetToTicks { get; } = + new(dto => dto.HasValue ? dto.Value.ToUniversalTime().UtcTicks : null, + ticks => ticks.HasValue ? new DateTimeOffset(ticks.Value, TimeSpan.Zero) : null); + + public static ValueConverter TimeSpanToTicks { get; } = + new(ts => ts.Ticks, ticks => TimeSpan.FromTicks(ticks)); + + public static ValueConverter NullableTimeSpanToTicks { get; } = + new(ts => ts.HasValue ? ts.Value.Ticks : null, ticks => ticks.HasValue ? TimeSpan.FromTicks(ticks.Value) : null); + + public static ValueConverter GuidToBytes { get; } = + new(g => g.ToByteArray(), b => new Guid(b)); + + // Allow nullable byte[] result type to avoid nullability warning + public static ValueConverter NullableGuidToBytes { get; } = + new(g => g.HasValue ? g.Value.ToByteArray() : null, b => b != null ? new Guid(b) : null); + + public static ValueConverter DecimalToCents { get; } = + new(d => (long)Math.Round(d * 100m), l => l / 100m); + + public static ValueConverter NullableDecimalToCents { get; } = + new(d => d.HasValue ? (long?)Math.Round(d.Value * 100m) : null, l => l.HasValue ? l.Value / 100m : null); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/DependencyInjection/InfrastructureServiceCollectionExtensions.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/DependencyInjection/InfrastructureServiceCollectionExtensions.cs new file mode 100644 index 0000000..85ed35c --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/DependencyInjection/InfrastructureServiceCollectionExtensions.cs @@ -0,0 +1,94 @@ +using System.IO.Abstractions; +using AStar.Dev.OneDrive.Client.Core.ConfigurationSettings; +using AStar.Dev.OneDrive.Client.Core.Interfaces; +using AStar.Dev.OneDrive.Client.Infrastructure.Auth; +using AStar.Dev.OneDrive.Client.Infrastructure.Data; +using AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; +using AStar.Dev.OneDrive.Client.Infrastructure.FileSystem; +using AStar.Dev.OneDrive.Client.Infrastructure.Graph; +using AStar.Dev.OneDrive.Client.Infrastructure.HealthChecks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Polly; +using Polly.CircuitBreaker; +using Polly.Extensions.Http; +using Polly.Retry; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.DependencyInjection; + +public static class InfrastructureServiceCollectionExtensions +{ + public static IServiceCollection AddInfrastructure(this IServiceCollection services, string sqliteConnectionString, string localRoot, MsalConfigurationSettings msalConfigurationSettings) + { + _ = services.AddDbContext(opts => opts.UseSqlite(sqliteConnectionString)); + _ = services.AddDbContextFactory(opts => opts.UseSqlite(sqliteConnectionString)); + _ = services.AddScoped(sp => + { + IDbContextFactory factory = sp.GetRequiredService>(); + ILogger logger = sp.GetRequiredService>(); + return new EfSyncRepository(factory, logger); + }); + + _ = services.AddSingleton(_ => new MsalAuthService(msalConfigurationSettings)); + _ = services.AddHttpClient() + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + AllowAutoRedirect = true, + MaxConnectionsPerServer = 10 + }) + .ConfigureHttpClient(client => client.Timeout = TimeSpan.FromMinutes(5)) + .AddPolicyHandler(GetRetryPolicy()) + .AddPolicyHandler(GetCircuitBreakerPolicy()); + + _ = services.AddSingleton(sp => new System.IO.Abstractions.FileSystem()); + _ = services.AddSingleton(sp => new LocalFileSystemAdapter(localRoot, sp.GetRequiredService())); + + // Health checks + _ = services.AddHealthChecks() + .AddCheck("database") + .AddCheck("graph_api"); + + _ = services.AddSingleton>(sp => provider => + { + using IServiceScope scope = provider.CreateScope(); + AppDbContext db = scope.ServiceProvider.GetRequiredService(); + DbInitializer.EnsureDatabaseCreatedAndConfigured(db); + }); + + return services; + } + + /// + /// Creates a retry policy with exponential backoff for transient HTTP failures. + /// Retries on network failures, 5xx server errors, 429 rate limiting, and IOException. + /// + private static AsyncRetryPolicy GetRetryPolicy() + => Policy + .Handle() + .Or(ex => ex.Message.Contains("forcibly closed") || ex.Message.Contains("transport connection")) + .OrResult(msg => (int)msg.StatusCode >= 500 || msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests || msg.StatusCode == System.Net.HttpStatusCode.RequestTimeout) + .WaitAndRetryAsync( + retryCount: 3, + sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + onRetry: (outcome, timespan, retryCount, context) => + { + var error = outcome.Exception?.Message ?? outcome.Result?.StatusCode.ToString() ?? "Unknown"; + Console.WriteLine($"[Graph API] Retry {retryCount}/3 after {timespan.TotalSeconds:F1}s. Reason: {error}"); + }); + + /// + /// Creates a circuit breaker policy to prevent cascading failures. + /// Opens circuit after 5 consecutive failures, stays open for 30 seconds. + /// + private static AsyncCircuitBreakerPolicy GetCircuitBreakerPolicy() + => HttpPolicyExtensions + .HandleTransientHttpError() + .CircuitBreakerAsync( + handledEventsAllowedBeforeBreaking: 5, + durationOfBreak: TimeSpan.FromSeconds(30), + onBreak: (outcome, duration) => + Console.WriteLine($"Circuit breaker opened for {duration.TotalSeconds}s due to {outcome.Result?.StatusCode}"), + onReset: () => + Console.WriteLine("Circuit breaker reset")); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/FileSystem/LocalFileSystemAdapter.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/FileSystem/LocalFileSystemAdapter.cs new file mode 100644 index 0000000..beef57a --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/FileSystem/LocalFileSystemAdapter.cs @@ -0,0 +1,62 @@ +using System.IO.Abstractions; +using AStar.Dev.OneDrive.Client.Core.Dtos; +using AStar.Dev.OneDrive.Client.Core.Interfaces; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.FileSystem; + +public sealed class LocalFileSystemAdapter(string root, IFileSystem fileSystem) : IFileSystemAdapter +{ + private string FullPath(string relative) => Path.Combine(root, relative.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + + public IFileInfo GetFileInfo(string relativePath) + { + var full = FullPath(relativePath); + return fileSystem.FileInfo.New(full); + } + + public async Task WriteFileAsync(string relativePath, Stream content, CancellationToken cancellationToken) + { + var full = FullPath(relativePath); + _ = fileSystem.Directory.CreateDirectory(fileSystem.Path.GetDirectoryName(full)!); + await using Stream fs = fileSystem.File.Create(full); + await content.CopyToAsync(fs, cancellationToken); + } + + public Task OpenReadAsync(string relativePath, CancellationToken cancellationToken) + { + var full = FullPath(relativePath); + return !fileSystem.File.Exists(full) ? Task.FromResult(null) : Task.FromResult(fileSystem.File.OpenRead(full)); + } + + public Task DeleteFileAsync(string relativePath, CancellationToken cancellationToken) + { + var full = FullPath(relativePath); + if(fileSystem.File.Exists(full)) + fileSystem.File.Delete(full); + + return Task.CompletedTask; + } + + public Task> EnumerateFilesAsync(CancellationToken cancellationToken) + { + var list = new List(); + if(!fileSystem.Directory.Exists(root)) + return Task.FromResult(list.AsEnumerable()); + foreach(var file in fileSystem.Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)) + { + IFileInfo fi = fileSystem.FileInfo.New(file); + var rel = fileSystem.Path.GetRelativePath(root, file); + list.Add(new LocalFileInfo(rel, fi.Length, fi.LastWriteTimeUtc, null)); + } + + return Task.FromResult>(list); + } + + public Task OpenWriteAsync(string relativePath, CancellationToken cancellationToken) + { + var full = FullPath(relativePath); + _ = fileSystem.Directory.CreateDirectory(fileSystem.Path.GetDirectoryName(full)!); + Stream stream = fileSystem.File.Create(full); + return Task.FromResult(stream); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Graph/GraphClientWrapper.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Graph/GraphClientWrapper.cs new file mode 100644 index 0000000..1a8c8cc --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Graph/GraphClientWrapper.cs @@ -0,0 +1,156 @@ +using System.Globalization; +using System.Net.Http.Headers; +using System.Text.Json; +using AStar.Dev.OneDrive.Client.Core.ConfigurationSettings; +using AStar.Dev.OneDrive.Client.Core.Dtos; +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Interfaces; +using Microsoft.Extensions.Logging; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Graph; + +public sealed class GraphClientWrapper(IAuthService auth, HttpClient http, MsalConfigurationSettings msalConfigurationSettings, ILogger logger) : IGraphClient +{ + public async Task GetDriveDeltaPageAsync(string accountId, string? deltaOrNextLink, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var url = GetDeltaOrNextUrl(deltaOrNextLink); + + var token = await auth.GetAccessTokenAsync(accountId, cancellationToken); + using var req = new HttpRequestMessage(HttpMethod.Get, url); + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + + using HttpResponseMessage res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + _ = res.EnsureSuccessStatusCode(); + + await using Stream stream = await res.Content.ReadAsStreamAsync(cancellationToken); + using JsonDocument doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); + + List items = ParseDriveItemRecords(accountId, doc); + + var next = TryGetODataProperty(doc, "@odata.nextLink"); + var delta = TryGetODataProperty(doc, "@odata.deltaLink"); + + return new DeltaPage(items, next, delta); + } + + public async Task DownloadDriveItemContentAsync(string accountId, string driveItemId, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + logger.LogDebug("Requesting download for DriveItemId: {DriveItemId}", driveItemId); + var token = await auth.GetAccessTokenAsync(accountId, cancellationToken); + var url = $"{msalConfigurationSettings.GraphUri}/items/{driveItemId}/content"; + using var req = new HttpRequestMessage(HttpMethod.Get, url); + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + + HttpResponseMessage res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + _ = res.EnsureSuccessStatusCode(); + + Stream stream = await res.Content.ReadAsStreamAsync(cancellationToken); + logger.LogDebug("Download stream acquired for DriveItemId: {DriveItemId}", driveItemId); + + return stream; + } + catch(HttpRequestException ex) + { + logger.LogError(ex, "HTTP request failed for DriveItemId: {DriveItemId}. Status: {StatusCode}, Message: {Message}", + driveItemId, ex.StatusCode, ex.Message); + } + catch(IOException ex) + { + logger.LogError(ex, "Network I/O error downloading DriveItemId: {DriveItemId}. This may indicate a connection reset or timeout. Message: {Message}", + driveItemId, ex.Message); + } + catch(Exception ex) + { + logger.LogError(ex, "Unexpected error downloading DriveItemId: {DriveItemId}. Type: {ExceptionType}, Message: {Message}", + driveItemId, ex.GetType().Name, ex.Message); + } + + throw new InvalidOperationException("Failed to download DriveItem content"); + } + + public async Task CreateUploadSessionAsync(string accountId, string parentPath, string fileName, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var token = await auth.GetAccessTokenAsync(accountId, cancellationToken); + + var path = string.IsNullOrWhiteSpace(parentPath) ? fileName : $"{parentPath.Trim('/')}/{fileName}"; + var url = $"{msalConfigurationSettings.GraphUri}/root:/{Uri.EscapeDataString(path)}:/createUploadSession"; + using var req = new HttpRequestMessage(HttpMethod.Post, url); + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + req.Content = new StringContent("{}", System.Text.Encoding.UTF8, "application/json"); + using HttpResponseMessage res = await http.SendAsync(req, cancellationToken); + _ = res.EnsureSuccessStatusCode(); + await using Stream stream = await res.Content.ReadAsStreamAsync(cancellationToken); + using JsonDocument doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); + var uploadUrl = doc.RootElement.GetProperty("uploadUrl").GetString()!; + DateTimeOffset expiration = doc.RootElement.TryGetProperty("expirationDateTime", out JsonElement ex) ? DateTimeOffset.Parse(ex.GetString()!, CultureInfo.InvariantCulture) : DateTimeOffset.UtcNow.AddHours(1); + + return new UploadSessionInfo(uploadUrl, Guid.CreateVersion7().ToString(), expiration); + } + + public async Task UploadChunkAsync(UploadSessionInfo session, Stream chunk, long rangeStart, long rangeEnd, CancellationToken cancellationToken) + { + using var req = new HttpRequestMessage(HttpMethod.Put, session.UploadUrl); + req.Content = new StreamContent(chunk); + req.Content.Headers.Add("Content-Range", $"bytes {rangeStart}-{rangeEnd}/*"); + using HttpResponseMessage res = await http.SendAsync(req, cancellationToken); + + if(!res.IsSuccessStatusCode) + _ = res.EnsureSuccessStatusCode(); + } + + public async Task DeleteDriveItemAsync(string accountId, string driveItemId, CancellationToken cancellationToken) + { + var token = await auth.GetAccessTokenAsync(accountId, cancellationToken); + var url = $"{msalConfigurationSettings.GraphUri}/items/{driveItemId}"; + using var req = new HttpRequestMessage(HttpMethod.Delete, url); + req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + using HttpResponseMessage res = await http.SendAsync(req, cancellationToken); + if(res.StatusCode is not System.Net.HttpStatusCode.NoContent and not System.Net.HttpStatusCode.NotFound) + _ = res.EnsureSuccessStatusCode(); + } + + private string GetDeltaOrNextUrl(string? deltaOrNextLink) + => string.IsNullOrEmpty(deltaOrNextLink) + ? $"{msalConfigurationSettings.GraphUri}/root/delta" + : deltaOrNextLink; + + private static List ParseDriveItemRecords(string accountId, JsonDocument doc) + { + var items = new List(); + if(doc.RootElement.TryGetProperty("value", out JsonElement arr)) + foreach(JsonElement el in arr.EnumerateArray()) items.Add(ParseDriveItemRecord(accountId, el)); + + return items; + } + + private static DriveItemRecord ParseDriveItemRecord(string accountId, JsonElement jsonElement) + { + var id = jsonElement.GetProperty("id").GetString()!; + var isFolder = jsonElement.TryGetProperty("folder", out _); + var size = jsonElement.TryGetProperty("size", out JsonElement sProp) ? sProp.GetInt64() : 0L; + var parentPath = SetParentPath(jsonElement); + var name = jsonElement.TryGetProperty("name", out JsonElement n) ? n.GetString() ?? id : id; + var relativePath = GraphPathHelpers.BuildRelativePath(parentPath, name); + var eTag = jsonElement.TryGetProperty("eTag", out JsonElement et) ? et.GetString() : null; + var cTag = jsonElement.TryGetProperty("cTag", out JsonElement ctProp) ? ctProp.GetString() : null; + DateTimeOffset lastModifiedUtc = GetLastModifiedUtc(jsonElement); + var isDeleted = jsonElement.TryGetProperty("deleted", out _); + + return new DriveItemRecord(accountId, id, id, relativePath, eTag, cTag, size, lastModifiedUtc, isFolder, isDeleted); + } + + private static DateTimeOffset GetLastModifiedUtc(JsonElement jsonElement) => jsonElement.TryGetProperty("lastModifiedDateTime", out JsonElement lm) + ? DateTimeOffset.Parse(lm.GetString()!, CultureInfo.InvariantCulture) + : DateTimeOffset.UtcNow; + private static string SetParentPath(JsonElement jsonElement) + => jsonElement.TryGetProperty("parentReference", out JsonElement pr) && pr.TryGetProperty("path", out JsonElement p) ? p.GetString() ?? string.Empty : string.Empty; + + private static string? TryGetODataProperty(JsonDocument doc, string propertyName) + => doc.RootElement.TryGetProperty(propertyName, out JsonElement prop) ? prop.GetString() : null; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Graph/GraphPathHelpers.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Graph/GraphPathHelpers.cs new file mode 100644 index 0000000..2bdb937 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Graph/GraphPathHelpers.cs @@ -0,0 +1,40 @@ +namespace AStar.Dev.OneDrive.Client.Infrastructure.Graph; + +/// +/// Utility methods for processing Microsoft Graph API path strings. +/// +public static class GraphPathHelpers +{ + /// + /// Builds a relative file path from a Graph API parent reference path and file name. + /// + /// + /// The parent path from Graph API response (e.g., "/drive/root:/Folder/SubFolder"). + /// + /// The file or folder name. + /// A relative path combining the parent path and name. + /// + /// Graph API returns parent paths in the format "/drive/root:/path/to/folder". + /// This method extracts the portion after "root:/" and combines it with the name. + /// + public static string BuildRelativePath(string parentReferencePath, string name) + { + // parentReference.path looks like "/drive/root:/Folder/SubFolder" + if(string.IsNullOrEmpty(parentReferencePath)) + return name; + + var idx = parentReferencePath.IndexOf(":/", StringComparison.Ordinal); + if(idx >= 0) + { + var after = parentReferencePath[(idx + 2)..].Trim('/'); + if(string.IsNullOrEmpty(after)) + return name; + + // Normalize forward slashes to Path.DirectorySeparatorChar before combining + var normalizedPath = after.Replace('/', Path.DirectorySeparatorChar); + return Path.Combine(normalizedPath, name); + } + + return name; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/HealthChecks/DatabaseHealthCheck.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/HealthChecks/DatabaseHealthCheck.cs new file mode 100644 index 0000000..2c4550c --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/HealthChecks/DatabaseHealthCheck.cs @@ -0,0 +1,40 @@ +using AStar.Dev.OneDrive.Client.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.HealthChecks; + +/// +/// Health check for database connectivity and basic operations. +/// +/// +/// Initializes a new instance of the class. +/// +/// The database context. +public sealed class DatabaseHealthCheck(AppDbContext dbContext) : IHealthCheck +{ + /// + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + // Test database connection by executing a simple query + _ = await dbContext.Database.CanConnectAsync(cancellationToken); + + // Check if database is accessible and has expected tables + var deltaTokenCount = await dbContext.DeltaTokens.CountAsync(cancellationToken); + + var data = new Dictionary + { + { "deltaTokenCount", deltaTokenCount }, + { "connectionState", "connected" } + }; + + return HealthCheckResult.Healthy("Database is accessible and operational", data); + } + catch(Exception ex) + { + return HealthCheckResult.Unhealthy("Database is not accessible", ex); + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/HealthChecks/GraphApiHealthCheck.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/HealthChecks/GraphApiHealthCheck.cs new file mode 100644 index 0000000..654a955 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/HealthChecks/GraphApiHealthCheck.cs @@ -0,0 +1,45 @@ +using AStar.Dev.OneDrive.Client.Core.Interfaces; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace AStar.Dev.OneDrive.Client.Infrastructure.HealthChecks; + +/// +/// Health check for Microsoft Graph API connectivity. +/// +/// +/// Initializes a new instance of the class. +/// +/// The authentication service. +public sealed class GraphApiHealthCheck(IAuthService authService) : IHealthCheck +{ + /// + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + try + { + var signedIn = await authService.IsUserSignedInAsync("PlaceholderAccountId", cancellationToken); + if(!signedIn) return HealthCheckResult.Degraded("User is not authenticated with Microsoft Graph API"); + + var token = await authService.GetAccessTokenAsync("PlaceholderAccountId", cancellationToken); + + var data = new Dictionary + { + { "authenticated", true }, + { "tokenAcquired", !string.IsNullOrEmpty(token) } + }; + + return HealthCheckResult.Healthy("Microsoft Graph API authentication is configured", data); + } + catch(HttpRequestException ex) + { + return HealthCheckResult.Unhealthy("Microsoft Graph API is not accessible", ex, new Dictionary + { + { "errorType", "NetworkError" } + }); + } + catch(Exception ex) + { + return HealthCheckResult.Unhealthy("Health check failed with unexpected error", ex); + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221025933_InitialCreation.Designer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221025933_InitialCreation.Designer.cs new file mode 100644 index 0000000..5233b2a --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221025933_InitialCreation.Designer.cs @@ -0,0 +1,137 @@ +// +using System; +using AStar.Dev.OneDrive.Client.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20251221025933_InitialCreation")] + partial class InitialCreation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DeltaToken", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("LastSyncedUtc") + .HasColumnType("TEXT"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DeltaTokens", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DriveItemRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CTag") + .HasColumnType("TEXT"); + + b.Property("DriveItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ETag") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("RelativePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DriveItemId"); + + b.ToTable("DriveItems", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.LocalFileRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + b.Property("LastWriteUtc") + .HasColumnType("TEXT"); + + b.Property("RelativePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SyncState") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RelativePath"); + + b.ToTable("LocalFiles", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.TransferLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CompletedUtc") + .HasColumnType("TEXT"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedUtc") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("TransferLogs", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221025933_InitialCreation.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221025933_InitialCreation.cs new file mode 100644 index 0000000..9aca923 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221025933_InitialCreation.cs @@ -0,0 +1,92 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Migrations; + +/// +public partial class InitialCreation : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + _ = migrationBuilder.CreateTable( + name: "DeltaTokens", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Token = table.Column(type: "TEXT", nullable: false), + LastSyncedUtc = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => _ = table.PrimaryKey("PK_DeltaTokens", x => x.Id)); + + _ = migrationBuilder.CreateTable( + name: "DriveItems", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + DriveItemId = table.Column(type: "TEXT", nullable: false), + RelativePath = table.Column(type: "TEXT", nullable: false), + ETag = table.Column(type: "TEXT", nullable: true), + CTag = table.Column(type: "TEXT", nullable: true), + Size = table.Column(type: "INTEGER", nullable: false), + LastModifiedUtc = table.Column(type: "TEXT", nullable: false), + IsFolder = table.Column(type: "INTEGER", nullable: false), + IsDeleted = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => _ = table.PrimaryKey("PK_DriveItems", x => x.Id)); + + _ = migrationBuilder.CreateTable( + name: "LocalFiles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + RelativePath = table.Column(type: "TEXT", nullable: false), + Hash = table.Column(type: "TEXT", nullable: true), + Size = table.Column(type: "INTEGER", nullable: false), + LastWriteUtc = table.Column(type: "TEXT", nullable: false), + SyncState = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => _ = table.PrimaryKey("PK_LocalFiles", x => x.Id)); + + _ = migrationBuilder.CreateTable( + name: "TransferLogs", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Type = table.Column(type: "INTEGER", nullable: false), + ItemId = table.Column(type: "TEXT", nullable: false), + StartedUtc = table.Column(type: "TEXT", nullable: false), + CompletedUtc = table.Column(type: "TEXT", nullable: true), + Status = table.Column(type: "INTEGER", nullable: false), + Error = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => _ = table.PrimaryKey("PK_TransferLogs", x => x.Id)); + + _ = migrationBuilder.CreateIndex( + name: "IX_DriveItems_DriveItemId", + table: "DriveItems", + column: "DriveItemId"); + + _ = migrationBuilder.CreateIndex( + name: "IX_LocalFiles_RelativePath", + table: "LocalFiles", + column: "RelativePath"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + _ = migrationBuilder.DropTable( + name: "DeltaTokens"); + + _ = migrationBuilder.DropTable( + name: "DriveItems"); + + _ = migrationBuilder.DropTable( + name: "LocalFiles"); + + _ = migrationBuilder.DropTable( + name: "TransferLogs"); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221055438_AddBytesTransferredToTransferLogs.Designer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221055438_AddBytesTransferredToTransferLogs.Designer.cs new file mode 100644 index 0000000..94d58b3 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221055438_AddBytesTransferredToTransferLogs.Designer.cs @@ -0,0 +1,144 @@ +// +using AStar.Dev.OneDrive.Client.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20251221055438_AddBytesTransferredToTransferLogs")] + partial class AddBytesTransferredToTransferLogs + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DeltaToken", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("LastSyncedUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastSyncedUtc_Ticks"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DeltaTokens", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DriveItemRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CTag") + .HasColumnType("TEXT"); + + b.Property("DriveItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ETag") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastModifiedUtc_Ticks"); + + b.Property("RelativePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DriveItemId"); + + b.ToTable("DriveItems", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.LocalFileRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + b.Property("LastWriteUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastWriteUtc_Ticks"); + + b.Property("RelativePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SyncState") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RelativePath"); + + b.ToTable("LocalFiles", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.TransferLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BytesTransferred") + .HasColumnType("INTEGER"); + + b.Property("CompletedUtc") + .HasColumnType("INTEGER") + .HasColumnName("CompletedUtc_Ticks"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedUtc") + .HasColumnType("INTEGER") + .HasColumnName("StartedUtc_Ticks"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("TransferLogs", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221055438_AddBytesTransferredToTransferLogs.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221055438_AddBytesTransferredToTransferLogs.cs new file mode 100644 index 0000000..3394070 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221055438_AddBytesTransferredToTransferLogs.cs @@ -0,0 +1,159 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Migrations; + +/// +public partial class AddBytesTransferredToTransferLogs : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + _ = migrationBuilder.RenameColumn( + name: "StartedUtc", + table: "TransferLogs", + newName: "StartedUtc_Ticks"); + + _ = migrationBuilder.RenameColumn( + name: "CompletedUtc", + table: "TransferLogs", + newName: "CompletedUtc_Ticks"); + + _ = migrationBuilder.RenameColumn( + name: "LastWriteUtc", + table: "LocalFiles", + newName: "LastWriteUtc_Ticks"); + + _ = migrationBuilder.RenameColumn( + name: "LastModifiedUtc", + table: "DriveItems", + newName: "LastModifiedUtc_Ticks"); + + _ = migrationBuilder.RenameColumn( + name: "LastSyncedUtc", + table: "DeltaTokens", + newName: "LastSyncedUtc_Ticks"); + + _ = migrationBuilder.AlterColumn( + name: "StartedUtc_Ticks", + table: "TransferLogs", + type: "INTEGER", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT"); + + _ = migrationBuilder.AlterColumn( + name: "CompletedUtc_Ticks", + table: "TransferLogs", + type: "INTEGER", + nullable: true, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT", + oldNullable: true); + + _ = migrationBuilder.AddColumn( + name: "BytesTransferred", + table: "TransferLogs", + type: "INTEGER", + nullable: true); + + _ = migrationBuilder.AlterColumn( + name: "LastWriteUtc_Ticks", + table: "LocalFiles", + type: "INTEGER", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT"); + + _ = migrationBuilder.AlterColumn( + name: "LastModifiedUtc_Ticks", + table: "DriveItems", + type: "INTEGER", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT"); + + _ = migrationBuilder.AlterColumn( + name: "LastSyncedUtc_Ticks", + table: "DeltaTokens", + type: "INTEGER", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "TEXT"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + _ = migrationBuilder.DropColumn( + name: "BytesTransferred", + table: "TransferLogs"); + + _ = migrationBuilder.RenameColumn( + name: "StartedUtc_Ticks", + table: "TransferLogs", + newName: "StartedUtc"); + + _ = migrationBuilder.RenameColumn( + name: "CompletedUtc_Ticks", + table: "TransferLogs", + newName: "CompletedUtc"); + + _ = migrationBuilder.RenameColumn( + name: "LastWriteUtc_Ticks", + table: "LocalFiles", + newName: "LastWriteUtc"); + + _ = migrationBuilder.RenameColumn( + name: "LastModifiedUtc_Ticks", + table: "DriveItems", + newName: "LastModifiedUtc"); + + _ = migrationBuilder.RenameColumn( + name: "LastSyncedUtc_Ticks", + table: "DeltaTokens", + newName: "LastSyncedUtc"); + + _ = migrationBuilder.AlterColumn( + name: "StartedUtc", + table: "TransferLogs", + type: "TEXT", + nullable: false, + oldClrType: typeof(long), + oldType: "INTEGER"); + + _ = migrationBuilder.AlterColumn( + name: "CompletedUtc", + table: "TransferLogs", + type: "TEXT", + nullable: true, + oldClrType: typeof(long), + oldType: "INTEGER", + oldNullable: true); + + _ = migrationBuilder.AlterColumn( + name: "LastWriteUtc", + table: "LocalFiles", + type: "TEXT", + nullable: false, + oldClrType: typeof(long), + oldType: "INTEGER"); + + _ = migrationBuilder.AlterColumn( + name: "LastModifiedUtc", + table: "DriveItems", + type: "TEXT", + nullable: false, + oldClrType: typeof(long), + oldType: "INTEGER"); + + _ = migrationBuilder.AlterColumn( + name: "LastSyncedUtc", + table: "DeltaTokens", + type: "TEXT", + nullable: false, + oldClrType: typeof(long), + oldType: "INTEGER"); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221085945_Test.Designer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221085945_Test.Designer.cs new file mode 100644 index 0000000..8a884c6 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221085945_Test.Designer.cs @@ -0,0 +1,144 @@ +// +using AStar.Dev.OneDrive.Client.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20251221085945_Test")] + partial class Test + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DeltaToken", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("LastSyncedUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastSyncedUtc_Ticks"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DeltaTokens", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DriveItemRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CTag") + .HasColumnType("TEXT"); + + b.Property("DriveItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ETag") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastModifiedUtc_Ticks"); + + b.Property("RelativePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DriveItemId"); + + b.ToTable("DriveItems", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.LocalFileRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + b.Property("LastWriteUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastWriteUtc_Ticks"); + + b.Property("RelativePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SyncState") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RelativePath"); + + b.ToTable("LocalFiles", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.TransferLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BytesTransferred") + .HasColumnType("INTEGER"); + + b.Property("CompletedUtc") + .HasColumnType("INTEGER") + .HasColumnName("CompletedUtc_Ticks"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedUtc") + .HasColumnType("INTEGER") + .HasColumnName("StartedUtc_Ticks"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("TransferLogs", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221085945_Test.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221085945_Test.cs new file mode 100644 index 0000000..f490dbb --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20251221085945_Test.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Migrations; + +/// +public partial class Test : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116201123_AdditionalTablesForConsumption.Designer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116201123_AdditionalTablesForConsumption.Designer.cs new file mode 100644 index 0000000..a7f0f63 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116201123_AdditionalTablesForConsumption.Designer.cs @@ -0,0 +1,370 @@ +// +using System; +using AStar.Dev.OneDrive.Client.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260116201123_AdditionalTablesForConsumption")] + partial class AdditionalTablesForConsumption + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.Account", b => + { + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("AutoSyncIntervalMinutes") + .HasColumnType("INTEGER"); + + b.Property("DeltaToken") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EnableDebugLogging") + .HasColumnType("INTEGER"); + + b.Property("EnableDetailedSyncLogging") + .HasColumnType("INTEGER"); + + b.Property("IsAuthenticated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncUtc") + .HasColumnType("TEXT"); + + b.Property("LocalSyncPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MaxItemsInBatch") + .HasColumnType("INTEGER"); + + b.Property("MaxParallelUpDownloads") + .HasColumnType("INTEGER"); + + b.HasKey("AccountId"); + + b.HasIndex("LocalSyncPath") + .IsUnique(); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DeltaToken", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("LastSyncedUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastSyncedUtc_Ticks"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DeltaTokens", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DriveItemRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CTag") + .HasColumnType("TEXT"); + + b.Property("DriveItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ETag") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastModifiedUtc_Ticks"); + + b.Property("RelativePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DriveItemId"); + + b.ToTable("DriveItems", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.FileMetadata", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CTag") + .HasColumnType("TEXT"); + + b.Property("ETag") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastSyncDirection") + .HasColumnType("INTEGER"); + + b.Property("LocalHash") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SyncStatus") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("AccountId", "Path"); + + b.ToTable("FileMetadata"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.LocalFileRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + b.Property("LastWriteUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastWriteUtc_Ticks"); + + b.Property("RelativePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SyncState") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RelativePath"); + + b.ToTable("LocalFiles", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId", "FolderPath"); + + b.ToTable("SyncConfiguration"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConflict", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DetectedUtc") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsResolved") + .HasColumnType("INTEGER"); + + b.Property("LocalModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LocalSize") + .HasColumnType("INTEGER"); + + b.Property("RemoteModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("RemoteSize") + .HasColumnType("INTEGER"); + + b.Property("ResolutionStrategy") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("AccountId", "IsResolved"); + + b.ToTable("SyncConflict"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.TransferLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BytesTransferred") + .HasColumnType("INTEGER"); + + b.Property("CompletedUtc") + .HasColumnType("INTEGER") + .HasColumnName("CompletedUtc_Ticks"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedUtc") + .HasColumnType("INTEGER") + .HasColumnName("StartedUtc_Ticks"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("TransferLogs", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.WindowPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(600.0); + + b.Property("IsMaximized") + .HasColumnType("INTEGER"); + + b.Property("Width") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(800.0); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("WindowPreferences"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.FileMetadata", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConfiguration", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConflict", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116201123_AdditionalTablesForConsumption.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116201123_AdditionalTablesForConsumption.cs new file mode 100644 index 0000000..a8a2c36 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116201123_AdditionalTablesForConsumption.cs @@ -0,0 +1,178 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Migrations +{ + /// + public partial class AdditionalTablesForConsumption : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Accounts", + columns: table => new + { + AccountId = table.Column(type: "TEXT", nullable: false), + DisplayName = table.Column(type: "TEXT", nullable: false), + LocalSyncPath = table.Column(type: "TEXT", nullable: false), + IsAuthenticated = table.Column(type: "INTEGER", nullable: false), + LastSyncUtc = table.Column(type: "TEXT", nullable: true), + DeltaToken = table.Column(type: "TEXT", nullable: true), + EnableDetailedSyncLogging = table.Column(type: "INTEGER", nullable: false), + EnableDebugLogging = table.Column(type: "INTEGER", nullable: false), + MaxParallelUpDownloads = table.Column(type: "INTEGER", nullable: false), + MaxItemsInBatch = table.Column(type: "INTEGER", nullable: false), + AutoSyncIntervalMinutes = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Accounts", x => x.AccountId); + }); + + migrationBuilder.CreateTable( + name: "WindowPreferences", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + X = table.Column(type: "REAL", nullable: true), + Y = table.Column(type: "REAL", nullable: true), + Width = table.Column(type: "REAL", nullable: false, defaultValue: 800.0), + Height = table.Column(type: "REAL", nullable: false, defaultValue: 600.0), + IsMaximized = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_WindowPreferences", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "FileMetadata", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + AccountId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Path = table.Column(type: "TEXT", nullable: false), + Size = table.Column(type: "INTEGER", nullable: false), + LastModifiedUtc = table.Column(type: "TEXT", nullable: false), + LocalPath = table.Column(type: "TEXT", nullable: false), + CTag = table.Column(type: "TEXT", nullable: true), + ETag = table.Column(type: "TEXT", nullable: true), + LocalHash = table.Column(type: "TEXT", nullable: true), + SyncStatus = table.Column(type: "INTEGER", nullable: false), + LastSyncDirection = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_FileMetadata", x => x.Id); + table.ForeignKey( + name: "FK_FileMetadata_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "SyncConfiguration", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AccountId = table.Column(type: "TEXT", nullable: false), + FolderPath = table.Column(type: "TEXT", nullable: false), + IsSelected = table.Column(type: "INTEGER", nullable: false), + LastModifiedUtc = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SyncConfiguration", x => x.Id); + table.ForeignKey( + name: "FK_SyncConfiguration_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "SyncConflict", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + AccountId = table.Column(type: "TEXT", nullable: false), + FilePath = table.Column(type: "TEXT", nullable: false), + LocalModifiedUtc = table.Column(type: "TEXT", nullable: false), + RemoteModifiedUtc = table.Column(type: "TEXT", nullable: false), + LocalSize = table.Column(type: "INTEGER", nullable: false), + RemoteSize = table.Column(type: "INTEGER", nullable: false), + DetectedUtc = table.Column(type: "TEXT", nullable: false), + ResolutionStrategy = table.Column(type: "INTEGER", nullable: false), + IsResolved = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SyncConflict", x => x.Id); + table.ForeignKey( + name: "FK_SyncConflict_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Accounts_LocalSyncPath", + table: "Accounts", + column: "LocalSyncPath", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_FileMetadata_AccountId", + table: "FileMetadata", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_FileMetadata_AccountId_Path", + table: "FileMetadata", + columns: new[] { "AccountId", "Path" }); + + migrationBuilder.CreateIndex( + name: "IX_SyncConfiguration_AccountId_FolderPath", + table: "SyncConfiguration", + columns: new[] { "AccountId", "FolderPath" }); + + migrationBuilder.CreateIndex( + name: "IX_SyncConflict_AccountId", + table: "SyncConflict", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_SyncConflict_AccountId_IsResolved", + table: "SyncConflict", + columns: new[] { "AccountId", "IsResolved" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "FileMetadata"); + + migrationBuilder.DropTable( + name: "SyncConfiguration"); + + migrationBuilder.DropTable( + name: "SyncConflict"); + + migrationBuilder.DropTable( + name: "WindowPreferences"); + + migrationBuilder.DropTable( + name: "Accounts"); + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116223008_RenameTokenValueToToken.Designer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116223008_RenameTokenValueToToken.Designer.cs new file mode 100644 index 0000000..a9b69f2 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116223008_RenameTokenValueToToken.Designer.cs @@ -0,0 +1,430 @@ +// +using System; +using AStar.Dev.OneDrive.Client.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260116223008_RenameTokenValueToToken")] + partial class RenameTokenValueToToken + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.1"); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.Account", b => + { + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("AutoSyncIntervalMinutes") + .HasColumnType("INTEGER"); + + b.Property("DeltaToken") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EnableDebugLogging") + .HasColumnType("INTEGER"); + + b.Property("EnableDetailedSyncLogging") + .HasColumnType("INTEGER"); + + b.Property("IsAuthenticated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncUtc") + .HasColumnType("TEXT"); + + b.Property("LocalSyncPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MaxItemsInBatch") + .HasColumnType("INTEGER"); + + b.Property("MaxParallelUpDownloads") + .HasColumnType("INTEGER"); + + b.HasKey("AccountId"); + + b.HasIndex("LocalSyncPath") + .IsUnique(); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DeltaToken", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastSyncedUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastSyncedUtc_Ticks"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("DeltaTokens", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DriveItemRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CTag") + .HasColumnType("TEXT"); + + b.Property("DriveItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ETag") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastModifiedUtc_Ticks"); + + b.Property("RelativePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("DriveItemId"); + + b.ToTable("DriveItems", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.FileMetadata", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CTag") + .HasColumnType("TEXT"); + + b.Property("ETag") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastSyncDirection") + .HasColumnType("INTEGER"); + + b.Property("LocalHash") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SyncStatus") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("AccountId", "Path"); + + b.ToTable("FileMetadata"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.LocalFileRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + b.Property("LastWriteUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastWriteUtc_Ticks"); + + b.Property("RelativePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SyncState") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("RelativePath"); + + b.ToTable("LocalFiles", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId", "FolderPath"); + + b.ToTable("SyncConfiguration"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConflict", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DetectedUtc") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsResolved") + .HasColumnType("INTEGER"); + + b.Property("LocalModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LocalSize") + .HasColumnType("INTEGER"); + + b.Property("RemoteModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("RemoteSize") + .HasColumnType("INTEGER"); + + b.Property("ResolutionStrategy") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("AccountId", "IsResolved"); + + b.ToTable("SyncConflict"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.TransferLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BytesTransferred") + .HasColumnType("INTEGER"); + + b.Property("CompletedUtc") + .HasColumnType("INTEGER") + .HasColumnName("CompletedUtc_Ticks"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedUtc") + .HasColumnType("INTEGER") + .HasColumnName("StartedUtc_Ticks"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("TransferLogs", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.WindowPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(600.0); + + b.Property("IsMaximized") + .HasColumnType("INTEGER"); + + b.Property("Width") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(800.0); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("WindowPreferences"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DeltaToken", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DriveItemRecord", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.FileMetadata", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.LocalFileRecord", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConfiguration", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConflict", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.TransferLog", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116223008_RenameTokenValueToToken.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116223008_RenameTokenValueToToken.cs new file mode 100644 index 0000000..c2931eb --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260116223008_RenameTokenValueToToken.cs @@ -0,0 +1,146 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Migrations +{ + /// + public partial class RenameTokenValueToToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AccountId", + table: "TransferLogs", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "AccountId", + table: "LocalFiles", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "AccountId", + table: "DriveItems", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "AccountId", + table: "DeltaTokens", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateIndex( + name: "IX_TransferLogs_AccountId", + table: "TransferLogs", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_LocalFiles_AccountId", + table: "LocalFiles", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_DriveItems_AccountId", + table: "DriveItems", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_DeltaTokens_AccountId", + table: "DeltaTokens", + column: "AccountId"); + + migrationBuilder.AddForeignKey( + name: "FK_DeltaTokens_Accounts_AccountId", + table: "DeltaTokens", + column: "AccountId", + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_DriveItems_Accounts_AccountId", + table: "DriveItems", + column: "AccountId", + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_LocalFiles_Accounts_AccountId", + table: "LocalFiles", + column: "AccountId", + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_TransferLogs_Accounts_AccountId", + table: "TransferLogs", + column: "AccountId", + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_DeltaTokens_Accounts_AccountId", + table: "DeltaTokens"); + + migrationBuilder.DropForeignKey( + name: "FK_DriveItems_Accounts_AccountId", + table: "DriveItems"); + + migrationBuilder.DropForeignKey( + name: "FK_LocalFiles_Accounts_AccountId", + table: "LocalFiles"); + + migrationBuilder.DropForeignKey( + name: "FK_TransferLogs_Accounts_AccountId", + table: "TransferLogs"); + + migrationBuilder.DropIndex( + name: "IX_TransferLogs_AccountId", + table: "TransferLogs"); + + migrationBuilder.DropIndex( + name: "IX_LocalFiles_AccountId", + table: "LocalFiles"); + + migrationBuilder.DropIndex( + name: "IX_DriveItems_AccountId", + table: "DriveItems"); + + migrationBuilder.DropIndex( + name: "IX_DeltaTokens_AccountId", + table: "DeltaTokens"); + + migrationBuilder.DropColumn( + name: "AccountId", + table: "TransferLogs"); + + migrationBuilder.DropColumn( + name: "AccountId", + table: "LocalFiles"); + + migrationBuilder.DropColumn( + name: "AccountId", + table: "DriveItems"); + + migrationBuilder.DropColumn( + name: "AccountId", + table: "DeltaTokens"); + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260118103718_V3Tables.Designer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260118103718_V3Tables.Designer.cs new file mode 100644 index 0000000..8e92ae6 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260118103718_V3Tables.Designer.cs @@ -0,0 +1,777 @@ +// +using System; +using AStar.Dev.OneDrive.Client.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260118103718_V3Tables")] + partial class V3Tables + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.2"); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.Account", b => + { + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("AutoSyncIntervalMinutes") + .HasColumnType("INTEGER"); + + b.Property("DeltaToken") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EnableDebugLogging") + .HasColumnType("INTEGER"); + + b.Property("EnableDetailedSyncLogging") + .HasColumnType("INTEGER"); + + b.Property("IsAuthenticated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncUtc") + .HasColumnType("TEXT"); + + b.Property("LocalSyncPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MaxItemsInBatch") + .HasColumnType("INTEGER"); + + b.Property("MaxParallelUpDownloads") + .HasColumnType("INTEGER"); + + b.HasKey("AccountId"); + + b.HasIndex("LocalSyncPath") + .IsUnique(); + + b.ToTable("Account"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.AccountEntity", b => + { + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("AutoSyncIntervalMinutes") + .HasColumnType("INTEGER"); + + b.Property("DeltaToken") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EnableDebugLogging") + .HasColumnType("INTEGER"); + + b.Property("EnableDetailedSyncLogging") + .HasColumnType("INTEGER"); + + b.Property("IsAuthenticated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncUtc") + .HasColumnType("TEXT"); + + b.Property("LocalSyncPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MaxItemsInBatch") + .HasColumnType("INTEGER"); + + b.Property("MaxParallelUpDownloads") + .HasColumnType("INTEGER"); + + b.HasKey("AccountId"); + + b.HasIndex("LocalSyncPath") + .IsUnique(); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DebugLogEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Exception") + .HasColumnType("TEXT"); + + b.Property("LogLevel") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Source") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TimestampUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DebugLogs"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DeltaToken", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastSyncedUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastSyncedUtc_Ticks"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("DeltaTokens", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DriveItemRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CTag") + .HasColumnType("TEXT"); + + b.Property("DriveItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ETag") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastModifiedUtc_Ticks"); + + b.Property("RelativePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("DriveItemId"); + + b.ToTable("DriveItems", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.FileMetadata", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CTag") + .HasColumnType("TEXT"); + + b.Property("ETag") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastSyncDirection") + .HasColumnType("INTEGER"); + + b.Property("LocalHash") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SyncStatus") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("AccountId", "Path"); + + b.ToTable("FileMetadata"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.FileMetadataEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CTag") + .HasColumnType("TEXT"); + + b.Property("ETag") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastSyncDirection") + .HasColumnType("INTEGER"); + + b.Property("LocalHash") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SyncStatus") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("AccountId", "Path"); + + b.ToTable("FileMetadataEntity", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.FileOperationLogEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LocalHash") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OneDriveId") + .HasColumnType("TEXT"); + + b.Property("Operation") + .HasColumnType("INTEGER"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RemoteHash") + .HasColumnType("TEXT"); + + b.Property("SyncSessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("FileOperationLogs"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.LocalFileRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + b.Property("LastWriteUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastWriteUtc_Ticks"); + + b.Property("RelativePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SyncState") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("RelativePath"); + + b.ToTable("LocalFiles", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId", "FolderPath"); + + b.ToTable("SyncConfiguration"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConfigurationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId", "FolderPath"); + + b.ToTable("SyncConfigurations"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConflict", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DetectedUtc") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsResolved") + .HasColumnType("INTEGER"); + + b.Property("LocalModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LocalSize") + .HasColumnType("INTEGER"); + + b.Property("RemoteModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("RemoteSize") + .HasColumnType("INTEGER"); + + b.Property("ResolutionStrategy") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("AccountId", "IsResolved"); + + b.ToTable("SyncConflict"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConflictEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DetectedUtc") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsResolved") + .HasColumnType("INTEGER"); + + b.Property("LocalModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LocalSize") + .HasColumnType("INTEGER"); + + b.Property("RemoteModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("RemoteSize") + .HasColumnType("INTEGER"); + + b.Property("ResolutionStrategy") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("AccountId", "IsResolved"); + + b.ToTable("SyncConflicts"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncSessionLogEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CompletedUtc") + .HasColumnType("TEXT"); + + b.Property("ConflictsDetected") + .HasColumnType("INTEGER"); + + b.Property("FilesDeleted") + .HasColumnType("INTEGER"); + + b.Property("FilesDownloaded") + .HasColumnType("INTEGER"); + + b.Property("FilesUploaded") + .HasColumnType("INTEGER"); + + b.Property("StartedUtc") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("TotalBytes") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SyncSessionLogs"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.TransferLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BytesTransferred") + .HasColumnType("INTEGER"); + + b.Property("CompletedUtc") + .HasColumnType("INTEGER") + .HasColumnName("CompletedUtc_Ticks"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedUtc") + .HasColumnType("INTEGER") + .HasColumnName("StartedUtc_Ticks"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("TransferLogs", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.WindowPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(600.0); + + b.Property("IsMaximized") + .HasColumnType("INTEGER"); + + b.Property("Width") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(800.0); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("WindowPreferences"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.WindowPreferencesEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(600.0); + + b.Property("IsMaximized") + .HasColumnType("INTEGER"); + + b.Property("Width") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(800.0); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("WindowPreferencesEntity", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DeltaToken", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DriveItemRecord", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.FileMetadata", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.FileMetadataEntity", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.AccountEntity", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.LocalFileRecord", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConfiguration", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConfigurationEntity", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.AccountEntity", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConflict", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConflictEntity", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.AccountEntity", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.TransferLog", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260118103718_V3Tables.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260118103718_V3Tables.cs new file mode 100644 index 0000000..403f7b5 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/20260118103718_V3Tables.cs @@ -0,0 +1,417 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Migrations +{ + /// + public partial class V3Tables : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_DeltaTokens_Accounts_AccountId", + table: "DeltaTokens"); + + migrationBuilder.DropForeignKey( + name: "FK_DriveItems_Accounts_AccountId", + table: "DriveItems"); + + migrationBuilder.DropForeignKey( + name: "FK_FileMetadata_Accounts_AccountId", + table: "FileMetadata"); + + migrationBuilder.DropForeignKey( + name: "FK_LocalFiles_Accounts_AccountId", + table: "LocalFiles"); + + migrationBuilder.DropForeignKey( + name: "FK_SyncConfiguration_Accounts_AccountId", + table: "SyncConfiguration"); + + migrationBuilder.DropForeignKey( + name: "FK_SyncConflict_Accounts_AccountId", + table: "SyncConflict"); + + migrationBuilder.DropForeignKey( + name: "FK_TransferLogs_Accounts_AccountId", + table: "TransferLogs"); + + migrationBuilder.CreateTable( + name: "Account", + columns: table => new + { + AccountId = table.Column(type: "TEXT", nullable: false), + DisplayName = table.Column(type: "TEXT", nullable: false), + LocalSyncPath = table.Column(type: "TEXT", nullable: false), + IsAuthenticated = table.Column(type: "INTEGER", nullable: false), + LastSyncUtc = table.Column(type: "TEXT", nullable: true), + DeltaToken = table.Column(type: "TEXT", nullable: true), + EnableDetailedSyncLogging = table.Column(type: "INTEGER", nullable: false), + EnableDebugLogging = table.Column(type: "INTEGER", nullable: false), + MaxParallelUpDownloads = table.Column(type: "INTEGER", nullable: false), + MaxItemsInBatch = table.Column(type: "INTEGER", nullable: false), + AutoSyncIntervalMinutes = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Account", x => x.AccountId); + }); + + migrationBuilder.CreateTable( + name: "DebugLogs", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AccountId = table.Column(type: "TEXT", nullable: false), + TimestampUtc = table.Column(type: "TEXT", nullable: false), + LogLevel = table.Column(type: "TEXT", nullable: false), + Source = table.Column(type: "TEXT", nullable: false), + Message = table.Column(type: "TEXT", nullable: false), + Exception = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DebugLogs", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "FileMetadataEntity", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + AccountId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Path = table.Column(type: "TEXT", nullable: false), + Size = table.Column(type: "INTEGER", nullable: false), + LastModifiedUtc = table.Column(type: "TEXT", nullable: false), + LocalPath = table.Column(type: "TEXT", nullable: false), + CTag = table.Column(type: "TEXT", nullable: true), + ETag = table.Column(type: "TEXT", nullable: true), + LocalHash = table.Column(type: "TEXT", nullable: true), + SyncStatus = table.Column(type: "INTEGER", nullable: false), + LastSyncDirection = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_FileMetadataEntity", x => x.Id); + table.ForeignKey( + name: "FK_FileMetadataEntity_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "FileOperationLogs", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + SyncSessionId = table.Column(type: "TEXT", nullable: false), + AccountId = table.Column(type: "TEXT", nullable: false), + Timestamp = table.Column(type: "TEXT", nullable: false), + Operation = table.Column(type: "INTEGER", nullable: false), + FilePath = table.Column(type: "TEXT", nullable: false), + LocalPath = table.Column(type: "TEXT", nullable: false), + OneDriveId = table.Column(type: "TEXT", nullable: true), + FileSize = table.Column(type: "INTEGER", nullable: false), + LocalHash = table.Column(type: "TEXT", nullable: true), + RemoteHash = table.Column(type: "TEXT", nullable: true), + LastModifiedUtc = table.Column(type: "TEXT", nullable: false), + Reason = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FileOperationLogs", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "SyncConfigurations", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AccountId = table.Column(type: "TEXT", nullable: false), + FolderPath = table.Column(type: "TEXT", nullable: false), + IsSelected = table.Column(type: "INTEGER", nullable: false), + LastModifiedUtc = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SyncConfigurations", x => x.Id); + table.ForeignKey( + name: "FK_SyncConfigurations_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "SyncConflicts", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + AccountId = table.Column(type: "TEXT", nullable: false), + FilePath = table.Column(type: "TEXT", nullable: false), + LocalModifiedUtc = table.Column(type: "TEXT", nullable: false), + RemoteModifiedUtc = table.Column(type: "TEXT", nullable: false), + LocalSize = table.Column(type: "INTEGER", nullable: false), + RemoteSize = table.Column(type: "INTEGER", nullable: false), + DetectedUtc = table.Column(type: "TEXT", nullable: false), + ResolutionStrategy = table.Column(type: "INTEGER", nullable: false), + IsResolved = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SyncConflicts", x => x.Id); + table.ForeignKey( + name: "FK_SyncConflicts_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "SyncSessionLogs", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + AccountId = table.Column(type: "TEXT", nullable: false), + StartedUtc = table.Column(type: "TEXT", nullable: false), + CompletedUtc = table.Column(type: "TEXT", nullable: true), + Status = table.Column(type: "INTEGER", nullable: false), + FilesUploaded = table.Column(type: "INTEGER", nullable: false), + FilesDownloaded = table.Column(type: "INTEGER", nullable: false), + FilesDeleted = table.Column(type: "INTEGER", nullable: false), + ConflictsDetected = table.Column(type: "INTEGER", nullable: false), + TotalBytes = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SyncSessionLogs", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "WindowPreferencesEntity", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + X = table.Column(type: "REAL", nullable: true), + Y = table.Column(type: "REAL", nullable: true), + Width = table.Column(type: "REAL", nullable: false, defaultValue: 800.0), + Height = table.Column(type: "REAL", nullable: false, defaultValue: 600.0), + IsMaximized = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_WindowPreferencesEntity", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Account_LocalSyncPath", + table: "Account", + column: "LocalSyncPath", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_FileMetadataEntity_AccountId", + table: "FileMetadataEntity", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_FileMetadataEntity_AccountId_Path", + table: "FileMetadataEntity", + columns: new[] { "AccountId", "Path" }); + + migrationBuilder.CreateIndex( + name: "IX_SyncConfigurations_AccountId_FolderPath", + table: "SyncConfigurations", + columns: new[] { "AccountId", "FolderPath" }); + + migrationBuilder.CreateIndex( + name: "IX_SyncConflicts_AccountId", + table: "SyncConflicts", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_SyncConflicts_AccountId_IsResolved", + table: "SyncConflicts", + columns: new[] { "AccountId", "IsResolved" }); + + migrationBuilder.AddForeignKey( + name: "FK_DeltaTokens_Account_AccountId", + table: "DeltaTokens", + column: "AccountId", + principalTable: "Account", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_DriveItems_Account_AccountId", + table: "DriveItems", + column: "AccountId", + principalTable: "Account", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_FileMetadata_Account_AccountId", + table: "FileMetadata", + column: "AccountId", + principalTable: "Account", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_LocalFiles_Account_AccountId", + table: "LocalFiles", + column: "AccountId", + principalTable: "Account", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_SyncConfiguration_Account_AccountId", + table: "SyncConfiguration", + column: "AccountId", + principalTable: "Account", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_SyncConflict_Account_AccountId", + table: "SyncConflict", + column: "AccountId", + principalTable: "Account", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_TransferLogs_Account_AccountId", + table: "TransferLogs", + column: "AccountId", + principalTable: "Account", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_DeltaTokens_Account_AccountId", + table: "DeltaTokens"); + + migrationBuilder.DropForeignKey( + name: "FK_DriveItems_Account_AccountId", + table: "DriveItems"); + + migrationBuilder.DropForeignKey( + name: "FK_FileMetadata_Account_AccountId", + table: "FileMetadata"); + + migrationBuilder.DropForeignKey( + name: "FK_LocalFiles_Account_AccountId", + table: "LocalFiles"); + + migrationBuilder.DropForeignKey( + name: "FK_SyncConfiguration_Account_AccountId", + table: "SyncConfiguration"); + + migrationBuilder.DropForeignKey( + name: "FK_SyncConflict_Account_AccountId", + table: "SyncConflict"); + + migrationBuilder.DropForeignKey( + name: "FK_TransferLogs_Account_AccountId", + table: "TransferLogs"); + + migrationBuilder.DropTable( + name: "Account"); + + migrationBuilder.DropTable( + name: "DebugLogs"); + + migrationBuilder.DropTable( + name: "FileMetadataEntity"); + + migrationBuilder.DropTable( + name: "FileOperationLogs"); + + migrationBuilder.DropTable( + name: "SyncConfigurations"); + + migrationBuilder.DropTable( + name: "SyncConflicts"); + + migrationBuilder.DropTable( + name: "SyncSessionLogs"); + + migrationBuilder.DropTable( + name: "WindowPreferencesEntity"); + + migrationBuilder.AddForeignKey( + name: "FK_DeltaTokens_Accounts_AccountId", + table: "DeltaTokens", + column: "AccountId", + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_DriveItems_Accounts_AccountId", + table: "DriveItems", + column: "AccountId", + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_FileMetadata_Accounts_AccountId", + table: "FileMetadata", + column: "AccountId", + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_LocalFiles_Accounts_AccountId", + table: "LocalFiles", + column: "AccountId", + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_SyncConfiguration_Accounts_AccountId", + table: "SyncConfiguration", + column: "AccountId", + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_SyncConflict_Accounts_AccountId", + table: "SyncConflict", + column: "AccountId", + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_TransferLogs_Accounts_AccountId", + table: "TransferLogs", + column: "AccountId", + principalTable: "Accounts", + principalColumn: "AccountId", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..47f912e --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,774 @@ +// +using System; +using AStar.Dev.OneDrive.Client.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AStar.Dev.OneDrive.Client.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.2"); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.Account", b => + { + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("AutoSyncIntervalMinutes") + .HasColumnType("INTEGER"); + + b.Property("DeltaToken") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EnableDebugLogging") + .HasColumnType("INTEGER"); + + b.Property("EnableDetailedSyncLogging") + .HasColumnType("INTEGER"); + + b.Property("IsAuthenticated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncUtc") + .HasColumnType("TEXT"); + + b.Property("LocalSyncPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MaxItemsInBatch") + .HasColumnType("INTEGER"); + + b.Property("MaxParallelUpDownloads") + .HasColumnType("INTEGER"); + + b.HasKey("AccountId"); + + b.HasIndex("LocalSyncPath") + .IsUnique(); + + b.ToTable("Account"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.AccountEntity", b => + { + b.Property("AccountId") + .HasColumnType("TEXT"); + + b.Property("AutoSyncIntervalMinutes") + .HasColumnType("INTEGER"); + + b.Property("DeltaToken") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EnableDebugLogging") + .HasColumnType("INTEGER"); + + b.Property("EnableDetailedSyncLogging") + .HasColumnType("INTEGER"); + + b.Property("IsAuthenticated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncUtc") + .HasColumnType("TEXT"); + + b.Property("LocalSyncPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MaxItemsInBatch") + .HasColumnType("INTEGER"); + + b.Property("MaxParallelUpDownloads") + .HasColumnType("INTEGER"); + + b.HasKey("AccountId"); + + b.HasIndex("LocalSyncPath") + .IsUnique(); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DebugLogEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Exception") + .HasColumnType("TEXT"); + + b.Property("LogLevel") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Source") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TimestampUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DebugLogs"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DeltaToken", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastSyncedUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastSyncedUtc_Ticks"); + + b.Property("Token") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("DeltaTokens", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DriveItemRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CTag") + .HasColumnType("TEXT"); + + b.Property("DriveItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ETag") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastModifiedUtc_Ticks"); + + b.Property("RelativePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("DriveItemId"); + + b.ToTable("DriveItems", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.FileMetadata", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CTag") + .HasColumnType("TEXT"); + + b.Property("ETag") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastSyncDirection") + .HasColumnType("INTEGER"); + + b.Property("LocalHash") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SyncStatus") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("AccountId", "Path"); + + b.ToTable("FileMetadata"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.FileMetadataEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CTag") + .HasColumnType("TEXT"); + + b.Property("ETag") + .HasColumnType("TEXT"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LastSyncDirection") + .HasColumnType("INTEGER"); + + b.Property("LocalHash") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SyncStatus") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("AccountId", "Path"); + + b.ToTable("FileMetadataEntity", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.FileOperationLogEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LocalHash") + .HasColumnType("TEXT"); + + b.Property("LocalPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OneDriveId") + .HasColumnType("TEXT"); + + b.Property("Operation") + .HasColumnType("INTEGER"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RemoteHash") + .HasColumnType("TEXT"); + + b.Property("SyncSessionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("FileOperationLogs"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.LocalFileRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + b.Property("LastWriteUtc") + .HasColumnType("INTEGER") + .HasColumnName("LastWriteUtc_Ticks"); + + b.Property("RelativePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SyncState") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("RelativePath"); + + b.ToTable("LocalFiles", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId", "FolderPath"); + + b.ToTable("SyncConfiguration"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConfigurationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsSelected") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUtc") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccountId", "FolderPath"); + + b.ToTable("SyncConfigurations"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConflict", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DetectedUtc") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsResolved") + .HasColumnType("INTEGER"); + + b.Property("LocalModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LocalSize") + .HasColumnType("INTEGER"); + + b.Property("RemoteModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("RemoteSize") + .HasColumnType("INTEGER"); + + b.Property("ResolutionStrategy") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("AccountId", "IsResolved"); + + b.ToTable("SyncConflict"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConflictEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DetectedUtc") + .HasColumnType("TEXT"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsResolved") + .HasColumnType("INTEGER"); + + b.Property("LocalModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("LocalSize") + .HasColumnType("INTEGER"); + + b.Property("RemoteModifiedUtc") + .HasColumnType("TEXT"); + + b.Property("RemoteSize") + .HasColumnType("INTEGER"); + + b.Property("ResolutionStrategy") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.HasIndex("AccountId", "IsResolved"); + + b.ToTable("SyncConflicts"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncSessionLogEntity", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CompletedUtc") + .HasColumnType("TEXT"); + + b.Property("ConflictsDetected") + .HasColumnType("INTEGER"); + + b.Property("FilesDeleted") + .HasColumnType("INTEGER"); + + b.Property("FilesDownloaded") + .HasColumnType("INTEGER"); + + b.Property("FilesUploaded") + .HasColumnType("INTEGER"); + + b.Property("StartedUtc") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("TotalBytes") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("SyncSessionLogs"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.TransferLog", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BytesTransferred") + .HasColumnType("INTEGER"); + + b.Property("CompletedUtc") + .HasColumnType("INTEGER") + .HasColumnName("CompletedUtc_Ticks"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedUtc") + .HasColumnType("INTEGER") + .HasColumnName("StartedUtc_Ticks"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("TransferLogs", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.WindowPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(600.0); + + b.Property("IsMaximized") + .HasColumnType("INTEGER"); + + b.Property("Width") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(800.0); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("WindowPreferences"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.WindowPreferencesEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(600.0); + + b.Property("IsMaximized") + .HasColumnType("INTEGER"); + + b.Property("Width") + .ValueGeneratedOnAdd() + .HasColumnType("REAL") + .HasDefaultValue(800.0); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("WindowPreferencesEntity", (string)null); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DeltaToken", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.DriveItemRecord", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.FileMetadata", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.FileMetadataEntity", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.AccountEntity", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.LocalFileRecord", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConfiguration", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConfigurationEntity", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.AccountEntity", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConflict", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.SyncConflictEntity", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.AccountEntity", "Account") + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("AStar.Dev.OneDrive.Client.Core.Entities.TransferLog", b => + { + b.HasOne("AStar.Dev.OneDrive.Client.Core.Entities.Account", null) + .WithMany() + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/AStar.Dev.OneDrive.Client.Services.csproj b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/AStar.Dev.OneDrive.Client.Services.csproj new file mode 100644 index 0000000..156b1fa --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/AStar.Dev.OneDrive.Client.Services.csproj @@ -0,0 +1,31 @@ + + + net10.0 + enable + enable + preview + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ChannelFactory.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ChannelFactory.cs new file mode 100644 index 0000000..57c853a --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ChannelFactory.cs @@ -0,0 +1,24 @@ +using System.Threading.Channels; +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Services; + +/// +public class ChannelFactory : IChannelFactory +{ + /// + public Channel CreateBoundedDriveItemRecord(int capacity) + => Channel.CreateBounded( + new BoundedChannelOptions(capacity) + { + FullMode = BoundedChannelFullMode.Wait + }); + + /// + public Channel CreateBoundedLocalFileRecord(int capacity) + => Channel.CreateBounded( + new BoundedChannelOptions(capacity) + { + FullMode = BoundedChannelFullMode.Wait + }); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/AppPathHelper.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/AppPathHelper.cs new file mode 100644 index 0000000..d05ff42 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/AppPathHelper.cs @@ -0,0 +1,55 @@ +using System.Runtime.InteropServices; + +namespace AStar.Dev.OneDrive.Client.Services.ConfigurationSettings; + +/// +/// Provides helper methods for determining application-specific data paths +/// across different operating systems. The class ensures that the application +/// data path is appropriately resolved based on the platform (Windows, macOS, or Linux). +/// +public static class AppPathHelper +{ + /// + /// Resolves the application data directory path for the specified application name + /// based on the current operating system. This ensures that application-specific + /// data can be stored in standard locations supported by the platform. + /// + /// The name of the application whose data path needs to be resolved. + /// + /// A string representing the full path to the application data directory specific + /// to the provided application name. It varies based on the operating system: + /// Windows: "AppData\Roaming\ + /// + /// " + /// macOS: "Library/Application Support/ + /// + /// " + /// Linux/Unix: "~/.config/" + /// + public static string GetAppDataPath(string appName) + { + if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var baseDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + return Path.Combine(baseDir, appName); + } + + if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + return Path.Combine(homeDir, "Library", "Application Support", appName); + } + else // Linux and other Unix + { + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + return Path.Combine(homeDir, ".config", appName); + } + } + + /// + /// Gets the user's OS-specific home folder path. + /// + /// The full path to the user's home directory. + public static string GetUserHomeFolder() + => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/ApplicationSettings.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/ApplicationSettings.cs new file mode 100644 index 0000000..e250f89 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/ApplicationSettings.cs @@ -0,0 +1,134 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using AStar.Dev.Source.Generators.Attributes; + +namespace AStar.Dev.OneDrive.Client.Services.ConfigurationSettings; + +/// +/// Represents the application settings used for configuring the OneDrive client. +/// Provides properties to define various configuration parameters such as client identifiers, +/// download preferences, caching, paths, and scope definitions. +/// +[AutoRegisterOptions] +public partial class ApplicationSettings +{ + /// + /// The configuration section name for application settings. + /// + public const string SectionName = "AStarDevOneDriveClient"; + + /// + /// Gets or sets the cache tag value used to manage the token cache serialization + /// and rotation mechanism for the OneDrive client. This property determines the + /// version of the cache file being utilized, ensuring isolation and preventing + /// conflicts when the cache is refreshed or rotated. + /// + [Required] + [Range(1, 100, ErrorMessage = "CacheTag must be greater than 0.")] + public int CacheTag { get; set; } = 1; + + /// + /// Gets or sets the version of the application. This value is used to + /// indicate the current release version of the software, often employed + /// for logging, user-facing information, or compatibility checks. + /// + [Required] + [RegularExpression(@"^\d+\.\d+\.\d+(-[A-Za-z0-9]+)?$", ErrorMessage = "ApplicationVersion must follow semantic versioning (e.g., 1.0.0 or 1.0.0-beta).")] + public string ApplicationVersion { get; set; } = "1.0.0"; + + /// + /// Gets or sets the user preferences path. This property is used to define + /// the directory where user preferences are stored. + /// + [Required] + public string UserPreferencesPath { get; set; } = string.Empty; + + /// + /// Gets or sets the user preferences file name. This property is used to define + /// the name of the file where user preferences are stored. + /// + [Required] + public string UserPreferencesFile { get; set; } = "user-preferences.json"; + + /// + /// Gets or sets the name of the database file used for synchronization. + /// + [Required] + public string DatabaseName { get; set; } = "onedrive-sync.db"; + + /// + /// Gets or sets the root path for OneDrive storage. This property is used to define + /// the base directory where OneDrive files are stored and accessed by the client. + /// + [Required] + public string OneDriveRootDirectory { get; set; } = "OneDrive-Sync"; + + /// + /// Gets or sets the cache prefix used for naming cached items related to the OneDrive client. + /// + [Required] + public string CachePrefix { get; set; } = string.Empty; + + /// + /// Gets or sets the URI to which the authentication response is redirected. + /// + /// The redirect URI must be a valid absolute URI. This property is typically used in OAuth or + /// OpenID Connect authentication flows to specify where the authorization server should send the user after + /// authentication. Ensure that the value matches the redirect URI registered with the authentication + /// provider. + [Required] + [RegularExpression(@"^https?://.+", ErrorMessage = "RedirectUri must be a valid absolute URI starting with http:// or https://")] + public string RedirectUri { get; set; } = "http://localhost"; + + /// + /// Gets or sets the URI of the Microsoft Graph endpoint to use for API requests. + /// + /// The default value targets the signed-in user's OneDrive. Set this property to specify a + /// different Microsoft Graph resource or version as needed. + [Required] + public string GraphUri { get; set; } = "https://graph.microsoft.com/v1.0/me/drive"; + + /// + /// Gets the full path to the user preferences file, combining the base user preferences path + /// with the user preferences file name. This property is used to locate the specific file + /// where user preferences are stored. + /// + [JsonIgnore] + public string FullUserPreferencesPath + => Path.Combine(FullUserPreferencesDirectory, UserPreferencesFile); + + /// + /// Gets the full path to the directory where user preferences are stored for the application. + /// + /// The returned path is based on the application's name and the current user's profile. The + /// directory may not exist until it is created by the application. + [JsonIgnore] + public static string FullUserPreferencesDirectory + => Path.Combine(AppPathHelper.GetAppDataPath(ApplicationName)); + + /// + /// Gets the full file system path to the database, including the directory and file name. + /// + [JsonIgnore] + public string FullDatabasePath + => Path.Combine(FullDatabaseDirectory, DatabaseName); + + /// + /// Gets the full file system path to the application's database directory. + /// + /// The returned path is constructed by combining the application's data directory with the + /// "database" subdirectory. This property is intended for use when accessing or managing files within the + /// application's database folder. + [JsonIgnore] + public static string FullDatabaseDirectory + => Path.Combine(AppPathHelper.GetAppDataPath(ApplicationName), "database"); + + /// + /// Gets the full file system path to the user's OneDrive synchronization directory. + /// + [JsonIgnore] + public string FullUserSyncPath + => Path.Combine(AppPathHelper.GetUserHomeFolder(), OneDriveRootDirectory); + + private static readonly string ApplicationName = "astar-dev-onedrive-client"; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/EntraIdSettings.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/EntraIdSettings.cs new file mode 100644 index 0000000..8f98231 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/EntraIdSettings.cs @@ -0,0 +1,39 @@ +using System.ComponentModel.DataAnnotations; +using AStar.Dev.Source.Generators.Attributes; + +namespace AStar.Dev.OneDrive.Client.Services.ConfigurationSettings; + +/// +/// Represents the Entra ID settings used for configuring the OneDrive client. +/// Provides properties to define various configuration parameters such as client identifiers, +/// download preferences, caching, paths, and scope definitions. +/// +[AutoRegisterOptions] +public partial class EntraIdSettings +{ + /// + /// The configuration section name for Entra ID settings. + /// + public const string SectionName = "EntraId"; + + /// + /// Gets or sets the client identifier used to authenticate the application + /// with the OneDrive API and related services. This value is required for + /// configuring the application to interact with the Microsoft Graph API. + /// + [Required] + public string ClientId { get; set; } = string.Empty; + + /// + /// Gets or sets the scopes that the application requires access to. + /// + [Required] + public string[] Scopes { get; set; } = []; + + /// + /// Gets the URI to which the authentication response will be redirected. + /// + [Required] + [RegularExpression(@"^https?://.+", ErrorMessage = "RedirectUri must be a valid URI starting with http:// or https://")] + public string RedirectUri { get; set; } = string.Empty; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/FileServices.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/FileServices.cs new file mode 100644 index 0000000..f51ae0b --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/FileServices.cs @@ -0,0 +1,8 @@ +using System.IO.Abstractions; + +namespace AStar.Dev.OneDrive.Client.Services.ConfigurationSettings; + +public class FileServices(IFileSystem fileSystem) +{ + public string GetFileContents(string path) => fileSystem.File.ReadAllText(path); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/UiSettings.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/UiSettings.cs new file mode 100644 index 0000000..be9c7a9 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/UiSettings.cs @@ -0,0 +1,101 @@ +using System.ComponentModel.DataAnnotations; + +namespace AStar.Dev.OneDrive.Client.Services.ConfigurationSettings; + +/// +/// Represents the UI settings configured by the user. +/// It includes options for managing application behavior, appearance, and state persistence. +/// +public class UiSettings +{ + /// + /// Gets or sets a value indicating whether files should be automatically downloaded + /// after the synchronization process completes. + /// + /// + /// When set to true, the application will attempt to download files immediately + /// following a successful sync operation. This setting is primarily intended to enhance the + /// user experience by ensuring that the most recent versions of files are readily available locally. + /// The behavior of this property can be configured through user preferences. + /// + public bool DownloadFilesAfterSync { get; set; } + + /// + /// Gets or sets a value indicating whether files should be automatically uploaded + /// after the synchronization process completes. + /// + /// + /// When set to true, the application will attempt to upload files immediately + /// following a successful sync operation. This setting is designed to ensure that local changes + /// are promptly reflected in the cloud storage, maintaining up-to-date versions of files. + /// Users can configure this behavior according to their preferences for seamless file updates. + /// + public bool UploadFilesAfterSync { get; set; } + + /// + /// Gets or sets a value indicating whether the application should retain the user's authentication state across sessions. + /// + /// + /// When set to true, the user will remain signed in even after restarting the application, + /// provided that the authentication token remains valid. This property is often used to enhance + /// user convenience by avoiding repeated sign-in prompts. If false, the user will be signed out + /// at the end of the session. + /// + public bool RememberMe { get; set; } = true; + + /// + /// Gets or sets the UI theme preference selected by the user. + /// + /// + /// This property determines the visual appearance of the application's user interface. + /// It supports values such as "Light", "Dark", and "Auto", where "Auto" allows the theme + /// to dynamically adapt based on the system's theme settings. + /// The selected theme is applied throughout the application and can be modified via user preferences. + /// + [Required] + [RegularExpression("Light|Dark|Auto", ErrorMessage = "Theme must be 'Light', 'Dark', or 'Auto'.")] + public string Theme { get; set; } = "Auto"; + + /// + /// Gets or sets the description of the most recent action performed by the user or system. + /// + /// + /// This property is intended to store and reflect the last significant action taken within + /// the application, such as a synchronization, data upload, or UI interaction. It provides + /// context for the user or system regarding recent events and can be used to update status + /// displays within the UI. + /// Defaults to "No action yet" when no actions have been recorded. + /// + [Required] + public string LastAction { get; set; } = "No action yet"; + + /// + /// Gets or sets the synchronization settings related to file transfers and sync behavior. + /// + [Required] + public SyncSettings SyncSettings { get; set; } = new(); + + /// + /// Updates the current instance of with values from another + /// instance. + /// + /// + /// The instance of whose values + /// will be copied to the current instance. + /// + /// Returns the updated instance of . + public UiSettings Update(UiSettings other) + { + SyncSettings.DownloadBatchSize = other.SyncSettings.DownloadBatchSize; + SyncSettings.MaxParallelDownloads = other.SyncSettings.MaxParallelDownloads; + SyncSettings.MaxRetries = other.SyncSettings.MaxRetries; + SyncSettings.RetryBaseDelayMs = other.SyncSettings.RetryBaseDelayMs; + LastAction = other.LastAction; + Theme = other.Theme; + RememberMe = other.RememberMe; + DownloadFilesAfterSync = other.DownloadFilesAfterSync; + UploadFilesAfterSync = other.UploadFilesAfterSync; + + return this; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/UserPreferences.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/UserPreferences.cs new file mode 100644 index 0000000..317c917 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/UserPreferences.cs @@ -0,0 +1,54 @@ +namespace AStar.Dev.OneDrive.Client.Services.ConfigurationSettings; + +/// +/// Represents the preferences of a user, including settings related to the application window +/// and the user interface. This class serves as a container for various user-configurable +/// settings that inform application behavior and UI presentation. +/// +public class UserPreferences +{ + /// + /// Represents the configuration settings for an application window, including its dimensions + /// and position on the screen. These settings are used to define and persist the window's + /// size and location between application sessions. + /// Properties defined in the class include: + /// - WindowWidth: Specifies the width of the window. + /// - WindowHeight: Specifies the height of the window. + /// - WindowX: Specifies the X-coordinate of the window's top-left corner on the screen. + /// - WindowY: Specifies the Y-coordinate of the window's top-left corner on the screen. + /// This class is typically used in conjunction with user preferences to store and restore the + /// window's state, ensuring a consistent user experience. + /// + public WindowSettings WindowSettings { get; set; } = new(); + + /// + /// Represents the configuration settings related to the user interface. This class provides + /// a container for preferences and settings that define how the user interface behaves and appears. + /// These settings can influence aspects such as the theme, display preferences, and other + /// UI-related customizations. + /// Properties contained within the class may include: + /// - Theme: Determines the appearance of the application, such as "Light" or "Dark" mode. + /// - DownloadFilesAfterSync: Specifies whether files should be downloaded automatically + /// after synchronization is completed. + /// - RememberMe: Indicates whether to persist the user session for future application use. + /// This class is typically used to store and retrieve UI-related preferences, enabling the + /// application to provide a personalized and consistent user experience. + /// + public UiSettings UiSettings { get; set; } = new(); + + /// + /// Updates the current instance of with values from another + /// instance. + /// + /// + /// The instance of whose values + /// will be copied to the current instance. + /// + /// Returns the updated instance of . + public UserPreferences Update(UserPreferences other) + { + UiSettings = UiSettings.Update(other.UiSettings); + WindowSettings = WindowSettings.Update(other.WindowSettings); + return this; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/WindowSettings.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/WindowSettings.cs new file mode 100644 index 0000000..382c559 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ConfigurationSettings/WindowSettings.cs @@ -0,0 +1,60 @@ +using Avalonia; + +namespace AStar.Dev.OneDrive.Client.Services.ConfigurationSettings; + +/// +/// Represents settings related to the configuration and positioning of a window. +/// +public class WindowSettings +{ + /// + /// Gets or sets the width of the window in pixels. + /// This property defines or persists the horizontal size of the application window. + /// + public double WindowWidth { get; set; } = 1000; + + /// + /// Gets or sets the height of the window in pixels. + /// This property helps to define or persist the vertical size of the application window. + /// + public double WindowHeight { get; set; } = 800; + + /// + /// Represents the horizontal position of the window relative to the screen. + /// + public int WindowX { get; set; } = 100; + + /// + /// Gets or sets the Y-coordinate of the window position. + /// + public int WindowY { get; set; } = 100; + + /// + /// Updates the current instance of with values from another + /// instance. + /// + /// + /// The instance of whose values + /// will be copied to the current instance. + /// + /// Returns the updated instance of . + public WindowSettings Update(WindowSettings other) + { + WindowHeight = other.WindowHeight; + WindowWidth = other.WindowWidth; + WindowX = other.WindowX; + WindowY = other.WindowY; + + return this; + } + + public WindowSettings Update(PixelPoint windowPosition, double windowWidth, double windowHeight) + { + WindowHeight = windowHeight; + WindowWidth = windowWidth; + WindowX = windowPosition.X; + WindowY = windowPosition.Y; + + return this; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DeltaPageProcessor.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DeltaPageProcessor.cs new file mode 100644 index 0000000..b20e1f6 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DeltaPageProcessor.cs @@ -0,0 +1,114 @@ +using AStar.Dev.OneDrive.Client.Core.Dtos; +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Interfaces; +using Microsoft.Extensions.Logging; + +namespace AStar.Dev.OneDrive.Client.Services; + +/// +public class DeltaPageProcessor(ISyncRepository repo, IGraphClient graph, ILogger logger) : IDeltaPageProcessor +{ + + /// + public async Task<(string? finalDelta, int pageCount, int totalItemsProcessed)> ProcessAllDeltaPagesAsync(string accountId, CancellationToken cancellationToken) + { + logger.LogInformation("[DeltaPageProcessor] Starting delta page processing"); + string? nextOrDelta = null, finalDelta = null; + int pageCount = 0, totalItemsProcessed = 0; + try + { + do + { + logger.LogDebug("[DeltaPageProcessor] Requesting delta page: nextOrDelta={NextOrDelta}", nextOrDelta); + DeltaPage page = await graph.GetDriveDeltaPageAsync(accountId, nextOrDelta, cancellationToken); + logger.LogDebug("[DeltaPageProcessor] Received page: items={Count} nextLink={NextLink} deltaLink={DeltaLink}", page.Items.Count(), page.NextLink, page.DeltaLink); + await repo.ApplyDriveItemsAsync(accountId, page.Items, cancellationToken); + totalItemsProcessed += page.Items.Count(); + nextOrDelta = page.NextLink; + finalDelta = page.DeltaLink ?? finalDelta; + pageCount++; + logger.LogInformation("[DeltaPageProcessor] Applied page {PageNum}: items={Count} totalItems={Total} next={Next}", + pageCount, page.Items.Count(), totalItemsProcessed, page.NextLink is not null); + if(pageCount > 10000) + { + logger.LogWarning("[DeltaPageProcessor] Exceeded max page count (10000), aborting to prevent infinite loop."); + break; + } + } while(!string.IsNullOrEmpty(nextOrDelta) && !cancellationToken.IsCancellationRequested); + logger.LogInformation("[DeltaPageProcessor] Delta processing complete: finalDelta={FinalDelta} pageCount={PageCount} totalItems={TotalItems}", finalDelta, pageCount, totalItemsProcessed); + } + catch(Exception ex) + { + logger.LogError(ex, "[DeltaPageProcessor] Exception during delta processing: {Message}", ex.Message); + throw new IOException("Error processing delta pages", ex); + } + + return (finalDelta, pageCount, totalItemsProcessed); + } + + /// + public async Task<(DeltaToken finalDelta, int pageCount, int totalItemsProcessed)> ProcessAllDeltaPagesAsync(string accountId, DeltaToken deltaToken, CancellationToken cancellationToken, Action? progressCallback) + { + logger.LogInformation("[DeltaPageProcessor] Starting delta page processing (with progress callback)"); + string? nextOrDelta = null; + DeltaToken finalToken = deltaToken; + int pageCount = 0, totalItemsProcessed = 0; + try + { + do + { + logger.LogDebug("[DeltaPageProcessor] Requesting delta page: nextOrDelta={NextOrDelta}", nextOrDelta); + DeltaPage page = await graph.GetDriveDeltaPageAsync(accountId, nextOrDelta, cancellationToken); + DriveItemRecord? driveItemRecord = page.Items.FirstOrDefault(); + if(driveItemRecord is not null) deltaToken = new DeltaToken("PlaceholderAccountId", driveItemRecord.Id.Split('!')[0], page.DeltaLink ?? string.Empty, DateTimeOffset.UtcNow); + + logger.LogDebug("[DeltaPageProcessor] Received page: items={Count} nextLink={NextLink} deltaLink={DeltaLink}", page.Items.Count(), page.NextLink, page.DeltaLink); + await repo.ApplyDriveItemsAsync(accountId, page.Items, cancellationToken); + totalItemsProcessed += page.Items.Count(); + nextOrDelta = page.NextLink; + if(page.DeltaLink is not null) finalToken = new("PlaceholderAccountId", deltaToken.Id, page.DeltaLink, DateTimeOffset.UtcNow); + + pageCount++; + logger.LogInformation("[DeltaPageProcessor] Applied page {PageNum}: items={Count} totalItems={Total} next={Next}", + pageCount, page.Items.Count(), totalItemsProcessed, page.NextLink is not null); + progressCallback?.Invoke(CreateSyncProgressMessage(pageCount, totalItemsProcessed, page)); + if(pageCount > 10000) + { + logger.LogWarning("[DeltaPageProcessor] Exceeded max page count (10000), aborting to prevent infinite loop."); + break; + } + } while(!string.IsNullOrEmpty(nextOrDelta) && !cancellationToken.IsCancellationRequested); + logger.LogInformation("[DeltaPageProcessor] Delta processing complete: finalToken={FinalToken} pageCount={PageCount} totalItems={TotalItems}", finalToken, pageCount, totalItemsProcessed); + } + catch(Exception ex) + { + logger.LogError(ex, "[DeltaPageProcessor] Exception during delta processing: {Message}", ex.Message); + progressCallback?.Invoke(CreateErrorSyncProgress(totalItemsProcessed, ex.GetBaseException()?.Message ?? "Unknown error")); + throw new IOException("Error processing delta pages", ex); + } + + return (finalToken, pageCount, totalItemsProcessed); + } + + private static SyncProgress CreateSyncProgressMessage(int pageCount, int totalItemsProcessed, DeltaPage page) => new() + { + OperationType = SyncOperationType.Syncing, + ProcessedFiles = totalItemsProcessed, + TotalFiles = 0, // Unknown at this stage + PendingDownloads = 0, + PendingUploads = 0, + CurrentOperationMessage = $"Delta page {pageCount} applied ({page.Items.Count()} items)", + Timestamp = DateTimeOffset.Now + }; + + private static SyncProgress CreateErrorSyncProgress(int totalItemsProcessed, string errorMessage) => new() + { + OperationType = SyncOperationType.Failed, + ProcessedFiles = totalItemsProcessed, + TotalFiles = 0, + PendingDownloads = 0, + PendingUploads = 0, + CurrentOperationMessage = $"Delta sync failed: {errorMessage}", + Timestamp = DateTimeOffset.Now + }; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DependencyInjection/ServiceCollectionExtensions.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..4eb7080 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,79 @@ + +using System.IO.Abstractions; +using AStar.Dev.OneDrive.Client.Core.Interfaces; +using AStar.Dev.OneDrive.Client.Services.ConfigurationSettings; +using AStar.Dev.OneDrive.Client.Services.Syncronisation; +using AStar.Dev.Utilities; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace AStar.Dev.OneDrive.Client.Services.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddSyncServices(this IServiceCollection services, IConfiguration configuration) + { + _ = services.AddSingleton(); + _ = services.AddSingleton(); + + EntraIdSettings entraId = configuration.GetSection(EntraIdSettings.SectionName).Get()!; + ApplicationSettings appSettings = configuration.GetSection(ApplicationSettings.SectionName).Get()!; + _ = services.AddSingleton(entraId); + _ = services.AddSingleton(appSettings); + + using(IServiceScope scope = services.BuildServiceProvider().CreateScope()) + { + FileServices fileSystem = scope.ServiceProvider.GetRequiredService(); + var userPreferencesContent = fileSystem.GetFileContents(appSettings.FullUserPreferencesPath); + UserPreferences userPreferences = userPreferencesContent.FromJson(); + _ = services.AddSingleton(userPreferences); + } + + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(sp => sp.GetRequiredService()); + _ = services.AddSingleton(sp => sp.GetRequiredService()); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + + // Register supporting abstractions for DI + _ = services.AddSingleton(); + _ = services.AddSingleton(sp => + new SyncErrorLogger(sp.GetRequiredService>())); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddTransient(sp => + new DownloadQueueProducer( + sp.GetRequiredService(), + sp.GetRequiredService().UiSettings.SyncSettings.DownloadBatchSize > 0 + ? sp.GetRequiredService().UiSettings.SyncSettings.DownloadBatchSize + : 100)); + _ = services.AddSingleton(); + + // Upload queue DI + _ = services.AddSingleton(); + _ = services.AddSingleton(); + + // Update TransferService registration to inject upload queue dependencies + _ = services.AddSingleton(sp => + new TransferService( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService() + )); + _ = services.AddSingleton(sp => sp.GetRequiredService()); + + return services; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DownloadQueueConsumer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DownloadQueueConsumer.cs new file mode 100644 index 0000000..08d3f01 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DownloadQueueConsumer.cs @@ -0,0 +1,27 @@ +using System.Threading.Channels; +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Services; + +/// +public class DownloadQueueConsumer : IDownloadQueueConsumer +{ + /// + public async Task ConsumeAsync(string accountId, ChannelReader reader, Func processItemAsync, int parallelism, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(reader); + ArgumentNullException.ThrowIfNull(processItemAsync); + ArgumentOutOfRangeException.ThrowIfNegative(parallelism); + + var consumers = new Task[parallelism]; + for(var i = 0; i < parallelism; i++) + { + consumers[i] = Task.Run(async () => + { + await foreach(DriveItemRecord item in reader.ReadAllAsync(cancellationToken)) await processItemAsync(item); + }, cancellationToken); + } + + await Task.WhenAll(consumers); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DownloadQueueProducer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DownloadQueueProducer.cs new file mode 100644 index 0000000..f49a7cd --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/DownloadQueueProducer.cs @@ -0,0 +1,26 @@ +using System.Threading.Channels; +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Interfaces; + +namespace AStar.Dev.OneDrive.Client.Services; + +/// +public class DownloadQueueProducer(ISyncRepository repo, int batchSize) : IDownloadQueueProducer +{ + /// + public async Task ProduceAsync(string accountId, ChannelWriter writer, CancellationToken cancellationToken) + { + var page = 0; + while(!cancellationToken.IsCancellationRequested) + { + var items = (await repo.GetPendingDownloadsAsync(accountId, batchSize, page, cancellationToken)).ToList(); + if(items.Count == 0) + break; + foreach(DriveItemRecord? item in items) await writer.WriteAsync(item, cancellationToken); + + page++; + } + + writer.Complete(); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/HealthCheckService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/HealthCheckService.cs new file mode 100644 index 0000000..21e8508 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/HealthCheckService.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace AStar.Dev.OneDrive.Client.Services; + +/// +/// Service for checking application health status. +/// +public interface IHealthCheckService +{ + /// + /// Gets the current health status of all registered health checks. + /// + /// Cancellation token. + /// Health report containing status of all checks. + Task GetHealthAsync(CancellationToken cancellationToken = default); + + /// + /// Gets the health status of a specific check by name. + /// + /// Name of the health check. + /// Cancellation token. + /// Health check result or null if not found. + Task GetHealthCheckAsync(string checkName, CancellationToken cancellationToken = default); +} + +/// +/// Implementation of health check service wrapper. +/// +/// +/// Initializes a new instance of the class. +/// +/// The underlying health check service. +public sealed class ApplicationHealthCheckService(HealthCheckService healthCheckService) : IHealthCheckService +{ + + /// + public async Task GetHealthAsync(CancellationToken cancellationToken = default) + => await healthCheckService.CheckHealthAsync(cancellationToken); + + /// + public async Task GetHealthCheckAsync(string checkName, CancellationToken cancellationToken = default) + { + HealthReport report = await healthCheckService.CheckHealthAsync(cancellationToken); + return !report.Entries.TryGetValue(checkName, out HealthReportEntry entry) + ? null + : UpdateStatus(entry); + } + + private static HealthCheckResult UpdateStatus(HealthReportEntry entry) => entry.Status == HealthStatus.Healthy + ? HealthCheckResult.Healthy(entry.Description, entry.Data) + : HealthCheckResult.Unhealthy(entry.Description, entry.Exception, entry.Data); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IChannelFactory.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IChannelFactory.cs new file mode 100644 index 0000000..fa16049 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IChannelFactory.cs @@ -0,0 +1,24 @@ +using System.Threading.Channels; +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Services; + +/// +/// Factory for creating and configuring channels for DriveItemRecord transfer. +/// +public interface IChannelFactory +{ + /// + /// Creates a new bounded channel for DriveItemRecord transfer. + /// + /// The channel capacity. + /// A configured bounded channel. + Channel CreateBoundedDriveItemRecord(int capacity); + + /// + /// Creates a new bounded channel for LocalFileRecord transfer. + /// + /// The channel capacity. + /// A configured bounded channel. + Channel CreateBoundedLocalFileRecord(int capacity); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IDeltaPageProcessor.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IDeltaPageProcessor.cs new file mode 100644 index 0000000..771a323 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IDeltaPageProcessor.cs @@ -0,0 +1,23 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Services; + +/// +/// Processes delta pages from the OneDrive Graph API and applies them to the local repository. +/// +public interface IDeltaPageProcessor +{ + /// + /// Processes all delta pages, applies them to the repository, and returns the final delta link, page count, and total items processed. + /// + Task<(string? finalDelta, int pageCount, int totalItemsProcessed)> ProcessAllDeltaPagesAsync(string accountId, CancellationToken cancellationToken); + + /// + /// Processes all delta pages and reports progress via callback. + /// + /// The delta token to start from. + /// The cancellation token to cancel the operation. + /// Callback to report progress. + /// Tuple with final delta, page count, and total items processed. + Task<(DeltaToken finalDelta, int pageCount, int totalItemsProcessed)> ProcessAllDeltaPagesAsync(string accountId, DeltaToken deltaToken, CancellationToken cancellationToken, Action? progressCallback); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IDownloadQueueConsumer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IDownloadQueueConsumer.cs new file mode 100644 index 0000000..6ee3d29 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IDownloadQueueConsumer.cs @@ -0,0 +1,19 @@ +using System.Threading.Channels; +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Services; + +/// +/// Consumes download items from a channel and processes them in parallel. +/// +public interface IDownloadQueueConsumer +{ + /// + /// Reads download items from the provided channel and processes them with bounded concurrency. + /// + /// The channel reader to read download items from. + /// The async action to process each item. + /// The maximum number of parallel consumers. + /// Token to observe for cancellation. + Task ConsumeAsync(string accountId, ChannelReader reader, Func processItemAsync, int parallelism, CancellationToken cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IDownloadQueueProducer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IDownloadQueueProducer.cs new file mode 100644 index 0000000..7e481a8 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IDownloadQueueProducer.cs @@ -0,0 +1,14 @@ +using System.Threading.Channels; +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Services; + +public interface IDownloadQueueProducer +{ + /// + /// Fetches pending download items in batches and writes them to the provided channel. + /// + /// The channel writer to write download items to. + /// Token to observe for cancellation. + Task ProduceAsync(string accountId, ChannelWriter writer, CancellationToken cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ILocalFileScanner.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ILocalFileScanner.cs new file mode 100644 index 0000000..40e523e --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ILocalFileScanner.cs @@ -0,0 +1,12 @@ +namespace AStar.Dev.OneDrive.Client.Services; + +/// +/// Scans local files and updates sync state in the repository. +/// +public interface ILocalFileScanner +{ + /// + /// Scans local files, updates sync state, and returns summary statistics. + /// + Task<(int processedCount, int newFilesCount, int modifiedFilesCount)> ScanAndSyncLocalFilesAsync(string accountId, CancellationToken cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ISyncEngine.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ISyncEngine.cs new file mode 100644 index 0000000..e79f23b --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ISyncEngine.cs @@ -0,0 +1,39 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Services; + +/// +/// Defines the contract for synchronizing OneDrive items with the local repository. +/// +public interface ISyncEngine +{ + /// + /// Gets an observable stream of sync progress updates. + /// + IObservable Progress { get; } + + /// + /// Performs the initial full enumeration using Graph delta. Pages until exhausted, + /// persists DriveItemRecords and the final deltaLink for incremental syncs. + /// + /// The cancellation token to cancel the operation. + /// A task representing the asynchronous operation. + Task InitialFullSyncAsync(string accountId, CancellationToken cancellationToken); + + /// + /// Performs an incremental sync using the stored delta token. + /// + /// The delta token to use for the sync. + /// The cancellation token to cancel the operation. + /// A task representing the asynchronous operation. + Task IncrementalSyncAsync(string accountId, DeltaToken deltaToken, CancellationToken cancellationToken); + + /// + /// Scans the local file system and marks new or modified files for upload. + /// + /// The cancellation token to cancel the operation. + /// A task representing the asynchronous operation. + Task ScanLocalFilesAsync(string accountId, CancellationToken cancellationToken); + + Task GetDeltaTokenAsync(string accountId, CancellationToken cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ISyncErrorLogger.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ISyncErrorLogger.cs new file mode 100644 index 0000000..a164322 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ISyncErrorLogger.cs @@ -0,0 +1,14 @@ +namespace AStar.Dev.OneDrive.Client.Services; + +/// +/// Logs synchronization errors in a consistent manner. +/// +public interface ISyncErrorLogger +{ + /// + /// Logs an error that occurred during sync. + /// + /// The exception to log. + /// The path or context for the error. + void LogError(Exception ex, string path); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ISyncProgressReporter.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ISyncProgressReporter.cs new file mode 100644 index 0000000..8e97867 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ISyncProgressReporter.cs @@ -0,0 +1,13 @@ +namespace AStar.Dev.OneDrive.Client.Services; + +/// +/// Reports synchronization progress to observers or UI. +/// +public interface ISyncProgressReporter +{ + /// + /// Reports the current synchronization progress. + /// + /// The progress information to report. + void Report(SyncProgress progress); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ITransferService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ITransferService.cs new file mode 100644 index 0000000..7d508fe --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/ITransferService.cs @@ -0,0 +1,26 @@ +namespace AStar.Dev.OneDrive.Client.Services; + +/// +/// Defines the contract for managing file transfers between OneDrive and local storage. +/// +public interface ITransferService +{ + /// + /// Gets an observable stream of transfer progress updates. + /// + IObservable Progress { get; } + + /// + /// Pulls pending downloads from repository in batches and downloads them with bounded concurrency. + /// + /// The cancellation token to cancel the operation. + /// A task representing the asynchronous operation. + Task ProcessPendingDownloadsAsync(string accountId, CancellationToken cancellationToken); + + /// + /// Scans repository for pending uploads and uploads them using upload sessions and chunked uploads. + /// + /// The cancellation token to cancel the operation. + /// A task representing the asynchronous operation. + Task ProcessPendingUploadsAsync(string accountId, CancellationToken cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IUploadQueueConsumer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IUploadQueueConsumer.cs new file mode 100644 index 0000000..f3b289e --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IUploadQueueConsumer.cs @@ -0,0 +1,9 @@ +using System.Threading.Channels; +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Services; + +public interface IUploadQueueConsumer +{ + Task ConsumeAsync(string accountId, ChannelReader reader, Func processItemAsync, int parallelism, CancellationToken cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IUploadQueueProducer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IUploadQueueProducer.cs new file mode 100644 index 0000000..9d211c2 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/IUploadQueueProducer.cs @@ -0,0 +1,9 @@ +using System.Threading.Channels; +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Services; + +public interface IUploadQueueProducer +{ + Task ProduceAsync(string accountId, ChannelWriter writer, CancellationToken cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/LocalFileScanner.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/LocalFileScanner.cs new file mode 100644 index 0000000..31a3b1a --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/LocalFileScanner.cs @@ -0,0 +1,91 @@ +using AStar.Dev.OneDrive.Client.Core.Dtos; +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Entities.Enums; +using AStar.Dev.OneDrive.Client.Core.Interfaces; +using Microsoft.Extensions.Logging; +using SyncState = AStar.Dev.OneDrive.Client.Core.Entities.Enums.SyncState; + +namespace AStar.Dev.OneDrive.Client.Services; + +/// +public class LocalFileScanner(ISyncRepository repo, IFileSystemAdapter fs, ILogger logger) : ILocalFileScanner +{ + + /// + public async Task<(int processedCount, int newFilesCount, int modifiedFilesCount)> ScanAndSyncLocalFilesAsync(string accountId, CancellationToken cancellationToken) + { + var localFilesList = (await fs.EnumerateFilesAsync(cancellationToken)).ToList(); + logger.LogInformation("Found {FileCount} local files to process", localFilesList.Count); + int processedCount = 0, newFilesCount = 0, modifiedFilesCount = 0; + foreach(LocalFileInfo localFile in localFilesList) + { + cancellationToken.ThrowIfCancellationRequested(); + processedCount++; + FileProcessResult result = await ProcessLocalFileAsync(accountId, localFile, cancellationToken); + if(result == FileProcessResult.New) + newFilesCount++; + else if(result == FileProcessResult.Modified) + modifiedFilesCount++; + } + + return (processedCount, newFilesCount, modifiedFilesCount); + } + + private enum FileProcessResult { None, New, Modified } + + private async Task ProcessLocalFileAsync(string accountId, LocalFileInfo localFile, CancellationToken cancellationToken) + { + LocalFileRecord? existingFile = await repo.GetLocalFileByPathAsync(accountId, localFile.RelativePath, cancellationToken); + DriveItemRecord? driveItem = await repo.GetDriveItemByPathAsync(accountId, localFile.RelativePath, cancellationToken); + if(driveItem is null) + { + if(existingFile is null) + { + await repo.AddOrUpdateLocalFileAsync(accountId, new LocalFileRecord(accountId, + Guid.CreateVersion7().ToString(), localFile.RelativePath, localFile.Hash, localFile.Size, localFile.LastWriteUtc, SyncState.PendingUpload), cancellationToken); + logger.LogDebug("Marked new file for upload: {Path}", localFile.RelativePath); + return FileProcessResult.New; + } + else if(existingFile.SyncState != SyncState.PendingUpload) + { + await repo.AddOrUpdateLocalFileAsync(accountId, existingFile with { SyncState = SyncState.PendingUpload }, cancellationToken); + logger.LogDebug("Marked existing local file (not in OneDrive) for upload: {Path}", localFile.RelativePath); + return FileProcessResult.New; + } + } + else if(ShouldMarkAsModified(localFile, driveItem, existingFile)) + { + if(existingFile is null) + { + await repo.AddOrUpdateLocalFileAsync(accountId, new LocalFileRecord(accountId, + driveItem.Id, localFile.RelativePath, localFile.Hash, localFile.Size, localFile.LastWriteUtc, SyncState.PendingUpload), cancellationToken); + logger.LogDebug("Marked modified file for upload: {Path}", localFile.RelativePath); + return FileProcessResult.Modified; + } + else if(ShouldUpdateExistingFileForUpload(existingFile, localFile)) + { + await repo.AddOrUpdateLocalFileAsync(accountId, existingFile with + { + Hash = localFile.Hash, + Size = localFile.Size, + LastWriteUtc = localFile.LastWriteUtc, + SyncState = SyncState.PendingUpload + }, cancellationToken); + logger.LogDebug("Marked modified file for upload: {Path}", localFile.RelativePath); + return FileProcessResult.Modified; + } + } + + return FileProcessResult.None; + } + + private static bool ShouldUpdateExistingFileForUpload(LocalFileRecord existingFile, LocalFileInfo localFile) + => existingFile.SyncState != SyncState.PendingUpload + || existingFile.LastWriteUtc != localFile.LastWriteUtc + || existingFile.Size != localFile.Size + || (localFile.Hash is not null && localFile.Hash != existingFile.Hash); + + private static bool ShouldMarkAsModified(LocalFileInfo localFile, DriveItemRecord driveItem, LocalFileRecord? existingFile) => localFile.LastWriteUtc > driveItem.LastModifiedUtc || + localFile.Size != driveItem.Size || + (localFile.Hash is not null && driveItem.Size > 0 && existingFile != null && localFile.Hash != existingFile.Hash); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncEngine.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncEngine.cs new file mode 100644 index 0000000..89a16db --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncEngine.cs @@ -0,0 +1,171 @@ +using System.Diagnostics; +using System.Reactive.Subjects; +using AStar.Dev.Logging.Extensions.Messages; +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Interfaces; +using Microsoft.Extensions.Logging; + +namespace AStar.Dev.OneDrive.Client.Services; + +/// +/// Orchestrates synchronization between local storage and OneDrive by delegating to service abstractions. +/// +/// +/// Coordinates delta page processing, local file scanning, and file transfers. Contains no business logic. +/// +/// +/// Initializes a new instance of the class. +/// +/// The delta page processor abstraction. +/// The local file scanner abstraction. +/// The transfer service abstraction. +/// The logger instance. +public sealed class SyncEngine(IDeltaPageProcessor deltaPageProcessor, ILocalFileScanner localFileScanner, ITransferService transfer, ISyncRepository repo, ILogger logger) : ISyncEngine +{ + private readonly Subject _progress = new(); + private IDisposable? _transferProgressSubscription; + + private async Task EmitProgressWithStatsAsync(string accountId, string message, CancellationToken cancellationToken) + { + var pendingDownloads = await repo.GetPendingDownloadCountAsync(accountId, cancellationToken); + var pendingUploads = await repo.GetPendingUploadCountAsync(accountId, cancellationToken); + _progress.OnNext(new SyncProgress + { + OperationType = SyncOperationType.Syncing, + CurrentOperationMessage = message, + PendingDownloads = pendingDownloads, + PendingUploads = pendingUploads, + Timestamp = DateTimeOffset.Now + }); + } + + /// + public IObservable Progress => _progress; + + /// + public async Task InitialFullSyncAsync(string accountId, CancellationToken cancellationToken) + { + AStarLog.OneDriveSync.FullSyncStarted(logger, DateTimeOffset.UtcNow); + var stopwatch = Stopwatch.StartNew(); + logger.LogInformation("[SyncEngine] Starting initial full sync"); + try + { + _transferProgressSubscription = transfer.Progress.Subscribe(_progress.OnNext); + await EmitProgressWithStatsAsync(accountId,"Starting delta sync...", cancellationToken); + var dummyToken = new DeltaToken(accountId,string.Empty, string.Empty,DateTimeOffset.MinValue); + (DeltaToken finalDelta, var _, var _) = await deltaPageProcessor.ProcessAllDeltaPagesAsync(accountId, dummyToken, cancellationToken, _progress.OnNext); + await repo.SaveOrUpdateDeltaTokenAsync(accountId,finalDelta, cancellationToken); + logger.LogInformation("[SyncEngine] Delta processing complete, starting downloads"); + await EmitProgressWithStatsAsync(accountId,"Downloading files...", cancellationToken); + await transfer.ProcessPendingDownloadsAsync(accountId, cancellationToken); + await EmitProgressWithStatsAsync(accountId,"Downloads complete, starting uploads...", cancellationToken); + logger.LogInformation("[SyncEngine] Downloads complete, starting uploads"); + await transfer.ProcessPendingUploadsAsync(accountId, cancellationToken); + stopwatch.Stop(); + _progress.OnNext(new SyncProgress + { + OperationType = SyncOperationType.Completed, + CurrentOperationMessage = $"Initial full sync complete in {stopwatch.ElapsedMilliseconds}ms", + Timestamp = DateTimeOffset.Now + }); + logger.LogInformation("[SyncEngine] Initial full sync complete in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds); + } + catch(Exception ex) + { + logger.LogError(ex, "[SyncEngine] Initial full sync failed: {Message}", ex.Message); + _progress.OnNext(new SyncProgress + { + OperationType = SyncOperationType.Failed, + CurrentOperationMessage = $"Initial full sync failed: {ex.Message}", + Timestamp = DateTimeOffset.Now + }); + + throw new IOException("Initial full sync failed", ex); + } + finally + { + _transferProgressSubscription?.Dispose(); + } + } + + /// + public async Task IncrementalSyncAsync(string accountId,DeltaToken deltaToken, CancellationToken cancellationToken) + { + logger.LogInformation("[SyncEngine] Starting incremental sync"); + try + { + _transferProgressSubscription = transfer.Progress.Subscribe(_progress.OnNext); + await EmitProgressWithStatsAsync(accountId,"Starting incremental delta sync...", cancellationToken); + (DeltaToken finalDelta, var _, var _) = await deltaPageProcessor.ProcessAllDeltaPagesAsync(accountId, deltaToken, cancellationToken, _progress.OnNext); + await repo.SaveOrUpdateDeltaTokenAsync(accountId,finalDelta, cancellationToken); + logger.LogInformation("[SyncEngine] Delta processing complete, starting downloads"); + await EmitProgressWithStatsAsync(accountId,"Downloading files...", cancellationToken); + await transfer.ProcessPendingDownloadsAsync(accountId, cancellationToken); + await EmitProgressWithStatsAsync(accountId, "Downloads complete, starting uploads...", cancellationToken); + logger.LogInformation("[SyncEngine] Downloads complete, starting uploads"); + await transfer.ProcessPendingUploadsAsync(accountId, cancellationToken); + _progress.OnNext(new SyncProgress + { + OperationType = SyncOperationType.Completed, + CurrentOperationMessage = "Incremental sync complete", + Timestamp = DateTimeOffset.Now + }); + logger.LogInformation("[SyncEngine] Incremental sync complete"); + } + catch(Exception ex) + { + logger.LogError(ex, "[SyncEngine] Incremental sync failed: {Message}", ex.Message); + _progress.OnNext(new SyncProgress + { + OperationType = SyncOperationType.Failed, + CurrentOperationMessage = $"Incremental sync failed: {ex.Message}", + Timestamp = DateTimeOffset.Now + }); + + throw new IOException("Incremental sync failed", ex); + } + finally + { + _transferProgressSubscription?.Dispose(); + } + } + + /// + public async Task ScanLocalFilesAsync(string accountId, CancellationToken cancellationToken) + { + logger.LogInformation("[SyncEngine] Starting local file sync"); + try + { + _progress.OnNext(new SyncProgress + { + OperationType = SyncOperationType.Syncing, + CurrentOperationMessage = "Scanning local files...", + Timestamp = DateTimeOffset.Now + }); + _ = await localFileScanner.ScanAndSyncLocalFilesAsync(accountId, cancellationToken); + _progress.OnNext(new SyncProgress + { + OperationType = SyncOperationType.Completed, + CurrentOperationMessage = "Local file sync complete", + Timestamp = DateTimeOffset.Now + }); + logger.LogInformation("[SyncEngine] Local file sync complete"); + } + catch(Exception ex) + { + logger.LogError(ex, "[SyncEngine] Local file sync failed: {Message}", ex.Message); + _progress.OnNext(new SyncProgress + { + OperationType = SyncOperationType.Failed, + CurrentOperationMessage = $"Local file sync failed: {ex.Message}", + Timestamp = DateTimeOffset.Now + }); + + throw new IOException("Local file sync failed", ex); + } + } + + /// + public async Task GetDeltaTokenAsync(string accountId, CancellationToken cancellationToken) + => await repo.GetDeltaTokenAsync(accountId, cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncErrorLogger.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncErrorLogger.cs new file mode 100644 index 0000000..d2aa3ca --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncErrorLogger.cs @@ -0,0 +1,10 @@ +using Microsoft.Extensions.Logging; + +namespace AStar.Dev.OneDrive.Client.Services; + +/// +public class SyncErrorLogger(ILogger logger) : ISyncErrorLogger +{ + /// + public void LogError(Exception ex, string path) => logger.LogError(ex, "Error processing download item {Path}", path); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncExtensions.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncExtensions.cs new file mode 100644 index 0000000..c50998d --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncExtensions.cs @@ -0,0 +1,13 @@ +using System.Reactive.Subjects; +using Microsoft.Extensions.Logging; + +namespace AStar.Dev.OneDrive.Client.Services; + +public static class SyncExtensions +{ + public static void LogSyncError(this ILogger logger, Exception ex, string path) + => logger.LogError(ex, "Error processing download item {Path}", path); + + public static void ReportSyncProgress(this ISubject progressSubject, SyncProgress progress) + => progressSubject.OnNext(progress); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncOperationType.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncOperationType.cs new file mode 100644 index 0000000..42c9840 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncOperationType.cs @@ -0,0 +1,10 @@ +namespace AStar.Dev.OneDrive.Client.Services; + +public enum SyncOperationType +{ + Idle, + Syncing, + Cancelled, + Completed, + Failed +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncProgress.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncProgress.cs new file mode 100644 index 0000000..5060e04 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncProgress.cs @@ -0,0 +1,46 @@ +namespace AStar.Dev.OneDrive.Client.Services; + +/// +/// Progress information for sync and transfer operations. +/// +public sealed record SyncProgress +{ + public required SyncOperationType OperationType { get; init; } + public int ProcessedFiles { get; init; } + public int TotalFiles { get; init; } + public int PendingDownloads { get; init; } + public int PendingUploads { get; init; } + public string CurrentOperationMessage { get; init; } = string.Empty; + public double PercentComplete => TotalFiles > 0 ? ProcessedFiles / (double)TotalFiles * 100 : 0; + public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.Now; + + /// + /// Total bytes transferred in this operation. + /// + public long BytesTransferred { get; init; } + + /// + /// Total bytes to transfer (0 if unknown). + /// + public long TotalBytes { get; init; } + + /// + /// Transfer speed in bytes per second (0 if not calculated). + /// + public double BytesPerSecond { get; init; } + + /// + /// Transfer speed in megabytes per second. + /// + public double MegabytesPerSecond => BytesPerSecond / (1024.0 * 1024.0); + + /// + /// Estimated time remaining for the operation (null if unknown). + /// + public TimeSpan? EstimatedTimeRemaining { get; init; } + + /// + /// Duration of the current operation. + /// + public TimeSpan ElapsedTime { get; init; } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncProgressReporter.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncProgressReporter.cs new file mode 100644 index 0000000..c05c48f --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncProgressReporter.cs @@ -0,0 +1,17 @@ +using System.Reactive.Subjects; + +namespace AStar.Dev.OneDrive.Client.Services; + +/// +public class SyncProgressReporter : ISyncProgressReporter +{ + private readonly Subject _progressSubject = new(); + + public IObservable Progress => _progressSubject; + + /// + public void Report(SyncProgress progress) => _progressSubject.OnNext(progress); + + public static bool ShouldCalculateEta(double bytesPerSecond, long totalBytes, long totalBytesTransferred) + => bytesPerSecond > 0 && totalBytes > 0 && totalBytesTransferred < totalBytes; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncSettings.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncSettings.cs new file mode 100644 index 0000000..d1d8203 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/SyncSettings.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; + +namespace AStar.Dev.OneDrive.Client.Services; + +/// +/// Settings related to synchronization behavior. +/// This class needs to be mutable for easy updates during runtime as it is injected. +/// +public sealed class SyncSettings() +{ + + /// + /// Gets or sets the maximum number of parallel download operations that can be + /// performed concurrently. This property is used to control the level of + /// concurrency when retrieving files from OneDrive, helping to manage system + /// resource usage effectively. + /// + [Range(1, 10, ErrorMessage = "MaxParallelDownloads must be between 1 and 10.")] + public int MaxParallelDownloads { get; set; } = 8; + + /// + /// Gets or sets the maximum number of items to be retrieved or processed in a single batch during + /// download operations. This value is used to optimize data retrieval by controlling the batch size + /// for network requests or processing chunks. Adjusting this property can balance performance and resource usage. + /// + [Range(1, 100, ErrorMessage = "DownloadBatchSize must be between 1 and 100.")] + public int DownloadBatchSize { get; set; } = 100; + + /// + /// Gets or sets the maximum number of times an operation is retried after a failure. + /// + /// Set this property to control how many retry attempts are made before the operation is + /// considered failed. A value less than zero disables retries. + [Range(1, 10, ErrorMessage = "MaxRetries must be between 1 and 10.")] + public int MaxRetries { get; set; } = 3; + + /// + /// Gets or sets the base delay, in milliseconds, between retry attempts. + /// + [Range(500, 10000, ErrorMessage = "RetryBaseDelayMs must be between 500 and 10000.")] + public int RetryBaseDelayMs { get; set; } = 500; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/Syncronisation/ISyncronisationCoordinator.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/Syncronisation/ISyncronisationCoordinator.cs new file mode 100644 index 0000000..686677f --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/Syncronisation/ISyncronisationCoordinator.cs @@ -0,0 +1,12 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Services.Syncronisation; + +public interface ISyncronisationCoordinator +{ + IObservable SyncProgress { get; } + IObservable TransferProgress { get; } + Task GetDeltaTokenAsync(string accountId, CancellationToken cancellationToken); + Task GetPendingDownloadCountAsync(string accountId, CancellationToken cancellationToken); + Task GetPendingUploadCountAsync(string accountId, CancellationToken cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/Syncronisation/SyncronisationCoordinator.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/Syncronisation/SyncronisationCoordinator.cs new file mode 100644 index 0000000..b1ceccb --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/Syncronisation/SyncronisationCoordinator.cs @@ -0,0 +1,38 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Interfaces; + +namespace AStar.Dev.OneDrive.Client.Services.Syncronisation; + +/// +/// +/// +/// +/// +/// +public class SyncronisationCoordinator(ISyncEngine sync, ISyncRepository repo, ITransferService transfer) : ISyncronisationCoordinator +{ + /// + /// Gets an observable stream of sync progress updates. + /// + public IObservable SyncProgress => sync.Progress; + + /// + /// Asynchronously retrieves the current delta token used to track incremental changes in the synchronization + /// process. + /// + /// A cancellation token that can be used to cancel the asynchronous operation. + /// A task that represents the asynchronous operation. The task result contains the current + /// if available; otherwise, . + public async Task GetDeltaTokenAsync(string accountId, CancellationToken cancellationToken) + => await repo.GetDeltaTokenAsync(accountId, cancellationToken); + + public async Task GetPendingDownloadCountAsync(string accountId, CancellationToken cancellationToken) + => await repo.GetPendingDownloadCountAsync(accountId, cancellationToken); + public async Task GetPendingUploadCountAsync(string accountId, CancellationToken cancellationToken) + => await repo.GetPendingUploadCountAsync(accountId, cancellationToken); + + /// + /// Gets an observable stream of transfer progress updates. + /// + public IObservable TransferProgress => transfer.Progress; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/TransferProgress.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/TransferProgress.cs new file mode 100644 index 0000000..3921fa7 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/TransferProgress.cs @@ -0,0 +1,3 @@ +namespace AStar.Dev.OneDrive.Client.Services; + +public sealed record TransferProgress(string ItemId, long BytesTransferred, long? TotalBytes, DateTimeOffset Timestamp); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/TransferService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/TransferService.cs new file mode 100644 index 0000000..8044d67 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/TransferService.cs @@ -0,0 +1,439 @@ +using System.Diagnostics; +using System.IO.Abstractions; +using System.Threading.Channels; +using AStar.Dev.OneDrive.Client.Core.Dtos; +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Entities.Enums; +using AStar.Dev.OneDrive.Client.Core.Interfaces; +using AStar.Dev.OneDrive.Client.Services.ConfigurationSettings; +using Microsoft.Extensions.Logging; +using Polly; +using SyncState = AStar.Dev.OneDrive.Client.Core.Entities.Enums.SyncState; + +namespace AStar.Dev.OneDrive.Client.Services; + +public class TransferService : ITransferService +{ + private readonly IFileSystemAdapter _fs; + private readonly IGraphClient _graph; + private readonly ISyncRepository _repo; + private readonly ILogger _logger; + private readonly UserPreferences _settings; + private readonly SemaphoreSlim _downloadSemaphore; + private readonly AsyncPolicy _retryPolicy; + private readonly Stopwatch _operationStopwatch = new(); + private long _totalBytesTransferred; + + private readonly SyncProgressReporter _progressReporter; + private readonly ISyncErrorLogger _errorLogger; + private readonly IChannelFactory _channelFactory; + private readonly IDownloadQueueProducer _producer; + private readonly IDownloadQueueConsumer _consumer; + private readonly IUploadQueueProducer _uploadProducer; + private readonly IUploadQueueConsumer _uploadConsumer; + + public IObservable Progress { get; } + + public TransferService( + IFileSystemAdapter fs, + IGraphClient graph, + ISyncRepository repo, + ILogger logger, + UserPreferences settings, + SyncProgressReporter progressReporter, + ISyncErrorLogger errorLogger, + IChannelFactory channelFactory, + IDownloadQueueProducer producer, + IDownloadQueueConsumer consumer, + IUploadQueueProducer uploadProducer, + IUploadQueueConsumer uploadConsumer) + { + _fs = fs; + _graph = graph; + _repo = repo; + _logger = logger; + _settings = settings; + _progressReporter = progressReporter; + _errorLogger = errorLogger; + _channelFactory = channelFactory; + _producer = producer; + _consumer = consumer; + _uploadProducer = uploadProducer; + _uploadConsumer = uploadConsumer; + _downloadSemaphore = new SemaphoreSlim(settings.UiSettings.SyncSettings.MaxParallelDownloads); + + Progress = progressReporter.Progress; + + _retryPolicy = Policy.TimeoutAsync(TimeSpan.FromMinutes(5)) + .WrapAsync(Policy.Handle() + .Or() + .WaitAndRetryAsync( + settings.UiSettings.SyncSettings.MaxRetries, + retryAttempt => TimeSpan.FromMilliseconds(settings.UiSettings.SyncSettings.RetryBaseDelayMs * Math.Pow(2, retryAttempt)), + (ex, ts, retryCount, ctx) => + { + var exceptionType = ex.GetType().Name; + var isNetworkError = ex is IOException || (ex is HttpRequestException && ex.InnerException is IOException); + var errorCategory = isNetworkError ? "Network I/O" : exceptionType; + + _logger.LogWarning(ex, + "[{ErrorCategory}] Retry {Retry}/{MaxRetries} after {Delay}ms. Error: {Message}", + errorCategory, retryCount, settings.UiSettings.SyncSettings.MaxRetries, + ts.TotalMilliseconds, ex.Message); + })); + } + + /// + /// Pulls pending downloads from repository in batches and downloads them with bounded concurrency. + /// + public async Task ProcessPendingDownloadsAsync(string accountId, CancellationToken cancellationToken) + { + _operationStopwatch.Restart(); + _totalBytesTransferred = 0; + _logger.LogInformation("Processing pending downloads"); + var batchSize = _settings.UiSettings.SyncSettings.DownloadBatchSize > 0 + ? _settings.UiSettings.SyncSettings.DownloadBatchSize + : 100; + var total = await _repo.GetPendingDownloadCountAsync(accountId, cancellationToken); + + _logger.LogInformation("Found {TotalPending} pending downloads (batch size: {BatchSize})", total, batchSize); + + if(total == 0) + { + _logger.LogInformation("No pending downloads found - sync complete"); + return; + } + + Channel channel = _channelFactory.CreateBoundedDriveItemRecord(_settings.UiSettings.SyncSettings.MaxParallelDownloads * 2); + // Start producer and consumer tasks + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + ChannelWriter writer = channel.Writer; + ChannelReader reader = channel.Reader; + + var processedCount = 0; + IEnumerable allPendingDownloads = await _repo.GetAllPendingDownloadsAsync(accountId, cancellationToken); + var totalBytesForAllDownloads = allPendingDownloads.Sum(i => i.Size); + var parallelism = Math.Max(1, _settings.UiSettings.SyncSettings.MaxParallelDownloads); + + async Task processItemAsync(DriveItemRecord item) + { + try + { + await DownloadItemWithRetryAsync(accountId, item, cancellationToken, (b, e, eta) => + { + // Optionally emit chunked progress here if desired + }); + var p = Interlocked.Increment(ref processedCount); + var totalTransferred = Interlocked.Add(ref _totalBytesTransferred, item.Size); + var elapsedSeconds = _operationStopwatch.Elapsed.TotalSeconds; + var bytesPerSecond = elapsedSeconds > 0 ? totalTransferred / elapsedSeconds : 0; + TimeSpan? eta = null; + if(bytesPerSecond > 0 && totalTransferred < totalBytesForAllDownloads) + { + var remainingBytes = totalBytesForAllDownloads - totalTransferred; + var remainingSeconds = remainingBytes / bytesPerSecond; + eta = TimeSpan.FromSeconds(remainingSeconds); + } + + _progressReporter.Report(new SyncProgress + { + OperationType = SyncOperationType.Syncing, + CurrentOperationMessage = $"Downloading \"{item.RelativePath}\"", + ProcessedFiles = p, + TotalFiles = total, + PendingDownloads = total - p, + PendingUploads = 0, + BytesTransferred = totalTransferred, + TotalBytes = totalBytesForAllDownloads, + BytesPerSecond = bytesPerSecond, + EstimatedTimeRemaining = eta, + ElapsedTime = _operationStopwatch.Elapsed + }); + } + catch(Exception ex) + { + _errorLogger.LogError(ex, item.RelativePath); + } + } + + Task producerTask = _producer.ProduceAsync(accountId, writer, cts.Token); + Task consumerTask = _consumer.ConsumeAsync(accountId, reader, processItemAsync, parallelism, cts.Token); + + // Wait for completion + try + { + await Task.WhenAll(producerTask, consumerTask); + } + catch(OperationCanceledException) + { + // Handle cancellation +#pragma warning disable S6667 // Logging in a catch clause should pass the caught exception as a parameter. + _logger.LogWarning("Download processing was cancelled"); +#pragma warning restore S6667 // Logging in a catch clause should pass the caught exception as a parameter. + } + finally + { + await cts.CancelAsync(); + cts.Dispose(); + } + + _logger.LogInformation("All pending downloads have been processed"); + } + + /// + /// Scans repository for pending uploads and uploads them using upload sessions and chunked uploads. + /// + public async Task ProcessPendingUploadsAsync(string accountId, CancellationToken cancellationToken) + { + _operationStopwatch.Restart(); + _totalBytesTransferred = 0; + _logger.LogInformation("Processing pending uploads"); + var allPendingUploads = (await _repo.GetPendingUploadsAsync(accountId, int.MaxValue, cancellationToken)).ToList(); + var total = allPendingUploads.Count; + var totalBytesForAllUploads = allPendingUploads.Sum(u => u.Size); + if (total == 0) + { + _logger.LogInformation("No pending uploads found - sync complete"); + return; + } + + Channel channel = _channelFactory.CreateBoundedLocalFileRecord(_settings.UiSettings.SyncSettings.MaxParallelDownloads * 2); + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + ChannelWriter writer = channel.Writer; + ChannelReader reader = channel.Reader; + var processedCount = 0; + var parallelism = Math.Max(1, _settings.UiSettings.SyncSettings.MaxParallelDownloads); + + async Task processItemAsync(string accountId, LocalFileRecord item) + { + try + { + var fileStopwatch = Stopwatch.StartNew(); + await UploadLocalFileWithRetryAsync(accountId, item, cancellationToken, (b, e, eta) => + { + // Optionally emit chunked progress here if desired + }); + fileStopwatch.Stop(); + var p = Interlocked.Increment(ref processedCount); + var totalTransferred = Interlocked.Add(ref _totalBytesTransferred, item.Size); + var elapsedSeconds = _operationStopwatch.Elapsed.TotalSeconds; + var bytesPerSecond = elapsedSeconds > 0 ? totalTransferred / elapsedSeconds : 0; + TimeSpan? eta = null; + if (bytesPerSecond > 0 && totalTransferred < totalBytesForAllUploads) + { + var remainingBytes = totalBytesForAllUploads - totalTransferred; + var remainingSeconds = remainingBytes / bytesPerSecond; + eta = TimeSpan.FromSeconds(remainingSeconds); + } + + _progressReporter.Report(new SyncProgress + { + OperationType = SyncOperationType.Syncing, + CurrentOperationMessage = $"Uploading \"{item.RelativePath}\"", + ProcessedFiles = p, + TotalFiles = total, + PendingDownloads = 0, + PendingUploads = total - p, + BytesTransferred = totalTransferred, + TotalBytes = totalBytesForAllUploads, + BytesPerSecond = bytesPerSecond, + EstimatedTimeRemaining = eta, + ElapsedTime = _operationStopwatch.Elapsed + }); + } + catch(Exception ex) + { + _errorLogger.LogError(ex, item.RelativePath); + } + } + + Task producerTask = _uploadProducer.ProduceAsync(accountId, writer, cts.Token); + Task consumerTask = _uploadConsumer.ConsumeAsync(accountId, reader, item => processItemAsync(accountId, item), parallelism, cts.Token); + + try + { + await Task.WhenAll(producerTask, consumerTask); + } + catch(OperationCanceledException) + { +#pragma warning disable S6667 // Logging in a catch clause should pass the caught exception as a parameter. + _logger.LogWarning("Upload processing was cancelled"); +#pragma warning restore S6667 // Logging in a catch clause should pass the caught exception as a parameter. + } + finally + { + await cts.CancelAsync(); + cts.Dispose(); + } + + _operationStopwatch.Stop(); + _logger.LogInformation("All pending uploads have been processed"); + } + + private async Task UploadLocalFileWithRetryAsync(string accountId, LocalFileRecord local, CancellationToken cancellationToken, Action? onProgress = null) + { + try + { + await _retryPolicy.ExecuteAsync(async _ => await UploadLocalFileAsync(accountId, local, cancellationToken, onProgress), cancellationToken); + } + catch(Exception ex) + { + _errorLogger.LogError(ex, local.RelativePath); + throw new IOException($"Upload failed for {local.RelativePath} after {_settings.UiSettings.SyncSettings.MaxRetries} retries. See inner exception for details.", ex); + } + } + + private async Task UploadLocalFileAsync(string accountId, LocalFileRecord local, CancellationToken cancellationToken, Action? onProgress = null) + { + var log = new TransferLog(accountId, Guid.CreateVersion7().ToString(), TransferType.Upload, local.Id, DateTimeOffset.UtcNow, null, TransferStatus.InProgress, 0, null); + await _repo.LogTransferAsync(accountId, log, cancellationToken); + try + { + var parent = Path.GetDirectoryName(local.RelativePath) ?? "/"; + var fileName = Path.GetFileName(local.RelativePath); + UploadSessionInfo session = await _graph.CreateUploadSessionAsync(accountId, parent, fileName, cancellationToken); + + await using Stream stream = await _fs.OpenReadAsync(local.RelativePath, cancellationToken) ?? throw new FileNotFoundException(local.RelativePath); + const int chunkSize = 320 * 1024; // 320KB + long uploaded = 0; + var fileStopwatch = Stopwatch.StartNew(); + const long logIntervalBytes = 10 * 1024 * 1024; // 10 MB + var nextLogThreshold = logIntervalBytes; + while(uploaded < stream.Length) + { + cancellationToken.ThrowIfCancellationRequested(); + var toRead = (int)Math.Min(chunkSize, stream.Length - uploaded); + var buffer = new byte[toRead]; + _ = stream.Seek(uploaded, SeekOrigin.Begin); + var read = await stream.ReadAsync(buffer.AsMemory(0, toRead), cancellationToken); + await using var ms = new MemoryStream(buffer, 0, read, writable: false); + await _graph.UploadChunkAsync(session, ms, uploaded, uploaded + read - 1, cancellationToken); + uploaded += read; + + if(stream.Length > logIntervalBytes && uploaded >= nextLogThreshold) + { + _logger.LogInformation("Uploading {Path}: {MB:F2} MB of {TotalMB:F2} MB complete", + local.RelativePath, uploaded / (1024.0 * 1024.0), stream.Length / (1024.0 * 1024.0)); + // Send progress update to UI + TimeSpan elapsed = fileStopwatch.Elapsed; + var bytesPerSecond = elapsed.TotalSeconds > 0 ? uploaded / elapsed.TotalSeconds : 0; + TimeSpan? eta = null; + if(SyncProgressReporter.ShouldCalculateEta(bytesPerSecond, stream.Length, uploaded)) + { + var remainingBytes = stream.Length - uploaded; + var remainingSeconds = bytesPerSecond > 0 ? remainingBytes / bytesPerSecond : 0; + eta = TimeSpan.FromSeconds(remainingSeconds); + } + + onProgress?.Invoke(uploaded, elapsed, eta); + nextLogThreshold += logIntervalBytes; + } + } + + await _repo.MarkLocalFileStateAsync(accountId, local.Id, SyncState.Uploaded, cancellationToken); + log = log with { CompletedUtc = DateTimeOffset.UtcNow, Status = TransferStatus.Success, BytesTransferred = stream.Length }; + await _repo.LogTransferAsync(accountId, log, cancellationToken); + _logger.LogInformation("Uploaded {Path}", local.RelativePath); + } + catch(OperationCanceledException) + { +#pragma warning disable S6667 // Logging in a catch clause should pass the caught exception as a parameter. + _logger.LogWarning("Upload cancelled for {Id}", local.Id); +#pragma warning restore S6667 // Logging in a catch clause should pass the caught exception as a parameter. + } + catch(Exception ex) + { + _errorLogger.LogError(ex, local.RelativePath); + log = log with { CompletedUtc = DateTimeOffset.UtcNow, Status = TransferStatus.Failed, Error = ex.Message, BytesTransferred = 0 }; + await _repo.LogTransferAsync(accountId, log, cancellationToken); + throw; + } + } + + private async Task DownloadItemWithRetryAsync(string accountId, DriveItemRecord item, CancellationToken cancellationToken, Action? onProgress = null) + { + try + { + await _retryPolicy.ExecuteAsync(async _ => await DownloadItemAsync(accountId, item, cancellationToken, onProgress), cancellationToken); + } + catch(Exception ex) + { + _errorLogger.LogError(ex, item.RelativePath); + throw new IOException($"Download failed for {item.RelativePath} after {_settings.UiSettings.SyncSettings.MaxRetries} retries. See inner exception for details.", ex); + } + } + + private async Task DownloadItemAsync(string accountId, DriveItemRecord item, CancellationToken cancellationToken, Action? onProgress = null) + { + _logger.LogDebug("Starting download: {Path} ({SizeKB:F2} KB)", item.RelativePath, item.Size / 1024.0); + _logger.LogDebug("Waiting to acquire semaphore for {Path}", item.RelativePath); + await _downloadSemaphore.WaitAsync(cancellationToken); + _logger.LogDebug("Semaphore acquired for {Path}", item.RelativePath); + + var log = new TransferLog(accountId, Guid.CreateVersion7().ToString(), TransferType.Download, item.Id, DateTimeOffset.UtcNow, null, TransferStatus.InProgress, item.Size, null); + await _repo.LogTransferAsync(accountId, log, cancellationToken); + try + { + _logger.LogDebug("Downloading content for: {Path}", item.RelativePath); + await using Stream stream = await _graph.DownloadDriveItemContentAsync(accountId, item.DriveItemId, cancellationToken); + + _logger.LogDebug("Writing file to disk: {Path}", item.RelativePath); + + // Enhanced: Write file in chunks and log progress for semi-large files + const long logIntervalBytes = 10 * 1024 * 1024; // 10 MB + long totalWritten = 0; + var nextLogThreshold = logIntervalBytes; + await using Stream output = await _fs.OpenWriteAsync(item.RelativePath, cancellationToken); + var buffer = new byte[1024 * 1024]; // 1 MB buffer + int read; + while((read = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken)) > 0) + { + cancellationToken.ThrowIfCancellationRequested(); + _logger.LogDebug("Read {Bytes} bytes from stream for {Path}", read, item.RelativePath); + await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken); + totalWritten += read; + + if(item.Size > logIntervalBytes && totalWritten >= nextLogThreshold) + { + _logger.LogInformation("Downloading {Path}: {MB:F2} MB of {TotalMB:F2} MB complete", + item.RelativePath, totalWritten / (1024.0 * 1024.0), item.Size / (1024.0 * 1024.0)); + // Send progress update to UI + TimeSpan elapsed = _operationStopwatch.Elapsed; + var bytesPerSecond = elapsed.TotalSeconds > 0 ? totalWritten / elapsed.TotalSeconds : 0; + TimeSpan? eta = null; + if(SyncProgressReporter.ShouldCalculateEta(bytesPerSecond, item.Size, totalWritten)) + { + var remainingBytes = item.Size - totalWritten; + var remainingSeconds = bytesPerSecond > 0 ? remainingBytes / bytesPerSecond : 0; + eta = TimeSpan.FromSeconds(remainingSeconds); + } + + onProgress?.Invoke(totalWritten, elapsed, eta); + nextLogThreshold += logIntervalBytes; + } + } + + _logger.LogDebug("Completed reading stream for {Path}", item.RelativePath); + + _logger.LogDebug("Marking file as downloaded: {Path}", item.RelativePath); + await _repo.MarkLocalFileStateAsync(accountId, item.Id, SyncState.Downloaded, cancellationToken); + + IFileInfo fileInfo = _fs.GetFileInfo(item.RelativePath); + log = log with { CompletedUtc = DateTimeOffset.UtcNow, Status = TransferStatus.Success, BytesTransferred = fileInfo.Length }; + await _repo.LogTransferAsync(accountId, log, cancellationToken); + _logger.LogInformation("Downloaded {Path} ({SizeKB:F2} KB)", item.RelativePath, fileInfo.Length / 1024.0); + } + catch(Exception ex) + { + _errorLogger.LogError(ex, item.RelativePath); + log = log with { CompletedUtc = DateTimeOffset.UtcNow, Status = TransferStatus.Failed, Error = ex.Message, BytesTransferred = 0 }; + await _repo.LogTransferAsync(accountId, log, cancellationToken); + throw new IOException($"Download failed for {item.RelativePath} after {_settings.UiSettings.SyncSettings.MaxRetries} retries. See inner exception for details.", ex); + } + finally + { + _logger.LogDebug("Releasing semaphore for {Path}", item.RelativePath); + _ = _downloadSemaphore.Release(); + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/Untitled-1.sqlite3-query b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/Untitled-1.sqlite3-query new file mode 100644 index 0000000..021fad0 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/Untitled-1.sqlite3-query @@ -0,0 +1,29 @@ +-- database: /home/jason/.config/astar-dev/astar-dev-onedrive-client/database/app.db + +SELECT ItemId, COUNT(*) AS Count FROM TransferLogs +GROUP BY ItemId +HAVING COUNT(*) > 1; + +SELECT count(*) FROM TransferLogs; +SELECT * FROM TransferLogs +WHERE ItemId IN ( + SELECT ItemId FROM TransferLogs + GROUP BY ItemId + HAVING COUNT(*) > 1 +) +ORDER BY ItemId; + +DROP TABLE "TransferLogs"; + +CREATE TABLE "TransferLogs" ( + "Id" TEXT NOT NULL CONSTRAINT "PK_TransferLogs" PRIMARY KEY, + "BytesTransferred" INTEGER NULL, + "CompletedUtc_Ticks" INTEGER NULL, + "Error" TEXT NULL, + "ItemId" TEXT NOT NULL, + "StartedUtc_Ticks" INTEGER NOT NULL, + "Status" INTEGER NOT NULL, + "Type" INTEGER NOT NULL +); +CREATE UNIQUE INDEX `sqlite_autoindex_TransferLogs_1` ON `TransferLogs` (Id); +CREATE INDEX "IX_DriveItems_DriveItemId" ON "DriveItems" ("DriveItemId"); \ No newline at end of file diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/UploadQueueConsumer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/UploadQueueConsumer.cs new file mode 100644 index 0000000..9bb74b7 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/UploadQueueConsumer.cs @@ -0,0 +1,21 @@ +using System.Threading.Channels; +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.Services; + +public class UploadQueueConsumer : IUploadQueueConsumer +{ + public async Task ConsumeAsync(string accountId, ChannelReader reader, Func processItemAsync, int parallelism, CancellationToken cancellationToken) + { + var tasks = new List(); + for(var i = 0; i < parallelism; i++) + { + tasks.Add(Task.Run(async () => + { + await foreach(LocalFileRecord item in reader.ReadAllAsync(cancellationToken)) await processItemAsync(item); + }, cancellationToken)); + } + + await Task.WhenAll(tasks); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/UploadQueueProducer.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/UploadQueueProducer.cs new file mode 100644 index 0000000..1d706cb --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client.Services/UploadQueueProducer.cs @@ -0,0 +1,19 @@ +using System.Threading.Channels; +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Interfaces; + +namespace AStar.Dev.OneDrive.Client.Services; + +public class UploadQueueProducer : IUploadQueueProducer +{ + private readonly ISyncRepository _repo; + public UploadQueueProducer(ISyncRepository repo) => _repo = repo; + + public async Task ProduceAsync(string accountId, ChannelWriter writer, CancellationToken cancellationToken) + { + IEnumerable uploads = await _repo.GetPendingUploadsAsync(accountId, int.MaxValue, cancellationToken); + foreach(LocalFileRecord item in uploads) await writer.WriteAsync(item, cancellationToken); + + writer.Complete(); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/AStar.Dev.OneDrive.Client.csproj b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/AStar.Dev.OneDrive.Client.csproj new file mode 100644 index 0000000..dcf09f1 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/AStar.Dev.OneDrive.Client.csproj @@ -0,0 +1,58 @@ + + + net10.0 + WinExe + enable + enable + latest + false + + true + false + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + Always + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/App.axaml b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/App.axaml new file mode 100644 index 0000000..cc5c497 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/App.axaml @@ -0,0 +1,7 @@ + + + + + diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/App.axaml.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/App.axaml.cs new file mode 100644 index 0000000..4736971 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/App.axaml.cs @@ -0,0 +1,41 @@ +using System.Diagnostics.CodeAnalysis; +using AStar.Dev.OneDrive.Client.ViewModels; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Avalonia.Styling; +using Microsoft.Extensions.DependencyInjection; + +namespace AStar.Dev.OneDrive.Client; + +[ExcludeFromCodeCoverage] +public partial class App : Application +{ + public static IServiceProvider? Services { get; set; } + + public override void Initialize() => AvaloniaXamlLoader.Load(this); + + public override void OnFrameworkInitializationCompleted() + { + if(ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + if(Services is null) + throw new InvalidOperationException("DI Services not initialized. Ensure Program.ConfigureServices sets App.Services before starting."); + + try + { + MainWindow window = Services.GetRequiredService(); + desktop.MainWindow = window; + } + catch(Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to create MainWindow: {ex}"); + throw; + } + } + + base.OnFrameworkInitializationCompleted(); + } + + public void SetTheme(ThemeVariant? variant) => RequestedThemeVariant = variant ?? ThemeVariant.Default; // Default == Auto +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ApplicationMetadata.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ApplicationMetadata.cs new file mode 100644 index 0000000..6f5c372 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/ApplicationMetadata.cs @@ -0,0 +1,22 @@ + +namespace AStar.Dev.OneDrive.Client; + +public static class ApplicationMetadata +{ + /// + /// The name of the application. + /// + public const string ApplicationName = "AStar Dev OneDrive Sync Client"; + public static readonly string ApplicationFolder = ApplicationName.Replace(" ", "-").ToLowerInvariant(); + + /// + /// The version of the application. + /// + public static readonly string ApplicationVersion = BuildApplicationVersion(); + + private static string BuildApplicationVersion() + { + Version? version = typeof(ApplicationMetadata).Assembly.GetName().Version; + return version is null ? "1.0.0-alpha" : $"{version.Major}.{version.Minor}.{version.Build}-alpha"; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Assets/astar-logo.png b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Assets/astar-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..609a0215a4bdc983afb9d2cf1ea03129bb32a4a9 GIT binary patch literal 14032 zcmZvib8sd+wD6zW_EX!oZQHhOV{32CEw|meeQVq8*0$|Fb-%sezxQV5Bss~MGnq^# zIrGbjQBjgcf(OC_001Oe840z2ZSOx(fCc|oHznR30|4lmei}L+YNp;KE^f|Nwhopg z9=w*F&;-t}RLoD4C zG*H;%`6mhA*7w_N=2v*#@_*&!fvX&o0nKYxh94~VH^!iogQ?fy*#zOE6ZfN5?es^K z-j^r!y?2z^+^-MEBfLG;ppS>vcUc3OFBj7HtA>xI2g&cpt`D#GHxzo{^G5y!aUsa) zUN|6O=ez6Vpy33w&Z!FTsi83Ut$Fx{+Ng~-KERp1hd(&2@Es)hO5}U=Q*N?Ac{`wx zO`b8;*mii5u5R<@E^QE9zq|QC=c4jnkh^q6H_3u_K9$4vXo*SPAX0<(DfZUk(~Sb{Rv_H8NFa|!53qbeEG>x+d%dxMks zNPk;*BOE-Z!!+-sndV#HpAJfrcVO%_m!Ds;+50hJ{(g6SEzB$u)7ILUMdm@GkH)Cw zR@a;#PsZaGIURikUy zjl}Yw#($N6Y*&iZGh;Z_*)Oo0+BzPWoAqx3AJQ&9PV+u|-v0>@ymPxsHNsOMFbe5gd_dZ>NWz74%U&wSEChLFRQ-5kRS)*hd<3;+KC+uMF8C~H?z5^om)~6+ij~ry1H5Z6}vN|zXubKjlumrz6n7LX4OMlN7*g^$~)XiQyzVs$ zy7)OY3yhe_j9hiPO*Xwnr*7di5YMEpC`;a zAZz=XwZq$X8!Y2kHd`WA^H7gCNp`9Zr>yZ3EB(4@d+SwNxAX6hP%&?tVJClUPC3zZ zzidXvX@=*Sd}?}K3@jm4yXcCCdTvVLa3O*9k{{WLJJ+O{mC~2clQRdk5mO2;3cXUF zp9j<7^Ika!G&lyjOV7OwuwDN0+diKLE)=OYEL8@s>Vi9IqsA2+b_m`!STPnpIB zR{K%}`KrfrSWvjnLfDtX-MhRxP1;$%2`k+!ja&(ovVk1kpf%OjAb1MP2UX#m-TK7< zl;A~HHKt*ak1&kY0X}x;3;sH!#gi_%D}P^ce_nmS)y9Xt69ID5a6O^2n*^f~GD(;n zGQzW@D!&c*Tf8fBN!njeLG!Bvc6z!?IPdnU8NoR>L&M*AtGi4oa z`w*b-DJmYjjOLp~9A9@|c~jVJ8jK(mDzejs7)QGRJEgMe$~g9>d$pMt-3QY_iL$BF zX`Skw;j#^mt;{u*s%eO=C4vpzt)y_LyI~9OjAc0=XXWp{&yqIRrJK}g;)mOu)cs^b;s50@E{&?B%J03UE?#yt7GJqJ~>-XC$HsgsnCX2(5B58 z&#F2P{V@)W2uIj`4^qCioR;q{C3?!v`wl9=0v(dKG8H``v|Fut3(RAuyNM#NuWl7`R#)%{`&o@~&DZTX0=;Yih!1H-Sf z9_gEKh+~>%xf!v!JQG{|*FCaZGjo;%+@Gk-<<-Q*{5=5)jMTBeyse-Ciat|YHm=21 z)&ofL`shmgoWMUQz2tqU|pAes~h~arQd)Y3#VZZgQiE zCc4a<1fF>HMfI2!IKYOyoOn@)I{_rJLoK97&YVN6F~6-v$Z$v??$%KXF_2|zibiE7 z-hFC6dlz;TJx@2enD4}QOdXZ`q6Gcaq$jFbW!o$ay?^aCPv#@GtlFfxhY~M&k_jEC zD0Q|V@=X8p)k?lpSq$-9ygJ9(162pt#qT!RPzQ=}*j(lYPM&Lz%0L=3&CJIN$f9s- zk-Y1aM3*?uqXcn@HUS9%!kvO1H-TeOic(?E5SLm6=E^>G|3XkvY>4?X7j}gvFBBm; zjI7!ppA)w4-%MeFru$l#L9|)haZ!SsghuytZ3u(hheRqcQW32|suAL$ac{>@;NI2A z#yqjM;^|LoiP!MeB|E6Fo=$cIzlN03a1sMDBpuQ$i82ycxN$n#oYOt|xr9o1T z5NH|CXqb~^w>|MDxRBbcSrM8KXm}|CtW4nZO$`NXr*JsbpaaoVR&YGRGU$*&PI=7U zA*IFAY`j)xQlScVN`{#3xs67wwXBQlPk_(a#G?@4e7UANoZ2vDi2Mk1{k#vg=BxYJtJwRXmfC=tMYpeXi6jb$s4PX1uxC3SKSJ+vS@oB_MS_2i-=_x<8nL`^`ja~q z1*j^Fk^~c)k1U=a+l@y^-Vg|3AOTh^31cGU1D+a=UjK)g7Bcby@uxU&wce=x5g8n{ zMQ&Kd%La^n23%T)P9SWUbX9auMeE{ZnlvWH9PJ?L27GbPSJ^0n6E^O`b5e{jFGJx^ za6`dvjKJph6r;lwEcxm@zp0OCv&RTrue_M3-(W{Xvv{-204yClSIy(ET4oycsWF7H z!xD?%DUIge0~@E@48~%F21Bz7|H{^Ur})sP6*yKt%t2NXou1qHr8$%xw4 zTM}<3I5IdaXb7mvMroFfBvNQ5Dgv{)6D6d4V(2P^nEl{H zl655okW64eorzbEiK%3woe2;d&pKWW{}FChh6ZXZp)XbMHcpN#+O;#v@6CE!SEKS7 zXg|3`NRUYJ5q3o>!NZ}Jin<@RMF&^6kHn)D(!R-LL4?{RYpBeU)fnl+OU?ATva|p{ zmC&N-{Too)SUFVuTxN<5k#bQRHW0n`3?t6dAD zyp^GbD~zswxtE9-Hg=IU@k#7*MlMC{81_1z0CH$2olVwaQ*+isMo4&m2;$))vjC&8 zXL`e-CF6nnc-SAzBzMcqgg+?@#XPU%yH7Ji@&3RS$@0W5W#1@srG{vwVyw4PJ3Npz zm~Gyn2>MthAvijPBx$M`{SB9CwMMWbl~z+IV#}duN$!q7)`biVqAEv=09QnXc6@>r zK*c}~cU_Qj3%mHE71l|>PUpH$0j`-e%P5pXB10nXv!{(#j#b~$CX87L0X}+}Qo83Y zBK{>cq666)yBjq$ayA27D2hpms3!GNq(Dv7ABS>9{mQCOL;PrCDKS8LU>uh6;}+*4 zW=tdn+r?z}#STx+-Ls%i<&-=7FN&lTww26p&g0_M(Fqs@@V1D5y=c6AVVWM* zErj7kC)ath^oo=y6J8csIhlA29Gj$;_95M0TsfO-z=0QBfs&>Q2>1?LMS_x0z;(r) zs0w)jnWjq!@Xg0wqFgbd*Iht#NNOTH+sj6>$6b;RUm*Gk+`p%%6kNyko(K*10NdzL z$y%K7s&^poC#v4Mh)t~?8+7Q4H;(C#05+2t*oiA;M-JWEHJqHT9; zfO826ir0aVWcXTEC7^s5+8AVJtc8!8>85^FAvh|c*>41M#L4=N{r;ZYtD103C?s&F zC>i)NIT{oVRuVcNvOM-(sH@2c?%J_9YBMBv?fro4=>uMu-~YWT;icR&D!rnI$>W}) zcq{FNtwXSx5+J`qq?rPhW=r`7+J{7$^TI@jR&X5D@~rx?#U@6?*eN*t%>43Ay$yC$ zR463q7A4xL7UkKJ5UOD?>!*LiiG^gEB#E}dy@QpQyjCOlFQ8FR{jkFqrB+~9i{xzM zz5hd=KlWwn$Av(JwRvL!x3B#73Gv+*0{6w-BBv(Qq7iq}-&8XTFO_#>91e=bi(VpS z`sOxn3^|*d0ACf0viC)~Jf#S-Ckb+Hpm=B$iqKaNT%rOA4I}fy3P~BQR!HdL0g*wP zEf$euyh(>iB*AxG#VEk041X}uxtN3R-do^@^u4y5#oO9$Y~C7LD-Uxh3ABPZ+Ar9Q zkq7r{EfI$Qrf7zFW!rQTc$Q<{uSEAamGDTZJcu1D=~bCAFNXJclEn<7p}^3(*c!1$ zO?)^wQZF%(QTboADmgF7>4sQD)ovM#X{gswz_F6N*`I6NBDdHhE)4$l3v2%;hE7GJuG zdU*Ji(KuIM3Y8YYAYZFSDh=`^9RD^*9|~Kg8=xp>kSuPRlk8@BqKC5%R+;UWR7AA1 zYzxOq4kBSAS5@dZjHZa#0!P!?_WKmP5CM^>1gyWHs*_&+Ga{!!K9|`eLz}u`Z_cj^ zj7%`~I*5hATb#ptpR9ltA>X59dx;YehuY2N>FiJ5M~0Bv2?eSTRCv^fGBgI1S6@lR zJM(n8^f9xU$~UL;?2mntK0wXN8by~lkbzqzv*s~+G))!4u;A-`ZYUodi#{q3`o%!O)S`b!L7r-UsEWj;L<05~G52cc+VX+}em(UD zD}0~he_bIwfpDR=Z+BR;w41LsEDQgUDoATlMNt5tAqnBb6zZQ2=B_3!2DUcN5d2Re zF_%$O1OWV~0DzDP0O0N4P{=6&;K>32oSOgud|3bhj!RyLs=&VqI2RdRcK`qZ<9`GU zkdue|ZxF^qR#6h>3>E?!f@CrcA>f}&fUJb5hR^y{fNvIow&x8mXnFpfZDDJnsSao> zHLug?FrIo8$USlw+ux862!(`Y-b=~P%-B2vH8t$QoV5Wou$}3VJNS()9mlrl*Q;ArA}` zLuJK=_nFb(uC~|Vl>O%rZYx30P=Xo&xya%8x}URDb^eE{vhjp-@7$d{qXZQV0H&)i zUQxws@c*q7k5Rdn{V-GTtCC&oMrym-n(b2Q{EY~23^CGDQRxoBEF|jxP&ZU;sW(q_ zLOCnPksJ?Z@S&W*!eZToBccvMfA=xQeIFxqZtq`+h8#n0nY&DzOr$`BXMEa44PhJ) z+w0reiOV#q&}aFhYBilC=HdiCe#(=B zg@d8@u`??MoHKKOX~(S?t9Z{kc6Zm9GLuV5KuVm0oX*x=J-(c80)zadG}hdygi}S$ zYtFc{$-pJVz!$;(BALJucb+_F+z9$yGIkP)k6PyMJp)mw|4~%EPJu*3na_!I`gEU~qY(k2up!Z;Dc};w^=*`YTZcdc1q1r-f=I64`x*7AVE|k*v}AH-{@ycv5TT$i z-pby{q+Xx|B!Dm2ZuBtDzq|=g|G{RGZ*Zkn78*CT^uh1I5h=9t3=sf3%5=n=LPlA2 z$*X8tkS;n&I)euwK?Q)(kcgRx$cRj{mUMsBn9r77=zjod;?ksv^Ts?!0^&{m2YQ1P zubO6OiHyAtJQ+EOl;qeF{iBY1p2G$CK_`jd%kSDJ#_bG=5_=&%ZR7%+>VM$yVC87F z@%rbL>Q)ZncgsFs52QW4eef}iW&p65e*;E@{u(oEI02v*`Xm`6wXGppeqxE6D_J*f zr#-SlmsH-Th+eED3Qn6k#QOq5q5+1@;s8WgFtm_p8Eud8KRNG(SwZ$pXZsKMrIrwT zV-9!igcvp2-fXg_NnZWDF+bPT50*GcK#lIa{$AH8q;DQT_9PNa#`-^fD{?cW5?{Ae)QD zDs+ftoB(le3IFj5R)5RjS;@~IRWnugH6Yi>@4J9=;$GLof2-CmIPiQzA~VN2W+4%g z0GAZ`CBtJT86}K8kj;>PcG3PCnPDM9P1{!FxW&fClRbGnX{!E^UHw~dJ!^54iqP9~ zhDsD(Mi$1OZy?in^P>(0kwAEMS+qfvg~1S=DL*{^^K7eI`1|@O_!DN*xu@}Ke`#(8`yt{UCK)!B zRSXim3>Gz%eZI|Nvt>6CKUKRX-d~1R8jfPagwi6c1a&Cg4%uJG)w=bfAf1dhU2PHl zVYn*?Qw~fH3SzL`n19PXC>I;<9CB!EdUmVVrpOFdLPR1up5n0?3XE#@^oijb*=q@g zN1Otf!;q+)37w=HH3E+$YACHBG3A~y!qv-h!&H#_Q@NL8&BHRc2sT6qZymx4l!XL@6Zrx(7vq?LJ?idmFE8WziFk>LNq-9q*k{>#W_8R zW0Lf#AkL@Yn<8_4FUyw8iA`T~{N!W5g{T5t{-6Tu*5A<3_YKiOJ{+KY7`pTtnBW4v zIXowwe2S?2yLX6XrYrv)$YwuDHsF(gR@r3ZNVMxA0htja z-qIkN;(-Fpt;=75JhZcTU&Tm7PLTzRoEZ*9dVP++!dI&;`sbn&fRzgtaDa#!dUP}< zKtilX`0H<@^0p@?wl>q3pMrxuEK~sWno`dx0pYYoBRYV{&*2cmo6+&vPQp&r{8!y*@4E=)I|jh` zW%;X6*!}X2enX2niF>q51)U&;G`WVOG&1CCZUZMN5vO2Xpc8d?)7eYz^_(7rb$|EItkcsX=8`AP z_eC;HvyL#Ajf&8iS5>2>P1o;qy~)N8-+YgR4LQA2Yb>(5)PdguGNQ7S{>W7jiMOw% z*w2G0RxKwWUcuBjk1dVX_~8xc!xfLrBRz!%6+HzklH*(H9>ChqWnNd#NT}P3g2@13tZnBei6Kwt9C`!J_byb}w zg881JTjngb^wSpTrc~-o`gF$&bd)}z5wvy+w`y;*_e;-u>j0vlHdzyG zd>SOc*|DUS98PSoB+cy4{yerqNC+Yz`8}&Vx5$evr+nmu+FQVILYa3t@4vWQ&*;D9VtUz1eQR+*~%{bxx-w0~x(MV9yxXJu}wd%N7QIFoAdHA{Mvv#Yac@to!;#N3pe+|!k z40aj`=6>$J^ON(DQ=;wlY1|nHd8Lsu9EJiQyW_%I>+BJM@#Gg*h~$-9?`m=8R517F zm*w*MR95mJVK%>yh{7)$ZS#cD1TJ#-bfPegUuocgt=&=WH;);-4zpFEn1hq&mke(S zQN|biKQ1UHMd<;{kL}Njm6b@Z`r7`T=Yp0|m@Z{qKL#Fedyo!*#E8pp0bd<6JxQSsk!T(Q0u$TqI8#6TU^ERUCWsDeZFBRqkMmir(`mo z-hhnUoeWjD(=;3v`zVpi(mWUf!Cw=5(Y5v@0tn zl&^pQ>-m#}6@M$a+XSRC{u%8+MDs`KhHvz8!kKIAwhFneW_z^JEGzhFB|`@CT*Nr zm`hr+96T)+jieeH1TecLsH3^w`kvF1Yv$~!by#5}S*EPr5RbX+bO72LSopXt%{t{`oj5(+?a9@k}-Ux4^9wd1-_^PwOi z4J@XYWLI}4Gy!t&YxeX?wd_l=5eY2G_ZSl4BavEcb#b!B*>c-j*~XmfC~EYg?l6%y zx={Ue&h%uQA2?8YQdNWmtW-^m%)Ae;Y2^Kc3Y1&Uy&Rrue|0q6;ea&YJ8;}gj%m(82|Be-``xYA~;Q9#L7mXguN&@G1M50(}7 zs1VKeE|;&qaWala0+Wi^l{!JniZhtTydutpZ62CGN5Dc1T~#-B0$guqiSU`R@^hK& z`CB`=_D{UFwIF{*;}akWv8k`N1@4EAMvbVY`^JB7^2}}E8TJay5YR%U-y^@QZI_f0 z3kU_QJY8LR2B~V5by28wWG0=|RPtVY6ae!v(nD+!d4NIJAltFNsatOnf2AaGgA9w- zYUC{FCWkU?iXAbzA9>{FCN7bAzFp1RCN!xwr##-8?$Q`PD`N;}y^P*ZI*h=r(i+J; z4{sm$5`H{N(DI-0K#`82smZ3srfJB9rj?E@sZdZlTrdlDZB4A!9oXSCzhgH`@`{T9 zm5;oyC&#pby>M@HU;swaf=!;I?YfcD?QLA0B^={990XUDc% zZh^=P5s57F%kfI_O7Tn5-kEte@i+26X!UqAz_i-pMsNg6O9@*<*Ht8!_wTu)h*Y7Q zv_{DYYMBhB+osCn3g*!c+oMIhS25Jd{4bdv6)c1d0516pHiCk7JVg%HD#wJ+TvVBg zQvd^g3i~(r(R{mG0|mhuj~XmxMCv?}CJCjiY9+kuFAvd=lbQX4cgdv)YY2Y;$xYuJ z?YIc%(kWz{3NbXK1|U@i4e)F9@DAu~Z(h+ry1BZHosbjm-(t@xuh(fO1_tYwUGxoj`GF$m15xd#3ut05Ql)DMFHf7ip|%!6aG+-~PRw`Z(jh>2No1t#w}fd@G;+ zNv?il6XW#?yqQQ0A?pB1vpZA&i|gJia+ouO0z4H16HIXp7TRoG%3T%Bb7na6U(FS> zV@y?L^es)-H0YXA1ak*izvnGw2kGEORn>e0(Q!`+!oUQLQImSJf*dmQ{oSdeA>-Zk zN?d!HuxKdJQdo7iblsO;$C(3fc7M)J=WoC7a_jbq42m4yJZ4T--^5khA?UPp;26>S z&n#NdfpLuZQ2ffKTJfyC0J4qXy3KGVz0Zm2*aQf#+n5v-Tq1KHe26zHf7m zTxlGWJd7vzVpIe+c05^^sZx{I=1vSZGldb~;}Aqn=fE05|Izyef=t%=)f+J944 zfMLMCCtCL2=I^Ox+YMYc6`p4oFkT6zw)=S&k5s|^8Z`gG92D%sRKlcp)_oCqEIv5= zn&&BNoKX4DMH$pWnm(Dn9jxfSI9fJPqW)SZz#}AB0{B&w5qKLZ;q4wmo=)&IP_)S% z%m)F`uD;*&(+$`NB{;bJ7#39@O|9X&R`@=xygUi&ZJ4<0wY1huJ8|Xa?PsS$Q1q=kZr2F z8D;i4gJmP><*I#?%xA~=$c5ORVq?kzmOH*Shnw4{InOTI^ZMG8nVv90f!^K;l_~wv z^|WRl!5?|E!S>$LHEUW2KORaa3q0u|F8qo)Ngsv>vv+!{juIzx6#=HdBCB~XZC`4x zXZ7FV=s!dwd?G$SsWUx>0wVh{!Nivd?ijvyLU+C|g?bBXE@5Y)o|fXs!SY!X<;2)5 zD8B!`T&^Thh*=rC(_C|lfnD#(K*|A$dul}&bpIZ9KqpGxiST%|KA$lod2-(=Z!fPF z+-6g$!;(}A*-1fs5cdeD!`jE;cQp$M9xk*?NsthHBzXGOk^uL&lcn7W%%T}Rjkc`u z-}DRI38goaxQ$Wod^r`(hCL}EAIj%=_^QnFD zZ}p=197*h-G7fM(Q7#~IbbjK7N+=)MEizxL_3N$e@85fkv2~q1@}%b*kSrp4J!;qq zQnnX4z<-_&{P@1y^m4Is_5a0w{RI7=gXm)mBxl0b_ud37HahfUvXs>IfWo(0jyxsw@T zMh0tGhhxu(C*PM8lkonSdSi2=i(y<`^1#c|UL=$Bm*_PI>#1a-rPT@54gmnWLrS{MUqy-w(dlN5y|* zQe-RwufQl5d-g6fZ-|9i9KLsM5(x;VfBqVN`ij&e42dWnEi8OnkuHb%J!Jg#uzRa8 zceuKRzKa+Wg4*-eH@2sG*Bqr*Sv<(u;!gs)iu&zMv`deoq%>9aUO1jKt=CcpG%FB?@R~~rAxZc1u>Y#O zDqB%A=21_vcWgalO-^oqFploSm0sWuvZ*#;kf*I=Q<6#i73)r|0Z&jAYuWeuSR^?5 zrbXeo;NlRa8}AM|Gy<#r zW>vRV`;+|$cO;xf#~}OWNr;c%9s5fI+x>z18RteY*(mQZA`EB_8zR|i2NnVgAHu3o zMktLAKvdpP5C=abum|sOIeo4II(3($g^JKMVxCNyyG*76lCkEOZp)!9$-s9Bct9E9G{jOht~J2H0kmXd04PaTw|Zd%e7hDAsY@kpaJwgQ@7)a?^3xP=qU)zsk0dY0lysH8OB&zFZ&TlrTba^kN?Rtr2d zlIby=ke>HGq#fY|)P&)OPL^h4fTcx^acuMow!0A|u1s%(?BZ2f6l64UND2X==x4u3 z+*wTjR8A`QZe(!@7KqR_UVA?a1v$zesZNh7rO^>2&Git2SMhK$0C;(`?>HDS?b`S^ zh{PkFL=)@>A0Oo=>xGno+&Dw0(Srzx_g%#&`5bY?wa~rG?a(RC6UuwayS~VV_wSO% z%&&*wRZaoJ?LZnrgxj-fO&qS3x_Gd;{{G7bSAJv6L~_DXe&MDK!{9Thy|~O3h2yfs ziDu2(Qn?mH-~ZmSe2YD@Z`~l}E9UOV(0l|LfV?T%OvVHm=vK#!Ww2;x7D0?dJ3o8S zNkTL@a&c4(TRelG3yEtl;onvjIr~GrEJ`;QEDG*Ba-p>2N4c#E1cbi48cvQiiDuBY zK6zjbR^qUg5aW3h-TaSh!#yTP0evypes|G;*uE5ahY$s)QFj=ouEtT8xTD6w_IgL1 z?Q1Zfg}N1zSg7fp&JtZvy(Qz8^B+(yceP?-5|;Jx6^g-5 zz+CEjV(I5l`QliqrwvbQZL5{7I!S&r%!qqC`qi~^oiP!T|6}k+i6lr+z~S^V<7vF- z`L<_#zlZz*5F5feWGKz9T_fW`@k!h(*zP;JiDhq=FDo2+UO1*a!v_dK-}X0kV=}+t z$$H;SNO(f4qdXgv5tYs4_1L_|5-+^0-AoXGWhRg@?xyovdY#*=RQs-2+Oja}<oBQ~)6 zKG#TI^sr#OZa?!^&<(b*QA;mV>Gbzy_`b_-i1Bb;0lOQ*cwa;B9Ub@|-?veLVzkUB z?7Z&{R|^oM4uDhQ>B0>{E)Svn)HVB= zSQx+>yfI9wX#O#DY-obWD{az;8aWa-soxb;_blvGqg*!pyGMW{-FNar5IS5(hW$Ue z5ALpl#jbQt$7a}e`oq4Hu5Se+1u>M3yz!F{_ka?*~&lPB~bQ7vqF(@e0F zqT`xjHB#;da=B+x1~U)fw$=Ei_up>m|V@ya|@iA8C=sCBgmtX0|$&9pZ)k;6O&E+T^%UCub-4? z+|9R+$&NS^+j2?F9Qe8C3&54tWhNL?!jdSG0gzB!64T){nc0jmA-rv4to_RLXd|y=P&n(Up~Uc0zI>|Fms5OYXKlhwS7X zn1?i4bd31e%)n9|N9927F*!ZQ<aS@S`%M;}r)0???au2=D*N1yEo9{o$Wh_oquny9ogpZsiCHm>_b><2NMgKJ|u%M~dxSV_O z90*e+0tWkI*uZP@crR|5%MYG%Bc&mE8NJ{9j2pcR_Seh0-y>!2Thx`dfVeDbm)bO& ze(B{cZ4Hm~KtPI7OSXrb7Fd_q92i=1Q8EjM>%_Dwr0;tgd)K8j_=~1_(oEL}Ii~2> z;*HT;x;`u4JKrDD-|c!fZZ;LHiut5u3;#951XSvz#zv4ymfB}c@FZQ*eA0O)k{|S; zc=K$$O=gF2_)sR`psSJpjDl|bl38WpSlGpRF3@C#%8X?wBk*Zw_m4W@@TM~h`ba<+ z5JEZ0TlxuziDAYWyiMXbUXV3=&`Z!3nw}k+nGWF4CZE`mg=o-7>H@Ub5~ z(R)m#ITQM+K0!C0p*O-y2QcM_ife9`l3lf&Ic!Zs2XODRY+q)h%P3_-TbNvb;n>acbbhk;E4IIva!hRT*BA!zD-O$n%i_N;EMFD0@!I zIYnI#XUNw^L93nAvuSVqY>H=j6-f45S4FAl?}-_Iv;^=2l(qU=w$^@ot=%geBSF?X x%U!*@+I30XMbh*+EXDp`$u622u6(!OU~3Q}vAozP%Yc7HR#Hi#R?H;q{{i<)?I!>L literal 0 HcmV?d00001 diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Assets/astar.ico b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Assets/astar.ico new file mode 100644 index 0000000000000000000000000000000000000000..38b6709a751659db6d84ef77d794c46015e49f91 GIT binary patch literal 16958 zcmeI4`IlAImB*{7syEadYNTEb6jju~RK*aLK?W^AN@Rv&9t0TXequqbS?!EUp-*eBs@4kZ0uidf^`lVFS`0Z2I3)mw0@vaGvuQ^x|oz}ko$}Rd^54afS1-~D`Yhv1Y%7eQOc=}D zwYSo|QJpakxZ&?&Hs+0EmO0FGEXf_Gv*+JX{kt3>M}AI_haflL4SAjKfq56Dj%B`t z&%urumjm^IGPCDX(k#E7^XtB-56{ELk`w6a`j`Vd#^5fF;48)N=0p2fY3-HKTN{@L zopbESL*ReM4rZr6v;wo!sx6?+rc)$b*JAI^v5L70&T!@*Hwdvoyn%cJ=qehO2Yz@`$y&D&-qs*9z)Ua?f zWjn|7&%K>8{PQ#8ZlLC6J1NsOiE5_aL=AjDS=Y$dQzKhx}@`~n}fFI`MBrU1zn%(nMCcoPExX=nR<@=h?4nQ%CwHwv!v_rOVl`bxw7^0 z`cz#bU+bX!q#LPa?LI23xSJYhFQrV!SZZ1S05vdo1Fx?h)lF?%4pY8wCbg{FPgS`( z>bn0qs$|W(zx^_$3Ztl-@15lQW%*~-TsMzehwh_9Z6kF(aE8)_cIv$ENlLJf;<<$% z9tfiGvGq7=pvq`&a%22h!Cvh>eo=Lm*K{9zk?N-}q@Dxkn0FbqZn&Rv6KBxq?GI7I ztlRYc^c?x2UK_RL5Od9-R_1Tpcz|jqPGh~=Xw2THs9|78^OeXmFW*zvweh_YHTBGY zMt`2zYWdOTS6YgB!SQq%k`mp_YZ~sAl{(sAch1YPoGQHO(KQ z)@9o%$6TU+c62X|T6-V0Z+?&(xR#_pYW-g7yyr10%wJ6-=dGqOcb}xun;xL9dmp2g z6}zZ=?-N>EZ7X*2J-1MS*Ay0Rpw2r_@O>Vj4qjV#{Q$qe3)DDgvCToi3|6os%6y=I znAgmkIo->uv()>@o0Q<%uAjf2#_-RUHD;d^{{*i$bGT;a^OI&NzwBXHi~USIZu*F_ z_enp9;%Ob*&wV)S%oL<_CmLD_TvFjRn zuaXPIn0_PIL}3B%e<4=O1hzO{o(t78#yHPqj6L4^8l2EM9IMs$$+Yo5v8JU-q-{H;~l7oPpI{ z{Zz?b)ANL{C7VW3lJA#lC>T!gA`gfW_Gk`d?DD`r2YqnK`-yA*b@W3qq-T<9`=(Rx z!>>_}JtFH0w{E71r$45;sdH#7&yVE2vgNj|yjOjXucxSW(?ROt{j2$wHPpKG81>x$ zEVV4To%+uHibmczNX@)<{Nq2Nnz2&?A4GF7jQ%0+P!6#F8SjGK{~j{L8TE8%EIzh1 zz)WqM4^gtvK^2)i6;|!0&fQOFZk_-6oS*R2-%ys%!1lWyr2;cb9k(poL2a83PzCGX z|H7{+T|biKtnEGVHrF-IFZkV$#kCH7HCnH`zUZ$lc;-|;3X|7DbE-3Qde^ld(%@96vvEMtA}rc)9QyvEJ#;>u_DJ zF@xN5^p1xq$IpFzPkl(qT&=EYTC|n=&U`{W-+G2>x+ZANckX$bH9ttzoqc?!ZPaJU z{N&lxfBGXzRA>14Xo$vr?|o_-+@xooFvYcy-oDSZFZFI~9_%&`A&tRSDhiwBHs_J$ zEy_iTnwD&*(d=!>1J0;!epYNlD)7q)tubnd6 zt!Z6#H??m)LfKxfk3;8mKFNDQ?S$(*f3wH=o`w@KUUOL^x+9{dU7d^Ok`HA@v~^%} zp}an0RQn>n!0&h_K}0SvZs0vX!S6wNVTakF(Enyc4U zciGRZzm+xcHMEroPOxjNHuKU~MxUGeZcmh4XbbBN#!wUvFeyull1uhe<;^>(GE-w~ z+OjLBmxmBHYtFB)$Tm>rf_tfE@tuy})w%Hc&C}WE1%HU$=ETfptlpxv^)_%I8rm$yuy@=iLU*lWpU31NHFT6R557fJw?9i&Tc4qdd3RA|wvM?1?WM*>WE-h$?j2OV>^N1e zJ4F*;`X_2y_pNKv8oUzW0e?>WmRB?v?zH1CX7y`6#4XHz3$e>J@C%#en=(k*gO@42 z_aaqod4}`w990dSpo-RUCimj^wtgyKd|27bmpw#XPyC!FU;Hg)__JauuY)x%hS+tl zGt53W`l@ftJ`c{2@4*|@UbuA*{9deWV*THw%%RJay6Yw7Pki%?#)>J6seH;ZD(C${ zbRV(&VJaMYovwTRcQp1He*R^xpl5MRn_BigjlR>`)`T64=6);Xf3Ul#T3S8Pw>bzt zml{0z=bAg&y{(#r9d8~2Xmv!Q&^!6ty zfAn3=LH^tCQSwf+7f9XL+;x^_UiyrtGyims9s8dRX&+|x&bb(NVG>?Mzwh|vdM3+j zvPMgIxkz*HqV_S4Jb&37_|Ig>PbKfIBl;Fn#hOQ1^XDnE?-k8M{^&cTd%@b1H0{+- zY1$jVqn;-}qPhoOrzGzSRs9Qi|1;-(5kIt6?}t55nwxo*S=c2%mfz{iY~W`ebAAao z+VJec3x31S+?hHWanmlUSoing)M@=FeEtLCWhHLAa#6Xe3HdH5W>(WuWe zg|U2|9itIzPBQy8Bf1p`!|AA(|^GBM)Z91RvS^iug{A+*t zHLYZJiK~9`6)pYnOS2W~pZ|`_W_f zx&1zL6Mnysv13{)}euUOrQ}h5yPQHO}U7 za!hPHr6=_tNN9yC|KnG5fgnwCZpB0bkhfq4{i1Yr(NaSuvL94v$yP zqUf_Y=8DfLdschZy&VgYQ?+d4}p)XNYxS&FZrS&Oy$sA@ZyQa8%Ut)P^ts73uNhu&dhuG={pW7<-7{IW)~K%~ z`pL-)DZP*PKmI%*`Y+}^uW8N_yH5+V?B6!B_F$D5=Jot7nyU7e6MawXjOF&@a2l)j z>Ivh4SgoNyG%vxAvq^H0tR2bw-v!F^=Ye&i|9hWNx~9(Z2O4W0lp}_lxrWb$FV=ksvw3M;tHGp8N1rm5xpdocVAEd19WKQ}D9uYafy&4tmi ziVp(jU_M8}9Bpy6JS;!-mCsoEIVI2IueA=w%p)t=v*C=PHaU6eGZ{f82oaGCD<}^Nl&W=B-z2t!z;Mp|)-d67ZH)!>T zf1*r%qnj7KuqLFn@Iz1wXZZI@(3D?1U4l>aj5cA#oa0t~Jmuz(2FVb6bA&iz!`<8il)+jrVbAjgiS{`S)fder;%y%cU2JDVoKWErFcREMM zG1>};)IehsGYY4yL48|pWe@uDzW<^ebmz;bIoEI*iB4y?Wbqt(^&yZMm6g_$+w zd<<8^MP3J!YHf3%a{;&HBSg zYv^VpcoF?vg!`6FexJ_cgyx9gM!Y6|WioPhRrTF@?nymQnp<;AYK_14`5ttc705jg;#sCN;_5c6` z5DXnM#0~A{oNZ*rGoK)U*tE5DJw!v97%yM zs!f*cx`Blv1v9RR)1NuU4U({C)HSyJQR1A$1I=#>?`)g0me&6T0b|K3Gzc6ck{Ha+ z0nABKiVr*6Fw)T6BG4P~3oD3hPC|zkU&y6}bZSTrC=kYuD93ZIEFDe_VYTA<(Bm|X z8)JE4oHR;Wc`m_o^2o}^;jPx##|o7Sl+fXbL<-Q+tl`WjX*sC!{9r(PUTlBRG296F`4~>t1LT4^F4DFd5XHSt-O2*odAJx(vh#vHOHh9 zIQ%M|&DMbu31VI>ida;$5$mWau9Vy@ytFk5>4@URI$^_$D!pQT+r!Og+okSGn{s}mz!d^39TF7J;gbkeYw`9=UZr6$Q3B0DKL69ekADPMIQ zc%ni%dR$YKSv|9-D&Uz^D8wod7#^Kld6U=OQ4g|tfz(9^yHnl>4U?XEOGL5Lf*Pr< zsZ)H|>B1F(s{KTeDyCUAc2|I@fNmhqBSr{9>xb|#F)blC~95^OmW;{dDVCyQUACf&3JJhQ9V2>?b$8+qc(h0Mmw;0DE zy^JD`#rE(lO%)b*9+WtU9u4Nfz~KlXWyOa(*2bTdG4xH*F3`5>z|k2=R957YFstKX z0j;St)HytA2xw{L_Y}c&8cc0jxXU7PqFC`!r&*Mjk1RY}!L~;Tv``lf{fO~+vLA=% z()__s2Rn~#KHc=NWtb8KJmD1Z2b0S7iVD(=#B{VAUTkFP$V&mI5>|QqnF2s)^;aFPQC+>_#)?<4on7>_+`NJWPzk*&Mqz;K z?t@i(Qf;&h5jY0JXfP+i^hz4g$sjz{Tm_`^igyC7ZtKyul?L>-*(^yvMq&+HqUg|^ zuOzP?yJ653=3+9#PnL+WMH=B?N;gKMc#HB%c=jUQgBv$2__gF5irQ)I2EA5vf)$M+ z?*+7XyduEQ^BuG;+cVRAfe0iv|$K&g2>seAjs5G6{lj}ioD11%QL*~uC=i4BqIwPqIy#|x?{o3K09QfXsv@((uubytk}yK z_ySqc#LTCVT_S_M;)&Byb2G?Y>DU%Ze?jhcT-u0KEGs3;6Ee@c^LA_@u)N9RZUQlf z$I+OL1g#(~Se0=!Bmit%#h4r?Y}v31S8wb0ymM%1*G&ZvtMSy!9*FJB0#(M}RQMUMy^WC<1*p@H`}RyhI-}H6QW7CIC3uzvtBqjaKoVBSsqY0u1Bd)E z5MFWzsbK9}vkVX1xi%^iqm{d2k!Uhl)G+Q-N3Ms^xL4~nV?2l}`a>f5L*dAJAlsK*7s(*UR#&{U`ZLF_-^`8i7+fx*u7UgaOt^|WKtIVw zVza*PBvUCtmZkw-2_@+YK5P(6)XE2Yepv9NH;LTQJb;dRcg$(QPdvCtY1)J-jD_Q9 z!-sOkmwZyHP=q%WLxD&Xd}U_rxeO=@LPm^YXbN1Hc;JpTzL`pGl8q?kT9G6wi89C6 z0g@*cZREOs;RMNa(2f#PFRavTomyufCLptex}4JD?!lUP8`m-=NH~mOP-#HH;mZCU z^&weIA(=-8$5)F^LaA9@lBNPi z=B!lkCd6q_->e6S_|*pp@j&*JuG-Xj8L0BR4I=3z34T)*K#V`+2as&q@fxFCzOKf^ zr~y4@24w`I+7yFeTz|y`)=d=}+?8x8-RQ&ZMj-V~rVm*^*o8Ir**|YLFJK%nDgfQa z&P;mz4B#dg#YUEKD2c^mq&+@DP-|%NjJUW`$4r1|hq4Heh*B5w2=!o})?vcUTY)U2 z<`R!km9-eKLY5EU$8R26p(bfl^h$Y1QBYk2&06*wvF+j&L)ISD0%}U>^qnm3(G-Ey zaMoT*Sf$H~SevjV6}U<$Y%0;xML~oR;qpe?S(i!irgH&6RO80&evG)`Y-odFgrDH zqMb}Ta(q`z3N(-JAtaVZxqTg(=G;}6Fh+-S7yWe&)Kvvgj=bmxxJKDq#O`ZXVbypJ zYg+BQ4o;IO*cO@XnBtDAI0_c8q=H-6E4W$fbR{CHg%=>W9ZaDQINQbE?JqkLIX~hs zRv`d!f8ElIyR}b^TLJA!&5zZd7i9M~LYA2nP?oiMGBz)e<$dTa&4a5Rip~H~1NYxH zWvD4<3?v}0&QLP_fN01^a?=Y*+cFK*zC==|H>m@`YzqOgC{#piQ<%wXZh37R_DMm1 zB18g%B)oHljAEyqCG-*CucKM;lX&SsPK=wjn|5_=j#m|fxk6noAn$ZsI-sJ1dv2cc zVhA61&}>oDvJ-$IE4`=SSII6M-HFJdcAX96K6F9MjXr#U$onHcNc{MjyQ>sLJv;=< zAQ$iiAei=yui@1egmc0pS?ybeu;<5%=qX8l0_MfGYa?TzKZCt}T-PQIP*#BQ4(zRF z<{GGbxaFF0OpTFKo;hz*Wyp}#Q3z6PxmYo(D#Bg!l`JMLO1cQX|ZlDf)3J<$H%`#ej?zV@vu3~)Pm!>eeY6N`)1E`i4 zL3sy+3{|xZ$_g-7$nzX|o}*+!juJo)W=58o&%EfMu4`m{eJH9P&L2OEQ^(IjF@TAw z0qnW|ZRjmd1C)WQ8UUfHO7sn{!TIUqhCen}3RT6(vmBrr;Mzh7K$%r1sqPa>`KWLpb!xG5pJq9LD(zb)fBbCuqH9cw?KjQ)Cagv{`sl$Zsm?>Eo^s z%;?D697OOdal^k=#{rG_A6EblKp3dGLLXK5 zsT&D5?aZ*S#26jvVAJ+Xuwla_CRPuFnp9ABmys0(z>K1Q7b!zn> zZhhb#=*>@~s!I@Z434kI@`Y1PftaK2naE}$pz5M#AfpUrxePE+3{7HodI84|pTdz> z4`6v=8F@}1V$3cu{`(^pUO7`V3o|LaRV*SgOb01Mh|_XgscBCJX*$t?h%5r=B5fto zVXS-0wzLu?DdksU3r1!}RaICsT;U(xo?*=-q1Qu`i;eOI`ZH|Vz6P5&ufn+#k+Ta6aokiTY+0OO9&TzM~nu#K(XW0b&ZTmyz>Ua z%{w!6dj^T>TJ$3z&j_2ht-+QplUToL74oJQAj%p9C>sNI@XQm>Vrij=(eXjt_O=I6 zE}cg)FlNT9dZ@eeU=Zr6L|*inT4P`eXHJ~O@z;;w@T-SV^-9=Vw?#qb(e#|qU9R!@ zr)oTUAOo_@C|n4t14A0#&Qh)r*P$YPVfDtgAikR$O+E$ambR_^&*F?duixI;Qb0vLXi{7$~SN&qEjJ*1XO}J# zQMJOBKmbo5oaAZp3yWv#RjOdf_XpbZIvpg^hdiHf4C_}uB~*CFb-*o`W+*FZc=MTO zKIc|dQ+zkGf2&syVB^+x*tlsG=FU&!>O1ep+{`?V9efq9zw|QBo}5O_Ho6Igxi5q) zHx9rIWYjn{5z4B@-#k&_+plCG%8ZUkim}qGs1lxT+Y1K}02jqpM5Zi!_#0{Hj{M0RZ6P3o;RwG$vG?(WaLdEmi!UX8L>;pFid9DV&1 z=I1L@*fn&DB1hd+okfv>nX&IciO=o>mg^ir$VbM%nD?uW3I(-}G_|(N%J#7UX^)m7 zuxX|WH;LwHAzJnW9J_d4Y9r^N(QB!$85v4Ecr9ViWf{uSIBk}h%d&xi93#UyCdNA$ z7|5E1AcS*gyO_Pu!-?q%JGPDC?t899U00~9E-uV0;^^UXICXRyWmyAc?jIC6p{f}V zeYeEkS2Iv1SJnHp^G3zANGkUNl_d(sV7^~DoV$w9myeo=s27X_G!%Wrw|BC|m0m~~ zmNxRJps|u>QniaJA#99n5IUV4-EP^;<1;Q? zC^2`whh7bwoHmyeciy=LmtM6MfSJ~{H+s&VI)~#&&SCoW9L~;F_~fIEADz!&?xTq$ zO(EMdk{yUD!L+_@B2A(KLZT$bwE+=WbsB8(0!JoR5P^{A8BWd-CWi?>wtEP}L%Ere1JLVLP2SG1yj)@CTn|+ZloeyK zYwp;cIDP?}HjN;s3VG3SX=7x37^~NgV8iAq{6C*LjaN?Rh|4+(#Sv-@^pfo6dU)eU z`CIRkIxWU01Ys83e6t7vIM2w$cUNe+yPyJQ{3;jk;rc`W3B>WXX1YB? z7(bBAbM6srV#p+Fd%G36EAek0gHELB3iw~_wmLH9sI2^ADwoJYVQg#Z0O^+|e zlb)veX7N8D9w0#q8T6ofG~0~>CMSo1?Q02HmZ2(3WH}s^gKL~Wzl^#faajm(?Ui-r z%o3h^`gK%Q39cDM{}AfB0&@jMjknx1=H?l^JSsv(7a+9#4j*0IUVdR7IlA;4qi`Z=65Mmp}h~`zTpe6_e5`%HO zxl&#mdKmjZ*6mxmiZ3^9Z|?8cwOgwJFqXR&&Yv&c{R6wzb&VqTV^kGTGvo2cUI(K9 z*QL4NPiB!x-s!`eZt!VEM`2vnw>%(nOC-jU?8_sq(>xg(j*$CHGpu`1|!+fd2m6%-iP~ zh#1U-3v(5gm;Ktos;*I0#yVN)*5@vvy3GHImsR0V+zgmN8ke5Jp!g1FqYiyBjuH22>^C+&MGJlV#M*4w6$i z5%BoS6^@=YPC(5%LAyqRD!e`+shS)R{tVB6(L60khVlp$l&qF-_MsGFIzXT8U|}fQ4Q&5$0QGO z9KA7iZDREIHHvGl?dRt!^h!oX{yBhdw??NREOZ&4-do|>ql64K-gG%|`(^%)VwMr| z=H}ATWAk|Rr6btBa}9_XS)OChb>rwOrqQcX>{!0jDZxX1BB+F6q(7zR`2D_Jswc;N zV;eKuH-t#zDa8vHZcjZndKw{YmvD9{9NdmXUT1dSGm}menwuN9nZgc~ff-m@sxUWK zf#8h~HkWy?o~-c)U#zhIM2@`3L3sz?c!}_t$7?Js8^h-r(5o1f0U*XRPoKox+!BzP zOTwX{4t8JK7x5KJr{yqu4qtaEQA0dYB;!mP-|AM66m*o@jd`*)jvY9AWBFI`DF=|H zL5hq551H3oj7Z~4)`Llw@KsA;!eHFABSTSSsLC45!1;5P<0;DwhB5(v|4fCCev@&2 z*(}mDP@ADB3LH9@;}eh6caY ztt5hkP^g1!@@T0qBg35(3TQ+!TNdV*wnD3|MqqU)<(`ThLiBTmNy>o&#$)9%fGal; z#zu26o5`JX=Soz~6tEq0X1>P9zh2_Y&k<_m4QU|*0eO~VvFhM2o&dhNzed(5o0=Pb zvln{!;WNhoX5@K>+josPBef%ALioTT;EWmrUu;DAwp0MY&-wV;O6rQ$Unn^c6@gNe zkkNR?O655{Et=4Zj*Ec*WR1{nc#9-AZ3DnH>bl0#Vuhu}$`pLLS-|n~(Fz~_-!)!7 zZHh$y#zIAxO#P4-1-^4A$0xtdm|rrBM{M2p%BwRtb7~eR5v*GeA&rz8Lu4X% zh_QR8S@m1hjI(F$vjL`{`||foeB#@{T+c(kr$zxqmz{}F6giGvDDa6#83&G=H;NXy ziNGhGIA*4p0dqB=pjwG6#tBit>slh5m)MilsO-C$c^4A%24U#9PZy`s5l!1nwhp1# z&L~sT2!2z>XE})46woBg1~wzh=MFM^Uk01=PC!MGZ$)n>}wUi_HquU zJm5CqH$bs4U*%bb9(V9RzXv@0Y>m1$R|gA=J^WzbVNh1!-do2La*(1J!Z8P}R9?Q= z5^r5oHI6QeZWhDIc?3=pu~WfGZh<`*g~FW1O3!v4cGKK%DJ4o;ify5Y;LLl2CvEaZ99z9>3)@<%!Td~c1}c{9iG z`jI&tJA4-F*9_u{4dj$;X-2^YkD)83H`Y5^32AOk{%7Yy?#7dpJV+__kC3@|i1;xF z!s|_cpDBFtfKCZ1fz-yjP3UHUy#(^vQiA~O-eI7BcBTXa_`>%}eDZO^LS-O7974li zcs8}R(M}HKMULYa3jFC_#!E+yPM&=77|OE7k6kweXB<_~IzWWS(G@ba7@rgEG+Kzj zbvTcx6w?VN;DOm*1;D!uc|}{*cv$h+oyf|P8#QD~ASIioz=DT_UN%fHE}0_CoG&pw z%lPQmDtzme47O}{T@u6kXcK3T5ozyxryGAvu<{rL&hcpSxK`DC~F^>XTEDWZO5QkyWErP->HI zC_*^3wH=p67xp2Tcm8yH=7EfdzQU~3EbyKE zT|9b#%rkgW&K4L9%xa-oWvti;v3D{6t{MGV4?lBjfzEIrKK0Gg^_GjErGczSGI=|r zI(?_dOr?N0W!rX56RSRvo>-cP02=7;q~lL88Y_%sa%*8!`=ewsWHj;_MK^lP8I0k+ z3iI9EYg_r3JRGP-wbcM4YmBr5_+vFQ>Z-!}i43PN-oFVl~ z3{F+*FYzr+fsW?Ldabyq)vY9;~modvfA)By; zkA8N&?7;GfjJ<%GVxLq$as!eTLNg*sS(``q-aS{I*D&aOv(&GS={-8-G@M=ZJ!DHZ@IdyA#u9p_ECZos zq$U_4CF`zYnpB6Nn&k)-75F2&$g>P<*RzdXg$`ffO3JA_e@?3CZkST4(5oES7}2w; zd&bDa!NGXkcq5^}Ak+vbbTGMMMZu**wWc8>_S%S1N0)=6QG7)*C}hxZ55_OPV*{qf zJNT~;9l_GFcmfbvn^*j-vj-(1)E3(aX8a(u8&^+OT@+lJrzsVQ_9m+?1WKaA&JK7;q%K7il-r41et zBs#$3mc}cGOb8#ah*ku(8C%;)-vqKeNO5I1p?^05$EYM-GS;o4O=OmZyN*)&6=bpV z&xwa~)YNfUPlS^)Q9@DsI)eP?o!5>cqYj7&_ue!XGSGZ~>z)bRbwxj(JF<-5`_xf< z^9S?z_}`tzC%$+JouYYRW5d{n)qVKP|9u_gI=@JC4wdIeRMrYaln@%blhcnwRB|hK zqs^j&96Zuz8o6Qy$+4709kfkEN=VQM6eDW`voGSEC5tNGE4KZVa~Ny(+&woA*4OVC0A(4DpXtFq50_;bzV!Gxyn1xm@sfe@zIU$&EziTI4H;Jk z=@@y|yy=reYTM{HC% zs`1)5c3q($VKSTnK`0)A<6v8Z$X^~a=hjX2;kumzc<1|H!A<|?Foyaw+;{5)KKt-# zF*u`F)~M?WZ@PX2-*{#f_WkfI0AR7p_`shX1`*+o>&EcQ?_7t?6E%8F__x2b0~s+M z`{5kE`Q%v?1>xuJUyu85oxF2bb`l{_-#m9bEtcxaXEB?7nIgS8ndZ!4oAu z{gvbR_y6KDj1SiM%|HGTUOaR`ID#~ScLe{qyOaPbPWuy-1Ocv}ep z_uf2-f`EH(9K^26Cvo&_g)`@t9o?Zm#$boBYx@|wUB=!YoWacb1^nn_7iVWmeB?JS z!!4I}@ZrzAhQImNNvs|rT(`9!*X|s}7xtb7h;YM>0#~fham9uL%r)-WJ&uLBC9Iz; z@X#aYFf-R%L7Ay!V?5s8Dv)BBada(KpDZyv(l=PrPV@U5q3k!8S+HH>W=%u;Pzoqy+tbNH*TpE56ss2RJr zW%w_@wg$iR%R4bV)B!+v>2Mc6Jh%X^y6Dz~Pk-$^{^DzMc>2HsmU|V}j{}EKE#t`- zmhk**J^bNcpF)u{F5i@4e8jw=^mE^y#dlv=1mzhzIpd%I!2$f%M-SmI9zKQFkIlE} zG>ruER7=T_tO26gWAb_x+H*RTTh>}HNNORrheSoHOJ0(aKTZ_4u@T=E4_*(9>nw>Zjbp^cn>JI+nzubX5 zZxlcoP;^jn!-#!kJj?Jizw-iq{ll+2o==?VBFhPNSz&mvfL)DKHQK(gL|85}Tv#If z*_X~n(imA%3v6T~SqUXht8aR)M(Q=Z2iH_RDt8pEL&%HbRwp4zD=Cv5k6XH=N*6=Q zxP&D6w%aFBmW&Vl!!Zy|nxdf&0Sw^Y+g9Vd&z{4Dg$hIkzWDe& zUU>Z={^U1z0$_{{8oC*o*TWNaMD!WO{mwt#j(e`{$0z>oBtG=@!#I4Z#A6@326-)% z&dqD;!0rdA1fXk`IODf#*5=Fvu8St}jVlZgbC_AkF}s*!W+}%H4lLpLnI5vNz?*Mu(g4@^<@?5Q z^Od8{7?crSJ=VkYg$h80qi0J8jd`8{kgpyEZo75_Z@Q|$(K9{#)i=K@gk_Wf|L$59-`pJW=CrE;^@=4KfL?ev*U#dpOr;!rMDdsgZ$EaHJg*$WHFwEo zC5Q1GwJ~LnxwUJA=(?66a~R`ow~S)%bF=7H94v(*B0T*4In)fi`Pu>Z8rS8L@$>hs z#qdz!8eg$>7;7dvcP*oLj-6hPb9N$4+D{#FQ3I*>NP*t!B)VJk5qz*Y zceBCg9Y7DHxxI+x5*sQ0lcJ>RAJKMX5ZHN1(fk02Ir{X$o`cYBYYD5y%?jX}&D+5b zpRRG@%rb7eVgQ$I9K^^_j;gBh(&1%%_xT0L7dbOCHm}KX_jM!4i17Va7P0@(5-1~# z4ghyvGlEWs@SXkhICH+jJMI|AKp)}B=jZUF(-p4R)WKjsH95IPMglBBb!yQ3D!+sr>Cc$3k`;-HgL=tjR-1Ot|Y%hNW;i(%|cT(~|>j4O=kLsmbMv}Vm z#uo$F-XwUkx+IUzY97E&q^4LW-PC_&`N{YP_pigqPzR{&o5dLm-5Srnx`_QRp9kC3 zr8>ny<|;q5lS{TG-&U?0-E;L2{^{E%uz6j9C!U?g?1j>r&J|jzrB>7HMYVwc@K2`jp?`HLR!{b&NvYwSU{=V+=yLW;1LkATUceJC&ZDkj z9v7EGu;nS{TybWm;*hA#a+wRImaU8`t*Zs-45>Mz$SG?iyqedUd+8 zYzGXO%{f5Z>e3{bkvb=wln6L6U1Mf}QPqqiCu{6KRN~LSdLAGA)Nx$13HaS#*$U}6 z4*o2>Nd*VLGTlfKCVA1)3|Cj%(_Pr9Y2F}-5M>e!WVt~p%? zll~?L?|We$2aZ&@d=uf8YbLO7{|q`E!u#H{1sf(atefh`SD%>07alo=5B}<C4@b}R@TXrqfp_0MiEFkEVt%p2NB{OTzW>5` z0Km{-f%m;@JyuV4ux_d!fAg(#_{!cBxb2!T+_z^HF5f!Vq=w{{Qj{{^Oq?#QPo?!LCck z@Q0s1h&`9)n4Md~W8a?!5#h7{`zX$wnZrX5AIJakvm3E%Bjexv;Vbwze{=xvxxIr2 z?pk9;&)^FyFn;6bH{&hW^y6QD~>(nOS`By;E2{*^h5MaU5Hw2siKS!=w9- z;l+b9SX@}bM6rlHR}JAmfBF^t-M5b7%C%km>VsqW+}Drb z_y5}gtQukb$~&f99Q^+KHe-3Ii}(N0EBKR#UcdiU+ z-~VwCUwY&)c5MXiyLAkw&MxEk{`4RK;Nd5y@duwgfNwr=O2o2Ec-mrW6L~)Z@TMrp zQsiVqn``_N^niBxMA07Y~4DA`K1ik?-*8;0@iXJ^r?a!^*_bC1qopaa}}!x+xY0&@#poSm!i(%}*>9A4Ddmg9?$pU00**Le7Qv*`47@YKr-c>eVg&mHRFwd2c}95yJq`>J8= z+SI|Br4DYqb`^@jA>>7l8!sQig(br5e2Iaf4nF^#3pjG7#@C;^05D^8*j%+_x%r)< znFYqtvxM0NY0rxkPV&6HpAvKlL)l#g#F<2_RB4JD?@_v9NMJ}^fr>uQQ$ypDsXl-R z2M#acvW)`(3S2TWhx!sTH~*=xoWgUjnME64dU6goTsn*g_DteS-#Lq+fesEHUBy|ZrAR=rY>!EMGd1UNUUp|574|ZMEUsZ5X z$PDv#v#N4|l{G&`p-rpHFQwq5CBvtP!>!QAWRc=8DOo)is0+ieJOK#Ea@3W0IWIGY z2Xowf`2Y@{=wjasb9nQO6Ud11qcb%=^R*dhN80=JEN0%l77yGqf+7bV`o?L1OaxJu z;rwET|M9gsB>*+=12pUR7Q3dvS~Z%vD~JC63{_R*#96airEYdXjN!^1<6tm$ZXLy)H#WJH2%q0Ojj5p;H|`w3 zv#%@w%s6y(5icHD!ke!f!7bOW0sw4YKZGCOv!+p?xiw2fc<$ACoSrFh=XGNk=r=zB zcE`12ICr7MBR`li<1nMH8?`axC+?cUx!ErM{)rh=RQ7tPOY;i(Cg@RBJ@+G))KvTS zSHt!`JA?Vf5;w!UE2roOK+RP&)l~jTUQaj^w?RXR&o%fq(qg3B32gjktV6fiFLH8oPE5;b-17iN1ocW6KyG zeR>Yxd2R-~cNX}?cW%O)Z<@lUDc}R2JchHg2B99jZ4#Gm7{smDPvC*ur?6#Jh7W%7 z7*3uqaoaVc__=#mV`z}DW9uj$dwLeX{fnEhW`wbJbw6G?+QZv!pTHg03}GOzuxix^ zzWvNB_PuxkH|;F&%MY%?Pv5r*myDJ8@?+Du<*E_9^R999WfgX8o517GoX2nf;%2O$ zAgo{AkJ&}w`!CGl?(0W!*Yy*)|Blrd9w0n@VBRS{0{dVmx)zP>B=@PWQzTz|AI&H+ z!Y8%u@qh;dXH`}CPL?GHnW1+v45SYB8`$gCnfb*IarIG^m3gV7WekRax~`FDj3Os2 z_YCycH8(4BtcRO1tE$FepZPhtg=O>m!iEgvPkwVN=9d`%`i~BwuOKY;$h}qEtoC82 zlPojo!?I!b?UrR;&a$j9&;crD)U|nkfuq+L(!AiRc1F#cpPQ|!3ZsLB`6UKrxzmT7 zi4J}TSOjH=)GKd=?B(XA1LJ!`NuuG+7Y{4;En8_8Gz^)Zd7hzL0zlqemdGm#LQxd4 zbWNeeAoQ9ahcu-Jh3~*`w!FyEtpV5{l5FHOoRGghuJGr>Oz7g6vm8S*8XO!ulVL)xe%o zgc33-yr&&}Z@pK(vinK;M0#$+er+zEl#JnM=3$Q^J45a3#F3CbfKG|sRx4V{750PauZB~d%iLKNQV31S zZ10tLGm>l7=IC(eC{Qv*EeUZ?se3>;hB)prBI%A7^9HZ~@C6aV`)s-jytm;ZK(Rfl$bTk*%4OA*mPV+#HkkM-^c)wo{r2386&U zQ$AfC>Oq*c>7du9SU%F6q!kdpB8CyHyy3vxj7F>+fj0(@qSX~|QB^C14R3l%qlaoq zqDg?C3U%5tOw-MQ08w}kUA0$s3^)o0(!8f|T8D{gW5!^QSR78q$(2L_SXwn8b!DHn zNl*h25YHPY(s2SYW8zJ_|KF*}vlcK-(Ye%?!ky5?g5x6LyV0zjSnI;BZfZ+k?Hsf_ zZCo!{W+XpuU-G8OChCNg)Ori1f&;NGlB6s{wqGQ85+>U|DQYK`Sz%?f+@8pbYLSeh_<+!Q(u}V4WVfX1BcWEQ|U)4 zkW%m->eGCWSAE5ghFQ-Sig1O8V#pGWhi!L))aLVlidhz8kP;(BF?g>=8`J6w``wjD z!|O)YF~Q}%qbHvNjL)U`HsUo-l4xR2#ULhsiO9O+@_6H&q)UGCDH!q0oyd@6f#*jgJsmG|2*Eu~OO`UZ*S92^vs znv`KzWwQ`1r49L0znRP%j&Z?XWOvJqg-JKC-?SN|Z+aPH7f%OZ+hWy~5{*0`zHcbj zqHwd2!FXJKTV2D)U**Q?tK3h7kVFFVU6M0#LMLhS zCOW8n7dp=9GBgA~B3LEJNuz5SKxlq9P{4n%Md&4{3aAXAMp#xqv=Q9Ogf@dGq5?56 zoBsK@lw$qycopMHtv%k_Qxwg^s}=#=S!Zy3Q#vupt}#W)r>KJ@4~m%7s|!Sn;KDXe z=K{giiXcTf)V!b?_lVRhqeC(#I$2qwyN?_aO8+2*^uq=mn+ss?IBo=_pL_5EQeY{G z=NzXvu`h49SlQal8IqC3ZD{oHSR+$Fhyy<4Z#_8nSp|~dGq|r~CW4e(wT0FjJ+Ipe zc_M`&JZ}{!Z4pe!P&j-lvP9EBvM?R!mMy$%P;8EIN@qauRBk|n^eCG6$`}!!m&#BB zaEnY0B1nCvUl4IIho zPT`FV?Hz&P!)KS|Wsd`QA!I0#X3V;dbU`f-hsPQcZHQx=xGn;Mts`jpHR&@AbaDKA|iMY+p6N0^)F_fM

WzepBcQ#tsjJ*6_teB3bK*5wOHc5yA2^HKwlvNOmztnsaiE8;AcQ zqVW}mWio;H3EmxT%R6}#(CY^D$gZY{piLM#xC7btBc9rmgwx3G33SckAJ$`~F#TOK zz`=O!-W`6|zkIEo!x++k*O^1j?*&qk>Lc??I=&_L2uE2-CdaeAO`Jft79ervWdwz- z=FWqKBIIUvv{TCzE;OV076T-%0u(Vvux%6hiPgSjKt_jYZC(=ZTJuX?P;7SZ^jGx+D{!X&{#%g`uo&c8x{$Wx6B&Jir^ zi=p+X?bb*r(j5`eLH3>N(7>b9$lGIg|dXg|J&9dnG3I4ntPhEwzGa<|caFbexm z-L*kuKNN-MJm)&vty}oxvB_I>)555I?+ literal 0 HcmV?d00001 diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/AutoSaveService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/AutoSaveService.cs new file mode 100644 index 0000000..5bb2f78 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/AutoSaveService.cs @@ -0,0 +1,34 @@ +using System.ComponentModel; +using AStar.Dev.OneDrive.Client.ViewModels; +using AStar.Dev.Source.Generators.Attributes; + +namespace AStar.Dev.OneDrive.Client.Common; + +///

+/// Provides automatic persistence of user preferences when specific view model properties change. +/// Monitors property change notifications and triggers save actions accordingly. +/// +[AutoRegisterService(ServiceLifetime.Singleton)] +public class AutoSaveService : IAutoSaveService +{ + private PropertyChangedEventHandler? _handler; + + /// + public void MonitorForChanges(MainWindowViewModel mainWindowViewModel, Action saveAction) + { + _handler = (_, e) => + { + if(e.PropertyName == nameof(MainWindowViewModel.SyncStatusMessage)) + saveAction(); + }; + + mainWindowViewModel.PropertyChanged += _handler; + } + + /// + public void StopMonitoring(MainWindowViewModel mainWindowViewModel) + { + if(_handler is not null) + mainWindowViewModel.PropertyChanged -= _handler; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/FileWriter.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/FileWriter.cs new file mode 100644 index 0000000..04c2137 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/FileWriter.cs @@ -0,0 +1,13 @@ +namespace AStar.Dev.OneDrive.Client.Common; + +/// +/// Provides file writing operations with cancellation support. +/// +public class FileWriter +{ + public virtual async Task WriteFileAsync(Stream content, string localPath, CancellationToken token) + { + await using FileStream fileStream = File.Create(localPath); + await content.CopyToAsync(fileStream, token); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/IAutoSaveService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/IAutoSaveService.cs new file mode 100644 index 0000000..24c90c0 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/IAutoSaveService.cs @@ -0,0 +1,22 @@ +using AStar.Dev.OneDrive.Client.ViewModels; + +namespace AStar.Dev.OneDrive.Client.Common; + +/// +/// Defines the contract for automatically saving user preferences when specific properties change. +/// +public interface IAutoSaveService +{ + /// + /// Begins monitoring the view model for property changes and invokes the save action when appropriate. + /// + /// The view model to monitor for property changes. + /// The action to invoke when a monitored property changes. + void MonitorForChanges(MainWindowViewModel mainWindowViewModel, Action saveAction); + + /// + /// Stops monitoring the view model for property changes and unsubscribes from events. + /// + /// The view model to stop monitoring. + void StopMonitoring(MainWindowViewModel mainWindowViewModel); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/OneDriveClientConstants.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/OneDriveClientConstants.cs new file mode 100644 index 0000000..761f320 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Common/OneDriveClientConstants.cs @@ -0,0 +1,34 @@ +namespace AStar.Dev.OneDrive.Client.Common; + +/// +/// Defines constant values used throughout the OneDrive client application. +/// +public static class OneDriveClientConstants +{ + /// + /// Progress reporting constants for controlling UI update frequency. + /// + public static class ProgressReporting + { + /// + /// Default number of completed files between progress reports. + /// + public const int DefaultFileInterval = 5; + + /// + /// Default time interval in milliseconds between progress reports. + /// + public const int DefaultMillisecondInterval = 500; + } + + /// + /// Pagination and batch processing constants. + /// + public static class BatchProcessing + { + /// + /// Default page size for downloading files from the database. + /// + public const int DefaultPageSize = 100; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Converters/BooleanNegationConverter.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Converters/BooleanNegationConverter.cs new file mode 100644 index 0000000..242362f --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Converters/BooleanNegationConverter.cs @@ -0,0 +1,16 @@ +using System.Globalization; +using Avalonia.Data.Converters; + +namespace AStar.Dev.OneDrive.Client.Converters; + +/// +/// Converts a boolean value to its negation. +/// +public sealed class BooleanNegationConverter : IValueConverter +{ + public static readonly BooleanNegationConverter Instance = new(); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => value is bool b ? !b : value; + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => value is bool b ? !b : value; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Converters/EnumToBooleanConverter.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Converters/EnumToBooleanConverter.cs new file mode 100644 index 0000000..74bd6d5 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Converters/EnumToBooleanConverter.cs @@ -0,0 +1,31 @@ +using System.Globalization; +using Avalonia.Data.Converters; + +namespace AStar.Dev.OneDrive.Client.Converters; + +/// +/// Converts an enum value to a boolean for radio button bindings. +/// Returns true when the enum value matches the converter parameter. +/// +public sealed class EnumToBooleanConverter : IValueConverter +{ + /// + /// Converts an enum value to a boolean. + /// + /// The enum value to convert. + /// The target type (ignored). + /// The enum value to compare against. + /// The culture to use (ignored). + /// true if the value equals the parameter; otherwise, false. + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => value is not null && parameter is not null && value.Equals(parameter); + + /// + /// Converts a boolean back to an enum value. + /// + /// The boolean value. + /// The target enum type. + /// The enum value to return if true. + /// The culture to use (ignored). + /// The parameter value if is true; otherwise, null. + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => value is bool and true && parameter is not null ? parameter : null; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/ApplicationName.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/ApplicationName.cs new file mode 100644 index 0000000..8c16de0 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/ApplicationName.cs @@ -0,0 +1,6 @@ +namespace AStar.Dev.OneDrive.Client.Data; + +public readonly partial record struct ApplicationName(string Name) +{ + public static implicit operator string(ApplicationName appName) => appName.Name; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/DriveId.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/DriveId.cs new file mode 100644 index 0000000..c2d132a --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/DriveId.cs @@ -0,0 +1,3 @@ +namespace AStar.Dev.OneDrive.Client.Data; + +public readonly partial record struct DriveId(Guid Id); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/ItemId.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/ItemId.cs new file mode 100644 index 0000000..85f6ce0 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/ItemId.cs @@ -0,0 +1,3 @@ +namespace AStar.Dev.OneDrive.Client.Data; + +public readonly partial record struct ItemId(Guid Id); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/LocalDriveId.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/LocalDriveId.cs new file mode 100644 index 0000000..310f485 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Data/LocalDriveId.cs @@ -0,0 +1,6 @@ +namespace AStar.Dev.OneDrive.Client.Data; + +public readonly partial record struct LocalDriveId(Guid Id) +{ + public static LocalDriveId Empty => new(Guid.Empty); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FodyWeavers.xml b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FodyWeavers.xml new file mode 100644 index 0000000..63fc148 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthConfiguration.cs new file mode 100644 index 0000000..2b6587a --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthConfiguration.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.Configuration; + +namespace AStar.Dev.OneDrive.Client.FromV3.Authentication; + +/// +/// Configuration settings for MSAL authentication. +/// +public sealed class AuthConfiguration +{ + /// + /// Gets or sets the Azure AD client ID for the application. + /// + public required string ClientId { get; init; } + + /// + /// Gets or sets the redirect URI for OAuth callbacks. + /// + public required string RedirectUri { get; init; } + + /// + /// Gets or sets the Microsoft Graph API scopes required for OneDrive access. + /// + public required string[] Scopes { get; init; } + + /// + /// Gets or sets the authority URL for Microsoft identity platform. + /// + public required string Authority { get; init; } + + /// + /// Loads authentication configuration from IConfiguration. + /// + /// The configuration source. + /// Configured AuthConfiguration instance. + public static AuthConfiguration LoadFromConfiguration(IConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(configuration); + + IConfigurationSection authSection = configuration.GetSection("Authentication"); + if(!authSection.Exists()) throw new InvalidOperationException("Authentication configuration section not found. Ensure appsettings.json contains an 'Authentication' section."); + + var clientId = authSection["ClientId"]; + return string.IsNullOrWhiteSpace(clientId) + ? throw new InvalidOperationException("Authentication:ClientId is not configured. Please set it in appsettings.json or user secrets.") + : new AuthConfiguration + { + ClientId = clientId, + RedirectUri = authSection["RedirectUri"] ?? "http://localhost", + Authority = authSection["Authority"] ?? "https://login.microsoftonline.com/common", + Scopes = authSection.GetSection("Scopes").Get() ?? ["Files.ReadWrite", "User.Read", "offline_access"] + }; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthService.cs new file mode 100644 index 0000000..cfe550c --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthService.cs @@ -0,0 +1,190 @@ +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensions.Msal; + +namespace AStar.Dev.OneDrive.Client.FromV3.Authentication; + +/// +/// Service for managing Microsoft authentication via MSAL. +/// +public sealed class AuthService : IAuthService +{ + private readonly IAuthenticationClient _authClient; + private readonly AuthConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The authentication client wrapper. + /// Authentication configuration. + public AuthService(IAuthenticationClient authClient, AuthConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(authClient); + ArgumentNullException.ThrowIfNull(configuration); + _authClient = authClient; + _configuration = configuration; + } + + /// + /// Creates a new AuthService with default MSAL configuration. + /// + /// Authentication configuration. + /// Optional cancellation token. + /// Configured AuthService instance. + public static async Task CreateAsync(AuthConfiguration configuration, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(configuration); + + IPublicClientApplication app = PublicClientApplicationBuilder + .Create(configuration.ClientId) + .WithAuthority(configuration.Authority) + .WithRedirectUri(configuration.RedirectUri) + .Build(); + + // Setup token cache persistence + var cacheDirectory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "AStar.Dev.OneDrive.Client"); + + var storagePropertiesBuilder = new StorageCreationPropertiesBuilder( + "astar_onedrive_cache.dat", + cacheDirectory); + + // Use plaintext storage on Linux due to keyring/libsecret compatibility issues + // Windows and macOS use platform-specific secure storage (DPAPI and Keychain) + if(OperatingSystem.IsLinux()) _ = storagePropertiesBuilder.WithUnprotectedFile(); + + StorageCreationProperties storageProperties = storagePropertiesBuilder.Build(); + MsalCacheHelper cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties); + cacheHelper.RegisterCache(app.UserTokenCache); + + return new AuthService(new AuthenticationClient(app), configuration); + } + + /// + public async Task LoginAsync(CancellationToken cancellationToken = default) + { + try + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(TimeSpan.FromSeconds(30)); + + MsalAuthResult result = await _authClient + .AcquireTokenInteractiveAsync(_configuration.Scopes, cts.Token); + + return new AuthenticationResult( + Success: true, + AccountId: result.Account.HomeAccountId.Identifier, + DisplayName: result.Account.Username, + ErrorMessage: null + ); + } + catch(MsalException ex) + { + return new AuthenticationResult( + Success: false, + AccountId: null, + DisplayName: null, + ErrorMessage: ex.Message + ); + } + catch(OperationCanceledException) + { + var message = cancellationToken.IsCancellationRequested + ? "Login was cancelled." + : "Login timed out after 30 seconds."; + + return new AuthenticationResult( + Success: false, + AccountId: null, + DisplayName: null, + ErrorMessage: message + ); + } + } + + /// + public async Task LogoutAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + try + { + IEnumerable accounts = await _authClient.GetAccountsAsync(cancellationToken); + IAccount? account = accounts.FirstOrDefault(a => a.HomeAccountId.Identifier == accountId); + + if(account is not null) + { + await _authClient.RemoveAsync(account, cancellationToken); + return true; + } + + return false; + } + catch(MsalException) + { + return false; + } + } + + /// + public async Task> GetAuthenticatedAccountsAsync(CancellationToken cancellationToken = default) + { + try + { + IEnumerable accounts = await _authClient.GetAccountsAsync(cancellationToken); + return accounts + .Select(a => (a.HomeAccountId.Identifier, a.Username)) + .ToList(); + } + catch(MsalException) + { + return []; + } + } + + /// + public async Task GetAccessTokenAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + try + { + IEnumerable accounts = await _authClient.GetAccountsAsync(cancellationToken); + IAccount? account = accounts.FirstOrDefault(a => a.HomeAccountId.Identifier == accountId); + + if(account is null) return null; + + MsalAuthResult result = await _authClient + .AcquireTokenSilentAsync(_configuration.Scopes, account, cancellationToken); + + return result.AccessToken; + } + catch(MsalUiRequiredException) + { + return null; + } + catch(MsalException) + { + return null; + } + } + + /// + public async Task IsAuthenticatedAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + try + { + IEnumerable accounts = await _authClient.GetAccountsAsync(cancellationToken); + return accounts.Any(a => a.HomeAccountId.Identifier == accountId); + } + catch(MsalException) + { + return false; + } + } + + /// + public async Task AcquireTokenSilentAsync(string accountId, CancellationToken cancellationToken = default) => await GetAccessTokenAsync(accountId, cancellationToken); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthenticationClient.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthenticationClient.cs new file mode 100644 index 0000000..da0a7b5 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthenticationClient.cs @@ -0,0 +1,52 @@ +using Microsoft.Identity.Client; + +namespace AStar.Dev.OneDrive.Client.FromV3.Authentication; + +/// +/// Wrapper implementation for that delegates to MSAL. +/// +/// +/// This wrapper exists because MSAL's builder classes are sealed and cannot be mocked in tests. +/// By wrapping the interaction with MSAL behind this interface, we can inject mocks for testing. +/// +public sealed class AuthenticationClient : IAuthenticationClient +{ + private readonly IPublicClientApplication _publicClientApp; + + /// + /// Initializes a new instance of the class. + /// + /// The MSAL public client application instance. + /// Thrown when publicClientApp is null. + public AuthenticationClient(IPublicClientApplication publicClientApp) + { + ArgumentNullException.ThrowIfNull(publicClientApp); + _publicClientApp = publicClientApp; + } + + /// + public async Task AcquireTokenInteractiveAsync(IEnumerable scopes, CancellationToken cancellationToken = default) + { + Microsoft.Identity.Client.AuthenticationResult result = await _publicClientApp + .AcquireTokenInteractive(scopes) + .ExecuteAsync(cancellationToken); + + return MsalAuthResult.FromMsal(result); + } + + /// + public async Task AcquireTokenSilentAsync(IEnumerable scopes, IAccount account, CancellationToken cancellationToken = default) + { + Microsoft.Identity.Client.AuthenticationResult result = await _publicClientApp + .AcquireTokenSilent(scopes, account) + .ExecuteAsync(cancellationToken); + + return MsalAuthResult.FromMsal(result); + } + + /// + public async Task> GetAccountsAsync(CancellationToken cancellationToken = default) => await _publicClientApp.GetAccountsAsync(); + + /// + public async Task RemoveAsync(IAccount account, CancellationToken cancellationToken = default) => await _publicClientApp.RemoveAsync(account); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthenticationResult.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthenticationResult.cs new file mode 100644 index 0000000..b93990c --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/AuthenticationResult.cs @@ -0,0 +1,15 @@ +namespace AStar.Dev.OneDrive.Client.FromV3.Authentication; + +/// +/// Result of an authentication operation. +/// +/// Indicates whether the operation was successful. +/// The authenticated account identifier. +/// The display name of the authenticated user. +/// Error message if the operation failed. +public sealed record AuthenticationResult( + bool Success, + string? AccountId, + string? DisplayName, + string? ErrorMessage +); diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/IAuthService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/IAuthService.cs new file mode 100644 index 0000000..34c2eb7 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/IAuthService.cs @@ -0,0 +1,53 @@ +namespace AStar.Dev.OneDrive.Client.FromV3.Authentication; + +/// +/// Service for managing Microsoft authentication via MSAL. +/// +public interface IAuthService +{ + /// + /// Initiates interactive login flow for a new account. + /// + /// Cancellation token. + /// Authentication result containing account information. + Task LoginAsync(CancellationToken cancellationToken = default); + + /// + /// Logs out an account by removing it from the token cache. + /// + /// The account identifier to log out. + /// Cancellation token. + /// True if logout was successful, false otherwise. + Task LogoutAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Gets all authenticated accounts from the token cache. + /// + /// Cancellation token. + /// List of account identifiers and display names. + Task> GetAuthenticatedAccountsAsync(CancellationToken cancellationToken = default); + + /// + /// Acquires an access token for Microsoft Graph API calls. + /// + /// The account identifier to get a token for. + /// Cancellation token. + /// Access token if successful, null otherwise. + Task GetAccessTokenAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Checks if an account is currently authenticated. + /// + /// The account identifier to check. + /// Cancellation token. + /// True if the account is authenticated, false otherwise. + Task IsAuthenticatedAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Acquires a token silently (without user interaction) if possible. + /// + /// The account identifier to get a token for. + /// Cancellation token. + /// Access token if successful, null if user interaction is required. + Task AcquireTokenSilentAsync(string accountId, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/IAuthenticationClient.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/IAuthenticationClient.cs new file mode 100644 index 0000000..62780a7 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/IAuthenticationClient.cs @@ -0,0 +1,45 @@ +using Microsoft.Identity.Client; + +namespace AStar.Dev.OneDrive.Client.FromV3.Authentication; + +/// +/// Wrapper interface for to enable testing. +/// +/// +/// MSAL's builder classes (AcquireTokenInteractiveParameterBuilder, etc.) are sealed, +/// making them impossible to mock in tests. This interface provides a testable abstraction +/// over the authentication flows. +/// +public interface IAuthenticationClient +{ + /// + /// Acquires a token interactively (with UI) for the specified scopes. + /// + /// The scopes to request. + /// Optional cancellation token. + /// The authentication result containing the access token and account information. + Task AcquireTokenInteractiveAsync(IEnumerable scopes, CancellationToken cancellationToken = default); + + /// + /// Acquires a token silently (without UI) for the specified account and scopes. + /// + /// The scopes to request. + /// The account to acquire the token for. + /// Optional cancellation token. + /// The authentication result containing the access token. + Task AcquireTokenSilentAsync(IEnumerable scopes, IAccount account, CancellationToken cancellationToken = default); + + /// + /// Gets all accounts currently in the token cache. + /// + /// Optional cancellation token. + /// Collection of cached accounts. + Task> GetAccountsAsync(CancellationToken cancellationToken = default); + + /// + /// Removes an account from the token cache. + /// + /// The account to remove. + /// Optional cancellation token. + Task RemoveAsync(IAccount account, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/MsalAuthResult.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/MsalAuthResult.cs new file mode 100644 index 0000000..5f6a3c9 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Authentication/MsalAuthResult.cs @@ -0,0 +1,44 @@ +using Microsoft.Identity.Client; + +namespace AStar.Dev.OneDrive.Client.FromV3.Authentication; + +/// +/// Simplified authentication result wrapper to avoid dependencies on MSAL's sealed types in tests. +/// +public sealed class MsalAuthResult +{ + /// + /// Gets the authenticated account. + /// + public IAccount Account { get; init; } + + /// + /// Gets the access token. + /// + public string AccessToken { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The authenticated account. + /// The access token. + public MsalAuthResult(IAccount account, string accessToken) + { + ArgumentNullException.ThrowIfNull(account); + ArgumentNullException.ThrowIfNull(accessToken); + + Account = account; + AccessToken = accessToken; + } + + /// + /// Creates a wrapper from MSAL's AuthenticationResult. + /// + /// The MSAL authentication result. + /// Wrapped result. + public static MsalAuthResult FromMsal(Microsoft.Identity.Client.AuthenticationResult msalResult) + { + ArgumentNullException.ThrowIfNull(msalResult); + return new MsalAuthResult(msalResult.Account, msalResult.AccessToken); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/AutoSyncCoordinator.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/AutoSyncCoordinator.cs new file mode 100644 index 0000000..2a40dba --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/AutoSyncCoordinator.cs @@ -0,0 +1,137 @@ +using System.Reactive.Disposables; +using System.Reactive.Linq; +using AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; +using Microsoft.Extensions.Logging; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +#pragma warning disable CA1848 // Use LoggerMessage delegates +#pragma warning disable CA1873 // Avoid string interpolation in logging + +public sealed class AutoSyncCoordinator : IAutoSyncCoordinator +{ + private readonly IFileWatcherService _fileWatcherService; + private readonly ISyncEngine _syncEngine; + private readonly IAccountRepository _accountRepository; + private readonly ILogger _logger; + private readonly CompositeDisposable _disposables = []; + private readonly Dictionary _accountSubscriptions = []; + + /// + /// Initializes a new instance of . + /// + /// Service for monitoring file system changes. + /// Sync engine for performing synchronization. + /// Repository for account data. + /// Logger for diagnostic messages. + public AutoSyncCoordinator( + IFileWatcherService fileWatcherService, + ISyncEngine syncEngine, + IAccountRepository accountRepository, + ILogger logger) + { + ArgumentNullException.ThrowIfNull(fileWatcherService); + ArgumentNullException.ThrowIfNull(syncEngine); + ArgumentNullException.ThrowIfNull(accountRepository); + ArgumentNullException.ThrowIfNull(logger); + + _fileWatcherService = fileWatcherService; + _syncEngine = syncEngine; + _accountRepository = accountRepository; + _logger = logger; + } + + /// + /// Starts monitoring an account's sync directory for changes. + /// + /// Account identifier. + /// Local sync directory path. + /// Cancellation token. + /// + /// File changes are debounced with a 2-second delay to avoid excessive sync triggers + /// when multiple files are changed rapidly. + /// + public async Task StartMonitoringAsync(string accountId, string localPath, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + ArgumentNullException.ThrowIfNull(localPath); + + // Stop any existing monitoring for this account + StopMonitoring(accountId); + + try + { + // Start file watcher + _fileWatcherService.StartWatching(accountId, localPath); + + // Subscribe to file changes with debouncing (2 seconds) + // This groups rapid file changes into a single sync operation + IDisposable subscription = _fileWatcherService.FileChanges + .Where(e => e.AccountId == accountId) + .Buffer(TimeSpan.FromSeconds(2)) + .Where(changes => changes.Count > 0) + .Subscribe(async changes => + { + _logger.LogInformation("Detected {Count} file change(s) for account {AccountId}, triggering sync", + changes.Count, accountId); + + try + { + await _syncEngine.StartSyncAsync(accountId, CancellationToken.None); + } + catch (Exception ex) + { + _logger.LogError(ex, "Auto-sync failed for account {AccountId}", accountId); + } + }); + + _accountSubscriptions[accountId] = subscription; + + _logger.LogInformation("Started auto-sync monitoring for account {AccountId} at {Path}", + accountId, localPath); + } + catch(Exception ex) + { + _logger.LogError(ex, "Failed to start auto-sync monitoring for account {AccountId}", accountId); + throw; + } + + await Task.CompletedTask; + } + + /// + /// Stops monitoring an account's sync directory. + /// + /// Account identifier. + public void StopMonitoring(string accountId) + { + ArgumentNullException.ThrowIfNull(accountId); + + if(_accountSubscriptions.Remove(accountId, out IDisposable? subscription)) + { + subscription.Dispose(); + _fileWatcherService.StopWatching(accountId); + + _logger.LogInformation("Stopped auto-sync monitoring for account {AccountId}", accountId); + } + } + + /// + /// Stops monitoring all accounts. + /// + public void StopAll() + { + foreach(var accountId in _accountSubscriptions.Keys.ToList()) StopMonitoring(accountId); + } + + /// + /// Disposes resources and stops all monitoring. + /// + public void Dispose() + { + StopAll(); + _disposables.Dispose(); + _logger.LogInformation("AutoSyncCoordinator disposed"); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/AutoSyncSchedulerService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/AutoSyncSchedulerService.cs new file mode 100644 index 0000000..d1bb65f --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/AutoSyncSchedulerService.cs @@ -0,0 +1,138 @@ +using System.Collections.Concurrent; +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; +using Microsoft.Extensions.Logging; + +#pragma warning disable CA1848 // Use LoggerMessage delegates for high-performance logging +#pragma warning disable CA1873 // Argument may be expensive - acceptable for scheduler logging + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for scheduling automatic remote sync checks for accounts. +/// +public sealed class AutoSyncSchedulerService : IAutoSyncSchedulerService +{ + private readonly IAccountRepository _accountRepository; + private readonly ISyncEngine _syncEngine; + private readonly ILogger _logger; + private readonly ConcurrentDictionary _timers = new(); + private bool _isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// Repository for account data. + /// Sync engine for performing synchronization. + /// Logger instance. + public AutoSyncSchedulerService( + IAccountRepository accountRepository, + ISyncEngine syncEngine, + ILogger logger) + { + ArgumentNullException.ThrowIfNull(accountRepository); + ArgumentNullException.ThrowIfNull(syncEngine); + ArgumentNullException.ThrowIfNull(logger); + + _accountRepository = accountRepository; + _syncEngine = syncEngine; + _logger = logger; + } + + /// + public async Task StartAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("Starting auto-sync scheduler"); + + IReadOnlyList accounts = await _accountRepository.GetAllAsync(cancellationToken); + foreach(AccountInfo account in accounts) + if(account is { AutoSyncIntervalMinutes: not null, IsAuthenticated: true }) UpdateSchedule(account.AccountId, account.AutoSyncIntervalMinutes.Value); + + _logger.LogInformation("Auto-sync scheduler started with {Count} scheduled accounts", _timers.Count); + } + + /// + public Task StopAsync() + { + _logger.LogInformation("Stopping auto-sync scheduler"); + + foreach((var accountId, System.Timers.Timer? timer) in _timers) + { + timer.Stop(); + timer.Dispose(); + _logger.LogDebug("Stopped timer for account {AccountId}", accountId); + } + + _timers.Clear(); + _logger.LogInformation("Auto-sync scheduler stopped"); + + return Task.CompletedTask; + } + + /// + public void UpdateSchedule(string accountId, int? intervalMinutes) + { + ArgumentNullException.ThrowIfNull(accountId); + + // Remove existing timer if present + if(_timers.TryRemove(accountId, out System.Timers.Timer? existingTimer)) + { + existingTimer.Stop(); + existingTimer.Dispose(); + _logger.LogDebug("Removed existing timer for account {AccountId}", accountId); + } + + // Create new timer if interval is specified + if(intervalMinutes.HasValue) + { + var clampedInterval = Math.Clamp(intervalMinutes.Value, 60, 1440); // 1 hour to 24 hours + var intervalMs = clampedInterval * 60 * 1000; // Convert to milliseconds + + var timer = new System.Timers.Timer(intervalMs) + { + AutoReset = true + }; + + timer.Elapsed += async (sender, e) => + { + try + { + _logger.LogInformation("Auto-sync triggered for account {AccountId}", accountId); + await _syncEngine.StartSyncAsync(accountId, CancellationToken.None); + } + catch(Exception ex) + { + _logger.LogError(ex, "Auto-sync failed for account {AccountId}", accountId); + } + }; + + timer.Start(); + _timers[accountId] = timer; + + _logger.LogInformation("Scheduled auto-sync for account {AccountId} every {Interval} minutes", + accountId, clampedInterval); + } + } + + /// + public void RemoveSchedule(string accountId) + { + ArgumentNullException.ThrowIfNull(accountId); + + if(_timers.TryRemove(accountId, out System.Timers.Timer? timer)) + { + timer.Stop(); + timer.Dispose(); + _logger.LogInformation("Removed auto-sync schedule for account {AccountId}", accountId); + } + } + + /// + public void Dispose() + { + if(_isDisposed) return; + + StopAsync().GetAwaiter().GetResult(); + _isDisposed = true; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Configuration/AccountEntityConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Configuration/AccountEntityConfiguration.cs new file mode 100644 index 0000000..a8889dd --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/Configuration/AccountEntityConfiguration.cs @@ -0,0 +1,17 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace AStar.Dev.OneDrive.Client.FromV3.Configuration; + +public class AccountEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + _ = builder.HasKey(e => e.AccountId); + _ = builder.Property(e => e.AccountId).IsRequired(); + _ = builder.Property(e => e.DisplayName).IsRequired(); + _ = builder.Property(e => e.LocalSyncPath).IsRequired(); + _ = builder.HasIndex(e => e.LocalSyncPath).IsUnique(); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DatabaseConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DatabaseConfiguration.cs new file mode 100644 index 0000000..5750e76 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DatabaseConfiguration.cs @@ -0,0 +1,28 @@ +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Configuration for database connection settings. +/// +public sealed class DatabaseConfiguration +{ + /// + /// Gets the database file path. + /// + public static string DatabasePath + { + get + { + var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var appFolder = Path.Combine(appDataPath, "AStar.Dev.OneDrive.Client"); + + _ = Directory.CreateDirectory(appFolder); + + return Path.Combine(appFolder, "sync.db"); + } + } + + /// + /// Gets the SQLite connection string. + /// + public static string ConnectionString => $"Data Source={DatabasePath}"; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DebugLog.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DebugLog.cs new file mode 100644 index 0000000..24ac34f --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DebugLog.cs @@ -0,0 +1,47 @@ +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Static facade for convenient debug logging access throughout the application. +/// Provides static methods that delegate to the singleton IDebugLogger instance. +/// +public static class DebugLog +{ + private static IDebugLogger? _instance; + + /// + /// Initializes the static logger instance. Should be called once during application startup. + /// + /// The IDebugLogger instance to use. + public static void Initialize(IDebugLogger logger) => _instance = logger ?? throw new ArgumentNullException(nameof(logger)); + + /// + /// Logs an informational message. + /// + /// The source of the log (typically class or method name). + /// The log message. + /// Cancellation token. + public static Task InfoAsync(string source, string message, CancellationToken cancellationToken = default) => _instance?.LogInfoAsync(source, message, cancellationToken) ?? Task.CompletedTask; + + /// + /// Logs an error message. + /// + /// The source of the log (typically class or method name). + /// The log message. + /// Optional exception details. + /// Cancellation token. + public static Task ErrorAsync(string source, string message, Exception? exception = null, CancellationToken cancellationToken = default) => _instance?.LogErrorAsync(source, message, exception, cancellationToken) ?? Task.CompletedTask; + + /// + /// Logs a method entry. + /// + /// The source of the log (typically class.Method). + /// Cancellation token. + public static Task EntryAsync(string source, CancellationToken cancellationToken = default) => _instance?.LogEntryAsync(source, cancellationToken) ?? Task.CompletedTask; + + /// + /// Logs a method exit. + /// + /// The source of the log (typically class.Method). + /// Cancellation token. + public static Task ExitAsync(string source, CancellationToken cancellationToken = default) => _instance?.LogExitAsync(source, cancellationToken) ?? Task.CompletedTask; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DebugLogContext.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DebugLogContext.cs new file mode 100644 index 0000000..2701907 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DebugLogContext.cs @@ -0,0 +1,27 @@ +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Provides ambient context for the current account being processed. +/// Uses AsyncLocal to flow context through async operations without explicit parameter passing. +/// +public static class DebugLogContext +{ + private static readonly AsyncLocal _currentAccountId = new(); + + /// + /// Gets the current account ID from the ambient context. + /// + public static string? CurrentAccountId => _currentAccountId.Value; + + /// + /// Sets the current account ID for the ambient context. + /// This flows through all async operations in the current execution context. + /// + /// The account ID to set. + public static void SetAccountId(string? accountId) => _currentAccountId.Value = accountId; + + /// + /// Clears the current account ID from the ambient context. + /// + public static void Clear() => _currentAccountId.Value = null; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DebugLogger.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DebugLogger.cs new file mode 100644 index 0000000..2b1fc96 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/DebugLogger.cs @@ -0,0 +1,57 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Infrastructure.Data; +using AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Implementation of debug logging that writes to the database when enabled for an account. +/// +public sealed class DebugLogger : IDebugLogger +{ + private readonly AppDbContext _context; + private readonly IAccountRepository _accountRepository; + + public DebugLogger(AppDbContext context, IAccountRepository accountRepository) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(accountRepository); + _context = context; + _accountRepository = accountRepository; + } + + /// + public async Task LogInfoAsync(string source, string message, CancellationToken cancellationToken = default) => await LogAsync("Info", source, message, null, cancellationToken); + + /// + public async Task LogErrorAsync(string source, string message, Exception? exception = null, CancellationToken cancellationToken = default) => await LogAsync("Error", source, message, exception, cancellationToken); + + /// + public async Task LogEntryAsync(string source, CancellationToken cancellationToken = default) => await LogAsync("Entry", source, "Method entry", null, cancellationToken); + + /// + public async Task LogExitAsync(string source, CancellationToken cancellationToken = default) => await LogAsync("Exit", source, "Method exit", null, cancellationToken); + + private async Task LogAsync(string logLevel, string source, string message, Exception? exception, CancellationToken cancellationToken) + { + var accountId = DebugLogContext.CurrentAccountId; + if(string.IsNullOrEmpty(accountId)) return; // No account context, skip logging + + // Check if debug logging is enabled for this account + AccountInfo? account = await _accountRepository.GetByIdAsync(accountId, cancellationToken); + if(account is null || !account.EnableDebugLogging) return; // Debug logging not enabled for this account + + var logEntry = new DebugLogEntity + { + AccountId = accountId, + TimestampUtc = DateTime.UtcNow, + LogLevel = logLevel, + Source = source, + Message = message, + Exception = exception?.ToString() + }; + + _ = _context.DebugLogs.Add(logEntry); + _ = await _context.SaveChangesAsync(cancellationToken); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/FileWatcherService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/FileWatcherService.cs new file mode 100644 index 0000000..5644722 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/FileWatcherService.cs @@ -0,0 +1,186 @@ +using System.Reactive.Linq; +using System.Reactive.Subjects; +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Models.Enums; +using Microsoft.Extensions.Logging; + +#pragma warning disable CA1848 // Use LoggerMessage delegates + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for monitoring local file system changes to trigger synchronization. +/// +/// +/// Wraps FileSystemWatcher with debouncing (500ms) to handle rapid file changes +/// and partial writes. Supports monitoring multiple account directories independently. +/// +public sealed class FileWatcherService : IFileWatcherService +{ + private readonly ILogger _logger; + private readonly Dictionary _watchers = []; + private readonly Subject _fileChanges = new(); + private bool _disposed; + + /// + public IObservable FileChanges => _fileChanges.AsObservable(); + + /// + /// Initializes a new instance of . + /// + /// Logger for diagnostic messages. + /// Thrown if logger is null. + public FileWatcherService(ILogger logger) + { + ArgumentNullException.ThrowIfNull(logger); + _logger = logger; + } + + /// + public void StartWatching(string accountId, string localPath) + { + ArgumentNullException.ThrowIfNull(accountId); + ArgumentNullException.ThrowIfNull(localPath); + + if(!Directory.Exists(localPath)) throw new DirectoryNotFoundException($"Directory not found: {localPath}"); + + if(_watchers.ContainsKey(accountId)) + { + _logger.LogWarning("Already watching path for account {AccountId}. Stopping existing watcher first.", accountId); + StopWatching(accountId); + } + + try + { + var watcher = new FileSystemWatcher(localPath) + { + IncludeSubdirectories = true, + NotifyFilter = NotifyFilters.FileName + | NotifyFilters.DirectoryName + | NotifyFilters.Size + | NotifyFilters.LastWrite, + EnableRaisingEvents = true + }; + + // Debounce buffer for Changed/Created events (500ms throttle) + var changeBuffer = new Subject(); + IDisposable subscription = changeBuffer + .Throttle(TimeSpan.FromMilliseconds(500)) + .Subscribe(e => ProcessFileChange(accountId, localPath, e)); + + // Wire up FileSystemWatcher events + watcher.Changed += (s, e) => changeBuffer.OnNext(e); + watcher.Created += (s, e) => changeBuffer.OnNext(e); + watcher.Deleted += (s, e) => EmitFileChange(accountId, localPath, e, FileChangeType.Deleted); + watcher.Renamed += (s, e) => EmitFileChange(accountId, localPath, e, FileChangeType.Renamed); + watcher.Error += (s, e) => HandleWatcherError(accountId, e); + + _watchers[accountId] = new WatcherContext(watcher, changeBuffer, subscription); + _logger.LogInformation("Started watching {Path} for account {AccountId}", localPath, accountId); + } + catch(Exception ex) + { + _logger.LogError(ex, "Failed to start watching {Path} for account {AccountId}", localPath, accountId); + throw; + } + } + + /// + public void StopWatching(string accountId) + { + ArgumentNullException.ThrowIfNull(accountId); + + if(_watchers.Remove(accountId, out WatcherContext? context)) + { + context.Dispose(); + _logger.LogInformation("Stopped watching for account {AccountId}", accountId); + } + } + + private void ProcessFileChange(string accountId, string basePath, FileSystemEventArgs e) + { + // Determine if this is a creation or modification + FileChangeType changeType = e.ChangeType == WatcherChangeTypes.Created + ? FileChangeType.Created + : FileChangeType.Modified; + + EmitFileChange(accountId, basePath, e, changeType); + } + + private void EmitFileChange(string accountId, string basePath, FileSystemEventArgs e, FileChangeType changeType) + { + try + { + var relativePath = Path.GetRelativePath(basePath, e.FullPath); + + var changeEvent = new FileChangeEvent( + AccountId: accountId, + LocalPath: e.FullPath, + RelativePath: relativePath, + ChangeType: changeType, + DetectedUtc: DateTime.UtcNow + ); + + _fileChanges.OnNext(changeEvent); + _logger.LogDebug("File change detected: {ChangeType} - {RelativePath} (Account: {AccountId})", + changeType, relativePath, accountId); + } + catch(Exception ex) + { + _logger.LogError(ex, "Error emitting file change event for {Path}", e.FullPath); + } + } + + private void HandleWatcherError(string accountId, ErrorEventArgs e) + { + Exception exception = e.GetException(); + _logger.LogError(exception, "FileSystemWatcher error for account {AccountId}", accountId); + + // Optionally emit an error event or attempt to restart the watcher + // For now, just log the error + } + + /// + /// Disposes all file system watchers and cleans up resources. + /// + public void Dispose() + { + if(_disposed) return; + + foreach(WatcherContext context in _watchers.Values) context.Dispose(); + + _watchers.Clear(); + _fileChanges.Dispose(); + _disposed = true; + + _logger.LogInformation("FileWatcherService disposed"); + } + + /// + /// Context holding a FileSystemWatcher and its associated subscriptions. + /// + private sealed class WatcherContext : IDisposable + { + private readonly FileSystemWatcher _watcher; + private readonly Subject _changeBuffer; + private readonly IDisposable _subscription; + + public WatcherContext( + FileSystemWatcher watcher, + Subject changeBuffer, + IDisposable subscription) + { + _watcher = watcher; + _changeBuffer = changeBuffer; + _subscription = subscription; + } + + public void Dispose() + { + _watcher.EnableRaisingEvents = false; + _watcher.Dispose(); + _subscription.Dispose(); + _changeBuffer.Dispose(); + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IAutoSyncCoordinator.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IAutoSyncCoordinator.cs new file mode 100644 index 0000000..d6cd522 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IAutoSyncCoordinator.cs @@ -0,0 +1,26 @@ +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Coordinates automatic synchronization based on file system changes. +/// +/// +/// This service monitors local file system changes using +/// and automatically triggers synchronization operations when changes are detected. +/// +public interface IAutoSyncCoordinator : IDisposable +{ + /// + /// Starts monitoring a local directory for changes and triggers sync when changes are detected. + /// + /// The unique identifier of the account. + /// The local directory path to monitor. + /// Cancellation token. + /// A task representing the asynchronous operation. + Task StartMonitoringAsync(string accountId, string localPath, CancellationToken cancellationToken = default); + + /// + /// Stops monitoring file changes for the specified account. + /// + /// The unique identifier of the account. + void StopMonitoring(string accountId); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IAutoSyncSchedulerService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IAutoSyncSchedulerService.cs new file mode 100644 index 0000000..5a0a7a1 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IAutoSyncSchedulerService.cs @@ -0,0 +1,30 @@ +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for scheduling automatic remote sync checks for accounts. +/// +public interface IAutoSyncSchedulerService : IDisposable +{ + /// + /// Starts the scheduler and loads all accounts with auto-sync enabled. + /// + Task StartAsync(CancellationToken cancellationToken = default); + + /// + /// Stops all scheduled syncs. + /// + Task StopAsync(); + + /// + /// Updates the sync schedule for a specific account. + /// + /// The account identifier. + /// The interval in minutes (null to disable auto-sync). + void UpdateSchedule(string accountId, int? intervalMinutes); + + /// + /// Removes the sync schedule for a specific account. + /// + /// The account identifier. + void RemoveSchedule(string accountId); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IDebugLogger.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IDebugLogger.cs new file mode 100644 index 0000000..6fbe53a --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IDebugLogger.cs @@ -0,0 +1,38 @@ +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for logging debug information to the database when enabled for an account. +/// +public interface IDebugLogger +{ + /// + /// Logs an informational message. + /// + /// The source of the log (typically class or method name). + /// The log message. + /// Cancellation token. + Task LogInfoAsync(string source, string message, CancellationToken cancellationToken = default); + + /// + /// Logs an error message. + /// + /// The source of the log (typically class or method name). + /// The log message. + /// Optional exception details. + /// Cancellation token. + Task LogErrorAsync(string source, string message, Exception? exception = null, CancellationToken cancellationToken = default); + + /// + /// Logs a method entry. + /// + /// The source of the log (typically class.Method). + /// Cancellation token. + Task LogEntryAsync(string source, CancellationToken cancellationToken = default); + + /// + /// Logs a method exit. + /// + /// The source of the log (typically class.Method). + /// Cancellation token. + Task LogExitAsync(string source, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IFileWatcherService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IFileWatcherService.cs new file mode 100644 index 0000000..5cdfa9e --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IFileWatcherService.cs @@ -0,0 +1,38 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for monitoring local file system changes to trigger synchronization. +/// +/// +/// This service wraps FileSystemWatcher to detect file changes (create, modify, delete, rename) +/// in sync directories. Changes are debounced to avoid processing partial file writes. +/// Each account's sync directory is monitored independently. +/// +public interface IFileWatcherService : IDisposable +{ + /// + /// Starts watching a directory for file system changes. + /// + /// Account identifier for isolating change events. + /// Local directory path to monitor. + /// Thrown if accountId or localPath is null. + /// Thrown if localPath does not exist. + void StartWatching(string accountId, string localPath); + + /// + /// Stops watching a directory for the specified account. + /// + /// Account identifier whose watcher should be stopped. + void StopWatching(string accountId); + + /// + /// Gets an observable stream of file change events from all monitored directories. + /// + /// + /// Emits debounced file change events. Changes occurring within 500ms are throttled + /// to avoid processing incomplete file writes. + /// + IObservable FileChanges { get; } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IFolderTreeService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IFolderTreeService.cs new file mode 100644 index 0000000..282d679 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IFolderTreeService.cs @@ -0,0 +1,46 @@ +using AStar.Dev.OneDrive.Client.Models; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for retrieving and managing OneDrive folder hierarchies. +/// +public interface IFolderTreeService +{ + /// + /// Gets the root-level folders for the specified account. + /// + /// The account identifier. + /// Optional cancellation token. + /// Collection of root-level folder nodes. + /// + /// Root folders typically include: Documents, Pictures, Music, Videos, and the OneDrive root. + /// + Task> GetRootFoldersAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Gets the child folders for a specific parent folder. + /// + /// The account identifier. + /// The parent folder's DriveItem ID. + /// Optional cancellation token. + /// Collection of child folder nodes. + /// + /// Used for lazy loading tree nodes - only loads children when a node is expanded. + /// + Task> GetChildFoldersAsync(string accountId, string parentFolderId, bool? parentIsSelected = null, CancellationToken cancellationToken = default); + + /// + /// Gets the complete folder hierarchy for the specified account. + /// + /// The account identifier. + /// Maximum depth to traverse (default: unlimited). + /// Optional cancellation token. + /// Collection of root nodes with fully populated children. + /// + /// This method recursively loads the entire folder structure. Use with caution for accounts + /// with large numbers of folders. Consider using and + /// for lazy loading instead. + /// + Task> GetFolderHierarchyAsync(string accountId, int? maxDepth = null, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ILocalFileScanner.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ILocalFileScanner.cs new file mode 100644 index 0000000..06f0803 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ILocalFileScanner.cs @@ -0,0 +1,31 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for scanning local file system and detecting file changes. +/// +public interface ILocalFileScanner +{ + /// + /// Scans a local folder and returns metadata for all files found. + /// + /// The account identifier. + /// The local folder path to scan. + /// The corresponding OneDrive folder path. + /// Cancellation token. + /// List of file metadata for all files in the folder and subfolders. + Task> ScanFolderAsync( + string accountId, + string localFolderPath, + string oneDriveFolderPath, + CancellationToken cancellationToken = default); + + /// + /// Computes the SHA256 hash of a file. + /// + /// The path to the file. + /// Cancellation token. + /// Hexadecimal string representation of the file's SHA256 hash. + Task ComputeFileHashAsync(string filePath, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IRemoteChangeDetector.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IRemoteChangeDetector.cs new file mode 100644 index 0000000..265236d --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IRemoteChangeDetector.cs @@ -0,0 +1,23 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for detecting changes on OneDrive using delta queries. +/// +public interface IRemoteChangeDetector +{ + /// + /// Detects changes on OneDrive for the specified account and folder. + /// + /// The account identifier. + /// The OneDrive folder path to monitor. + /// Previous delta link for incremental sync, or null for initial sync. + /// Cancellation token. + /// Tuple containing list of changed file metadata and new delta link for next sync. + Task<(IReadOnlyList Changes, string? NewDeltaLink)> DetectChangesAsync( + string accountId, + string folderPath, + string? previousDeltaLink, + CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ISyncEngine.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ISyncEngine.cs new file mode 100644 index 0000000..803f045 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ISyncEngine.cs @@ -0,0 +1,32 @@ +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for synchronizing files between local storage and OneDrive. +/// +public interface ISyncEngine +{ + /// + /// Gets an observable stream of sync progress updates. + /// + IObservable Progress { get; } + + /// + /// Starts synchronization for the specified account. + /// + /// The account identifier. + /// Cancellation token. + Task StartSyncAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Stops any ongoing synchronization. + /// + Task StopSyncAsync(); + + /// + /// Gets all detected conflicts for the specified account. + /// + /// The account identifier. + /// Cancellation token. + /// List of unresolved conflicts. + Task> GetConflictsAsync(string accountId, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ISyncSelectionService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ISyncSelectionService.cs new file mode 100644 index 0000000..8ab6157 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ISyncSelectionService.cs @@ -0,0 +1,103 @@ +using AStar.Dev.OneDrive.Client.Core.Entities.Enums; +using AStar.Dev.OneDrive.Client.Models; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for managing folder selection state in the sync tree. +/// +/// +/// This service handles tri-state checkbox logic including: +/// - Cascading selection from parent to children +/// - Upward propagation to calculate indeterminate states +/// - Tracking selected folders for sync operations +/// +public interface ISyncSelectionService +{ + /// + /// Sets the selection state of a folder and cascades the change to all descendants. + /// + /// The folder to update. + /// True to select, false to deselect. + /// + /// When a folder is selected/deselected, all child folders inherit the same state. + /// This method also triggers upward propagation to update parent states. + /// + void SetSelection(OneDriveFolderNode folder, bool isSelected); + + /// + /// Updates the selection state of parent folders based on their children's states. + /// + /// The folder whose parents should be updated. + /// The root-level folders to search within. + /// + /// This method calculates indeterminate states when some (but not all) children are selected. + /// It propagates changes up the tree to the root level. + /// + void UpdateParentStates(OneDriveFolderNode folder, List rootFolders); + + /// + /// Updates the selection state of a single folder based on its children's states. + /// + /// The folder to update. + /// + /// This method calculates the folder's state (Checked/Unchecked/Indeterminate) based on + /// the current state of its children. Use this after loading children from the database. + /// + void UpdateParentState(OneDriveFolderNode folder); + + /// + /// Gets all folders that are explicitly selected for sync (excludes indeterminate). + /// + /// The root-level folders to search within. + /// List of folders with SelectionState.Checked. + /// + /// This method performs a recursive search to find all checked folders. + /// Indeterminate folders are excluded as they represent partial selection. + /// + List GetSelectedFolders(List rootFolders); + + /// + /// Clears all selection states, setting all folders to Unchecked. + /// + /// The root-level folders to clear. + void ClearAllSelections(List rootFolders); + + /// + /// Calculates the selection state of a folder based on its children. + /// + /// The folder to evaluate. + /// + /// Checked if all children are checked, + /// Unchecked if all children are unchecked, + /// Indeterminate if children have mixed states. + /// + /// + /// This is a helper method for determining parent states during upward propagation. + /// + SelectionState CalculateStateFromChildren(OneDriveFolderNode folder); + + /// + /// Saves the current selection state to the database for persistence. + /// + /// The account identifier. + /// The root-level folders containing current selections. + /// Cancellation token. + /// A task representing the asynchronous operation. + /// + /// Only explicitly checked folders are persisted. Indeterminate states are recalculated on load. + /// + Task SaveSelectionsToDatabaseAsync(string accountId, List rootFolders, CancellationToken cancellationToken = default); + + /// + /// Loads saved selection state from the database and applies it to the folder tree. + /// + /// The account identifier. + /// The root-level folders to apply selections to. + /// Cancellation token. + /// A task representing the asynchronous operation. + /// + /// After loading, parent states are automatically recalculated to reflect indeterminate states. + /// + Task LoadSelectionsFromDatabaseAsync(string accountId, List rootFolders, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IWindowPreferencesService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IWindowPreferencesService.cs new file mode 100644 index 0000000..4beeb09 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/IWindowPreferencesService.cs @@ -0,0 +1,23 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for managing window position and size preferences. +/// +public interface IWindowPreferencesService +{ + /// + /// Loads the window preferences from storage. + /// + /// Cancellation token. + /// The window preferences, or null if none exist. + Task LoadAsync(CancellationToken cancellationToken = default); + + /// + /// Saves the window preferences to storage. + /// + /// The preferences to save. + /// Cancellation token. + Task SaveAsync(WindowPreferences preferences, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/LocalFileScanner.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/LocalFileScanner.cs new file mode 100644 index 0000000..d1a84b7 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/LocalFileScanner.cs @@ -0,0 +1,176 @@ +using System.IO.Abstractions; +using System.Security.Cryptography; +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Models.Enums; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for scanning local file system and detecting file changes. +/// +public sealed class LocalFileScanner : ILocalFileScanner +{ + private readonly IFileSystem _fileSystem; + + public LocalFileScanner(IFileSystem fileSystem) + { + ArgumentNullException.ThrowIfNull(fileSystem); + _fileSystem = fileSystem; + } + + /// + public async Task> ScanFolderAsync( + string accountId, + string localFolderPath, + string oneDriveFolderPath, + CancellationToken cancellationToken = default) + { + var indexOfDrives = localFolderPath.IndexOf("drives", StringComparison.OrdinalIgnoreCase); + if(indexOfDrives >= 0) + { + var indexOfColon = localFolderPath.IndexOf(":/", StringComparison.OrdinalIgnoreCase); + if(indexOfColon > 0) + { + var part1 = localFolderPath[..indexOfDrives]; + var part2 = localFolderPath[(indexOfColon + 2)..]; + localFolderPath = part1 + part2; + } + } + + await DebugLog.EntryAsync("LocalFileScanner.ScanFolderAsync", cancellationToken); + ArgumentNullException.ThrowIfNull(accountId); + ArgumentNullException.ThrowIfNull(localFolderPath); + ArgumentNullException.ThrowIfNull(oneDriveFolderPath); + + if(!_fileSystem.Directory.Exists(localFolderPath)) return []; + + await DebugLog.InfoAsync("LocalFileScanner.ScanFolderAsync", $"Scanning folder: {localFolderPath}", cancellationToken); + var fileMetadataList = new List(); + await ScanDirectoryRecursiveAsync( + accountId, + localFolderPath, + oneDriveFolderPath, + fileMetadataList, + cancellationToken); + await DebugLog.ExitAsync("LocalFileScanner.ScanFolderAsync", cancellationToken); + + return fileMetadataList; + } + + private async Task ScanDirectoryRecursiveAsync( + string accountId, + string currentLocalPath, + string currentOneDrivePath, + List fileMetadataList, + CancellationToken cancellationToken) + { + await DebugLog.EntryAsync("LocalFileScanner.ScanDirectoryRecursiveAsync", cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var files = _fileSystem.Directory.GetFiles(currentLocalPath); + foreach(var filePath in files) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + IFileInfo fileInfo = _fileSystem.FileInfo.New(filePath); + if(!fileInfo.Exists) continue; + + var relativePath = GetRelativePath(currentLocalPath, filePath); + var oneDrivePath = CombinePaths(currentOneDrivePath, relativePath); + var hash = await ComputeFileHashAsync(filePath, cancellationToken); + + var metadata = new FileMetadata( + Id: string.Empty, // Will be populated from OneDrive after upload + AccountId: accountId, + Name: fileInfo.Name, + Path: oneDrivePath, + Size: fileInfo.Length, + LastModifiedUtc: fileInfo.LastWriteTimeUtc, + LocalPath: filePath, + CTag: null, + ETag: null, + LocalHash: hash, + SyncStatus: FileSyncStatus.PendingUpload, + LastSyncDirection: null); + + fileMetadataList.Add(metadata); + } + catch(UnauthorizedAccessException) + { + // Skip files we don't have access to +#pragma warning disable S3626 // Jump statements should not be redundant + continue; +#pragma warning restore S3626 // Jump statements should not be redundant + } + catch(IOException) + { + // Skip files that are locked or in use +#pragma warning disable S3626 // Jump statements should not be redundant + continue; +#pragma warning restore S3626 // Jump statements should not be redundant + } + } + + var directories = _fileSystem.Directory.GetDirectories(currentLocalPath); + foreach(var directory in directories) + { + var relativePath = GetRelativePath(currentLocalPath, directory); + var oneDrivePath = CombinePaths(currentOneDrivePath, relativePath); + + await ScanDirectoryRecursiveAsync( + accountId, + directory, + oneDrivePath, + fileMetadataList, + cancellationToken); + } + } + catch(UnauthorizedAccessException) + { + // Skip directories we don't have access to + } + catch(DirectoryNotFoundException) + { + // Directory was deleted during scan + } + + await DebugLog.ExitAsync("LocalFileScanner.ScanDirectoryRecursiveAsync", cancellationToken); + } + + /// + public async Task ComputeFileHashAsync(string filePath, CancellationToken cancellationToken = default) + { + using FileSystemStream stream = _fileSystem.File.OpenRead(filePath); + var hashBytes = await SHA256.HashDataAsync(stream, cancellationToken); + return Convert.ToHexString(hashBytes); + } + + private static string GetRelativePath(string basePath, string fullPath) + { + var baseUri = new Uri(EnsureTrailingSlash(basePath)); + var fullUri = new Uri(fullPath); + Uri relativeUri = baseUri.MakeRelativeUri(fullUri); + return Uri.UnescapeDataString(relativeUri.ToString()); + } + + private static string EnsureTrailingSlash(string path) + => !path.EndsWith(Path.DirectorySeparatorChar) && !path.EndsWith(Path.AltDirectorySeparatorChar) + ? path + Path.DirectorySeparatorChar + : path; + + private static string CombinePaths(string basePath, string relativePath) + { + basePath = basePath.Replace('\\', '/'); + relativePath = relativePath.Replace('\\', '/'); + + if(!basePath.EndsWith('/')) basePath += '/'; + + if(relativePath.StartsWith('/')) relativePath = relativePath[1..]; + + return basePath + relativePath; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/LogCleanupBackgroundService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/LogCleanupBackgroundService.cs new file mode 100644 index 0000000..038d366 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/LogCleanupBackgroundService.cs @@ -0,0 +1,61 @@ +using AStar.Dev.OneDrive.Client.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Background service to clean up old SyncSessionLogs and DebugLogs entries. +/// +public sealed class LogCleanupBackgroundService : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + private static readonly TimeSpan CleanupInterval = TimeSpan.FromHours(12); // Run twice a day + private static readonly TimeSpan RetentionPeriod = TimeSpan.FromDays(14); + + public LogCleanupBackgroundService(IServiceProvider serviceProvider, ILogger logger) + { + _scopeFactory = serviceProvider.GetRequiredService(); + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while(!stoppingToken.IsCancellationRequested) + { + try + { + using IServiceScope scope = _scopeFactory.CreateScope(); + AppDbContext db = scope.ServiceProvider.GetRequiredService(); + DateTime cutoff = DateTime.UtcNow - RetentionPeriod; + + var sessionLogsDeleted = await db.SyncSessionLogs + .Where(x => x.StartedUtc < cutoff) + .ExecuteDeleteAsync(stoppingToken); + + var debugLogsDeleted = await db.DebugLogs + .Where(x => x.TimestampUtc < cutoff) + .ExecuteDeleteAsync(stoppingToken); + + _logger.LogInformation("LogCleanupBackgroundService: Deleted {SessionLogs} session logs and {DebugLogs} debug logs older than {Cutoff}", sessionLogsDeleted, debugLogsDeleted, cutoff); + } + catch(Exception ex) + { + _logger.LogError(ex, "LogCleanupBackgroundService: Error during cleanup"); + } + + try + { + await Task.Delay(CleanupInterval, stoppingToken); + } + catch(TaskCanceledException) + { + // Service is stopping + break; + } + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/OneDriveServices/FolderTreeService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/OneDriveServices/FolderTreeService.cs new file mode 100644 index 0000000..1379afb --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/OneDriveServices/FolderTreeService.cs @@ -0,0 +1,169 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.FromV3.Authentication; +using AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; +using AStar.Dev.OneDrive.Client.Models; +using Microsoft.Graph.Models; + +namespace AStar.Dev.OneDrive.Client.FromV3.OneDriveServices; + +/// +/// Service for retrieving and managing OneDrive folder hierarchies. +/// +public sealed class FolderTreeService : IFolderTreeService +{ + private readonly IGraphApiClient _graphApiClient; + private readonly IAuthService _authService; + private readonly ISyncConfigurationRepository _syncConfigurationRepository; + + /// + /// Initializes a new instance of the class. + /// + /// The Graph API client. + /// The authentication service. + /// The sync configuration repository. + public FolderTreeService(IGraphApiClient graphApiClient, IAuthService authService, ISyncConfigurationRepository syncConfigurationRepository) + { + ArgumentNullException.ThrowIfNull(graphApiClient); + ArgumentNullException.ThrowIfNull(authService); + _graphApiClient = graphApiClient; + _authService = authService; + _syncConfigurationRepository = syncConfigurationRepository; + } + + /// + public async Task> GetRootFoldersAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + // Verify account is authenticated + var isAuthenticated = await _authService.IsAuthenticatedAsync(accountId, cancellationToken); + if(!isAuthenticated) return []; + + IEnumerable driveItems = await _graphApiClient.GetRootChildrenAsync(accountId, cancellationToken); + IEnumerable folders = driveItems.Where(item => item.Folder is not null); + + var nodes = new List(); + foreach(DriveItem? item in folders) + { + if(item.Id is null || item.Name is null) continue; + + var node = new OneDriveFolderNode( + id: item.Id, + name: item.Name, + path: $"/{item.Name}", + parentId: item.ParentReference?.Id, + isFolder: true) + { + IsSelected = false + }; + + // Add placeholder child so expansion toggle appears + node.Children.Add(new OneDriveFolderNode()); + + nodes.Add(node); + await ((Task)_syncConfigurationRepository.AddAsync(new SyncConfiguration + ( + 0, + accountId, + node.Path, + false, + DateTime.UtcNow + ), cancellationToken)).WaitAsync(cancellationToken); + } + + return nodes; + } + + /// + public async Task> GetChildFoldersAsync(string accountId, string parentFolderId, bool? parentIsSelected = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + ArgumentNullException.ThrowIfNull(parentFolderId); + + // Verify account is authenticated + var isAuthenticated = await _authService.IsAuthenticatedAsync(accountId, cancellationToken); + if(!isAuthenticated) return []; + + // Get parent folder to build paths + DriveItem? parentItem = await _graphApiClient.GetDriveItemAsync(accountId, parentFolderId, cancellationToken); + var parentPath = parentItem?.ParentReference?.Path is not null + ? $"{parentItem.ParentReference.Path}/{parentItem.Name}" + : $"/{parentItem?.Name}"; + + IEnumerable driveItems = await _graphApiClient.GetDriveItemChildrenAsync(accountId, parentFolderId, cancellationToken); + IEnumerable folders = driveItems.Where(item => item.Folder is not null); + + var nodes = new List(); + foreach(DriveItem? item in folders) + { + if(item.Id is null || item.Name is null) continue; + + // Retrieve or create sync config for this folder + SyncConfiguration updatedSyncConfiguration = await _syncConfigurationRepository.AddAsync(new SyncConfiguration + ( + 0, + accountId, + $"{parentPath}/{item.Name}", + false, + DateTime.UtcNow + ), cancellationToken); + + // If parent is selected, propagate selection to children + bool? isSelected = parentIsSelected == true || updatedSyncConfiguration.IsSelected; + + var node = new OneDriveFolderNode( + id: item.Id, + name: item.Name, + path: $"{parentPath}/{item.Name}", + parentId: parentFolderId, + isFolder: true) + { + IsSelected = isSelected + }; + + // Add placeholder child so expansion toggle appears + node.Children.Add(new OneDriveFolderNode()); + + nodes.Add(node); + } + + return nodes; + } + + /// + public async Task> GetFolderHierarchyAsync(string accountId, int? maxDepth = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + + // Verify account is authenticated + var isAuthenticated = await _authService.IsAuthenticatedAsync(accountId, cancellationToken); + if(!isAuthenticated) return []; + + IReadOnlyList rootFolders = await GetRootFoldersAsync(accountId, cancellationToken); + var rootList = rootFolders.ToList(); + + if(maxDepth is null or > 0) + foreach(OneDriveFolderNode? folder in rootList) await LoadChildrenRecursiveAsync(accountId, folder, maxDepth, 1, cancellationToken); + + return rootList; + } + + private async Task LoadChildrenRecursiveAsync( + string accountId, + OneDriveFolderNode parentNode, + int? maxDepth, + int currentDepth, + CancellationToken cancellationToken) + { + if(maxDepth.HasValue && currentDepth >= maxDepth.Value) return; + + IReadOnlyList children = await GetChildFoldersAsync(accountId, parentNode.Id, parentNode.IsSelected, cancellationToken); + foreach(OneDriveFolderNode child in children) + { + parentNode.Children.Add(child); + await LoadChildrenRecursiveAsync(accountId, child, maxDepth, currentDepth + 1, cancellationToken); + } + + parentNode.ChildrenLoaded = true; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/OneDriveServices/GraphApiClient.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/OneDriveServices/GraphApiClient.cs new file mode 100644 index 0000000..1061e32 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/OneDriveServices/GraphApiClient.cs @@ -0,0 +1,310 @@ +using AStar.Dev.OneDrive.Client.FromV3.Authentication; +using Microsoft.Graph; +using Microsoft.Graph.Models; +using Microsoft.Kiota.Abstractions.Authentication; + +namespace AStar.Dev.OneDrive.Client.FromV3.OneDriveServices; + +/// +/// Wrapper implementation for Microsoft Graph API client. +/// +/// +/// Wraps GraphServiceClient to provide a testable abstraction for OneDrive operations. +/// +public sealed class GraphApiClient : IGraphApiClient +{ + public async Task> GetDriveItemChildrenAsync(string accountId, string itemId, CancellationToken cancellationToken = default) + => await GetDriveItemChildrenAsync(accountId, itemId, 200, cancellationToken); + private readonly IAuthService _authService; + + /// + /// Initializes a new instance of the class. + /// + /// The authentication service. + public GraphApiClient(IAuthService authService) + { + ArgumentNullException.ThrowIfNull(authService); + _authService = authService; + } + + private async Task CreateGraphClientAsync(string accountId, CancellationToken cancellationToken) + { + if(string.IsNullOrEmpty(accountId)) throw new ArgumentException("Account ID cannot be null or empty", nameof(accountId)); + + // Create a token provider that uses the auth service + var authProvider = new BaseBearerTokenAuthenticationProvider( + new GraphTokenProvider(_authService, accountId)); + + return new GraphServiceClient(authProvider); + } + + // Token provider implementation for Graph API + private sealed class GraphTokenProvider : IAccessTokenProvider + { + private readonly IAuthService _authService; + private readonly string _accountId; + + public GraphTokenProvider(IAuthService authService, string accountId) + { + _authService = authService; + _accountId = accountId; + } + + public AllowedHostsValidator AllowedHostsValidator => new(); + + public async Task GetAuthorizationTokenAsync( + Uri uri, + Dictionary? additionalAuthenticationContext = null, + CancellationToken cancellationToken = default) + { + var token = await _authService.GetAccessTokenAsync(_accountId, cancellationToken) ?? throw new InvalidOperationException($"Failed to acquire access token for account: {_accountId}"); + return token; + } + } + + /// + public async Task GetMyDriveAsync(string accountId, CancellationToken cancellationToken = default) + { + GraphServiceClient graphClient = await CreateGraphClientAsync(accountId, cancellationToken); + return await graphClient.Me.Drive.GetAsync(cancellationToken: cancellationToken); + } + + /// + public async Task GetDriveRootAsync(string accountId, CancellationToken cancellationToken = default) + { + GraphServiceClient graphClient = await CreateGraphClientAsync(accountId, cancellationToken); + Drive? drive = await graphClient.Me.Drive.GetAsync(cancellationToken: cancellationToken); + return drive?.Id is null ? null : await graphClient.Drives[drive.Id].Root.GetAsync(cancellationToken: cancellationToken); + } + + /// + public async Task> GetDriveItemChildrenAsync(string accountId, string itemId, int maxItemsInBatch = 200, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(itemId); + + await DebugLog.EntryAsync("GraphApiClient.GetDriveItemChildrenAsync", cancellationToken); + await DebugLog.InfoAsync("GraphApiClient.GetDriveItemChildrenAsync", $"Fetching children for item ID: {itemId}", cancellationToken); + + GraphServiceClient graphClient = await CreateGraphClientAsync(accountId, cancellationToken); + Drive? drive = await graphClient.Me.Drive.GetAsync(cancellationToken: cancellationToken); + if(drive?.Id is null) + { + await DebugLog.ErrorAsync("GraphApiClient.GetDriveItemChildrenAsync", "Drive ID is null", null, cancellationToken); + await DebugLog.ExitAsync("GraphApiClient.GetDriveItemChildrenAsync", cancellationToken); + return []; + } + + await DebugLog.InfoAsync("GraphApiClient.GetDriveItemChildrenAsync", $"Using drive ID: {drive.Id}", cancellationToken); + + var itemsList = new List(); + string? nextLink = null; + do + { + DriveItemCollectionResponse? response = nextLink is null + ? await graphClient.Drives[drive.Id].Items[itemId].Children.GetAsync(q => q.QueryParameters.Top = maxItemsInBatch, cancellationToken: cancellationToken) + : await graphClient.RequestAdapter.SendAsync( + new Microsoft.Kiota.Abstractions.RequestInformation + { + HttpMethod = Microsoft.Kiota.Abstractions.Method.GET, + URI = new Uri(nextLink) + }, + DriveItemCollectionResponse.CreateFromDiscriminatorValue, + null, + cancellationToken); + + await DebugLog.InfoAsync("GraphApiClient.GetDriveItemChildrenAsync", $"Response received - Value count: {response?.Value?.Count ?? 0}, NextLink: {response?.OdataNextLink}", cancellationToken); + + IEnumerable items = response?.Value?.Where(item => item.Deleted is null) ?? []; + itemsList.AddRange(items); + + nextLink = response?.OdataNextLink; + } while(!string.IsNullOrEmpty(nextLink)); + + await DebugLog.InfoAsync("GraphApiClient.GetDriveItemChildrenAsync", $"After filtering deleted items: {itemsList.Count} items", cancellationToken); + await DebugLog.ExitAsync("GraphApiClient.GetDriveItemChildrenAsync", cancellationToken); + return itemsList; + } + + /// + public async Task GetDriveItemAsync(string accountId, string itemId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(itemId); + + GraphServiceClient graphClient = await CreateGraphClientAsync(accountId, cancellationToken); + Drive? drive = await graphClient.Me.Drive.GetAsync(cancellationToken: cancellationToken); + return drive?.Id is null ? null : await graphClient.Drives[drive.Id].Items[itemId].GetAsync(cancellationToken: cancellationToken); + } + + /// + public async Task> GetRootChildrenAsync(string accountId, CancellationToken cancellationToken = default) + { + GraphServiceClient graphClient = await CreateGraphClientAsync(accountId, cancellationToken); + Drive? drive = await graphClient.Me.Drive.GetAsync(cancellationToken: cancellationToken); + if(drive?.Id is null) return []; + + DriveItem? root = await graphClient.Drives[drive.Id].Root.GetAsync(cancellationToken: cancellationToken); + if(root?.Id is null) return []; + + DriveItemCollectionResponse? response = await graphClient.Drives[drive.Id].Items[root.Id].Children.GetAsync(cancellationToken: cancellationToken); + return response?.Value ?? Enumerable.Empty(); + } + + /// + public async Task DownloadFileAsync(string accountId, string itemId, string localFilePath, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(itemId); + ArgumentNullException.ThrowIfNull(localFilePath); + + GraphServiceClient graphClient = await CreateGraphClientAsync(accountId, cancellationToken); + Drive? drive = await graphClient.Me.Drive.GetAsync(cancellationToken: cancellationToken); + if(drive?.Id is null) throw new InvalidOperationException("Unable to access user's drive"); + + // Download file content stream from OneDrive + Stream contentStream = await graphClient.Drives[drive.Id].Items[itemId].Content.GetAsync(cancellationToken: cancellationToken) ?? throw new InvalidOperationException($"Failed to download file content for item {itemId}"); + + // Ensure the directory exists + var directory = Path.GetDirectoryName(localFilePath); + if(!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) _ = Directory.CreateDirectory(directory); + + // Write the stream to local file + using var fileStream = new FileStream(localFilePath, FileMode.Create, FileAccess.Write, FileShare.None); + await contentStream.CopyToAsync(fileStream, cancellationToken); + } + + /// + public async Task UploadFileAsync(string accountId, string localFilePath, string remotePath, IProgress? progress = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(localFilePath); + ArgumentNullException.ThrowIfNull(remotePath); + + if(!File.Exists(localFilePath)) throw new FileNotFoundException($"Local file not found: {localFilePath}", localFilePath); + + GraphServiceClient graphClient = await CreateGraphClientAsync(accountId, cancellationToken); + Drive? drive = await graphClient.Me.Drive.GetAsync(cancellationToken: cancellationToken); + if(drive?.Id is null) throw new InvalidOperationException("Unable to access user's drive"); + + var fileInfo = new FileInfo(localFilePath); + const long SmallFileThreshold = 4 * 1024 * 1024; // 4MB - Graph API threshold for simple upload + + // Normalize remote path (remove leading slash, Graph API uses root:/{path} format) + var normalizedPath = remotePath.TrimStart('/'); + + if(fileInfo.Length < SmallFileThreshold) + { + // Simple upload for small files - use path-based addressing + using var fileStream = new FileStream(localFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); + + // PUT to /drives/{driveId}/root:/{path}:/content + DriveItem uploadedItem = await graphClient.Drives[drive.Id] + .Items[$"root:/{normalizedPath}:"] + .Content + .PutAsync(fileStream, cancellationToken: cancellationToken) ?? throw new InvalidOperationException($"Upload failed for file: {localFilePath}"); + + // Report full file size as uploaded for small files + progress?.Report(fileInfo.Length); + + return uploadedItem; + } + else + { + // Resumable upload session for large files + var requestBody = new Microsoft.Graph.Drives.Item.Items.Item.CreateUploadSession.CreateUploadSessionPostRequestBody + { + Item = new DriveItemUploadableProperties + { + AdditionalData = new Dictionary + { + { "@microsoft.graph.conflictBehavior", "replace" } + } + } + }; + + UploadSession? uploadSession = await graphClient.Drives[drive.Id] + .Items[$"root:/{normalizedPath}:"] + .CreateUploadSession + .PostAsync(requestBody, cancellationToken: cancellationToken); + + if(uploadSession?.UploadUrl is null) throw new InvalidOperationException($"Failed to create upload session for file: {localFilePath}"); + + // Upload in chunks (recommended 5-10MB per chunk for optimal performance) + const int ChunkSize = 5 * 1024 * 1024; // 5MB chunks + using var fileStream = new FileStream(localFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); + + long position = 0; + var totalLength = fileStream.Length; + + using var httpClient = new HttpClient(); + + while(position < totalLength) + { + cancellationToken.ThrowIfCancellationRequested(); + + var chunkSize = (int)Math.Min(ChunkSize, totalLength - position); + var chunk = new byte[chunkSize]; + var bytesRead = await fileStream.ReadAsync(chunk.AsMemory(0, chunkSize), cancellationToken); + + if(bytesRead != chunkSize) throw new InvalidOperationException($"Failed to read expected bytes from file. Expected: {chunkSize}, Read: {bytesRead}"); + + var contentRange = $"bytes {position}-{position + chunkSize - 1}/{totalLength}"; + + using var content = new ByteArrayContent(chunk); + content.Headers.ContentLength = chunkSize; + content.Headers.ContentRange = new System.Net.Http.Headers.ContentRangeHeaderValue(position, position + chunkSize - 1, totalLength); + + HttpResponseMessage response = await httpClient.PutAsync(uploadSession.UploadUrl, content, cancellationToken); + + if(!response.IsSuccessStatusCode && response.StatusCode != System.Net.HttpStatusCode.Accepted) throw new InvalidOperationException($"Chunk upload failed. Status: {response.StatusCode}, Range: {contentRange}"); + + position += chunkSize; + + // Report progress after each chunk + progress?.Report(position); + + // If this is the last chunk, parse the response to get the DriveItem + if(position >= totalLength && response.IsSuccessStatusCode) + { + var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); + DriveItem uploadedItem = System.Text.Json.JsonSerializer.Deserialize(responseContent) ?? throw new InvalidOperationException("Failed to deserialize uploaded DriveItem from response"); + return uploadedItem; + } + } + + throw new InvalidOperationException("Upload completed but no DriveItem was returned"); + } + } + + /// + public async Task DeleteFileAsync(string accountId, string itemId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + ArgumentNullException.ThrowIfNull(itemId); + + await DebugLog.EntryAsync("GraphApiClient.DeleteFileAsync", cancellationToken); + await DebugLog.InfoAsync("GraphApiClient.DeleteFileAsync", $"Attempting to delete remote file. AccountId: {accountId}, ItemId: {itemId}", cancellationToken); + + GraphServiceClient graphClient = await CreateGraphClientAsync(accountId, cancellationToken); + Drive? drive = await graphClient.Me.Drive.GetAsync(cancellationToken: cancellationToken); + if(drive?.Id is null) + { + await DebugLog.ErrorAsync("GraphApiClient.DeleteFileAsync", "Drive ID is null. Cannot delete file.", null, cancellationToken); + await DebugLog.ExitAsync("GraphApiClient.DeleteFileAsync", cancellationToken); + throw new InvalidOperationException("Unable to access user's drive"); + } + + // DELETE /drives/{driveId}/items/{itemId} + try + { + await graphClient.Drives[drive.Id].Items[itemId].DeleteAsync(cancellationToken: cancellationToken); + await DebugLog.InfoAsync("GraphApiClient.DeleteFileAsync", $"Successfully deleted remote file. AccountId: {accountId}, ItemId: {itemId}", cancellationToken); + } + catch(Exception ex) + { + await DebugLog.ErrorAsync("GraphApiClient.DeleteFileAsync", $"Exception during remote file deletion. AccountId: {accountId}, ItemId: {itemId}", ex, cancellationToken); + throw; + } + finally + { + await DebugLog.ExitAsync("GraphApiClient.DeleteFileAsync", cancellationToken); + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/OneDriveServices/IGraphApiClient.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/OneDriveServices/IGraphApiClient.cs new file mode 100644 index 0000000..33f35d6 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/OneDriveServices/IGraphApiClient.cs @@ -0,0 +1,94 @@ +using Microsoft.Graph.Models; + +namespace AStar.Dev.OneDrive.Client.FromV3.OneDriveServices; + +/// +/// Wrapper interface for Microsoft Graph API client to enable testing. +/// +/// +/// Microsoft.Graph.GraphServiceClient and related classes are difficult to mock directly. +/// This interface provides a testable abstraction over OneDrive folder and file operations. +/// All methods require an accountId to specify which account to use for the operation. +/// +public interface IGraphApiClient +{ + /// + /// Gets the root drive for the specified account. + /// + /// The account identifier. + /// Optional cancellation token. + /// The user's root drive. + Task GetMyDriveAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Gets the root folder of the user's drive for the specified account. + /// + /// The account identifier. + /// Optional cancellation token. + /// The root drive item. + Task GetDriveRootAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Gets the children (files and folders) of a specific drive item. + /// + /// The account identifier. + /// The drive item ID. + /// Optional cancellation token. + /// Collection of child drive items. + Task> GetDriveItemChildrenAsync(string accountId, string itemId, CancellationToken cancellationToken = default); + Task> GetDriveItemChildrenAsync(string accountId, string itemId, int maxItemsInBatch, CancellationToken cancellationToken = default); + + /// + /// Gets a specific drive item by its ID. + /// + /// The account identifier. + /// The drive item ID. + /// Optional cancellation token. + /// The drive item, or null if not found. + Task GetDriveItemAsync(string accountId, string itemId, CancellationToken cancellationToken = default); + + /// + /// Gets the children of the root folder. + /// + /// The account identifier. + /// Optional cancellation token. + /// Collection of root-level drive items. + Task> GetRootChildrenAsync(string accountId, CancellationToken cancellationToken = default); + + /// + /// Downloads a file from OneDrive to a local path. + /// + /// The account identifier. + /// The drive item ID of the file to download. + /// The local file path where the file should be saved. + /// Optional cancellation token. + /// A task representing the asynchronous download operation. + Task DownloadFileAsync(string accountId, string itemId, string localFilePath, CancellationToken cancellationToken = default); + + /// + /// Uploads a file to OneDrive at the specified path. + /// + /// The account identifier. + /// The local file path to upload. + /// The remote path where the file should be uploaded (relative to drive root, e.g., "/Documents/file.txt"). + /// Optional progress reporter for upload progress (reports bytes uploaded). + /// Optional cancellation token. + /// The uploaded DriveItem with OneDrive metadata (ID, CTag, ETag, etc.). + /// + /// Uses simple upload for files under 4MB and resumable upload session for larger files. + /// The remotePath should include the filename and be relative to the drive root. + /// + Task UploadFileAsync(string accountId, string localFilePath, string remotePath, IProgress? progress = null, CancellationToken cancellationToken = default); + + /// + /// Deletes a file from OneDrive. + /// + /// The account identifier. + /// The OneDrive item ID to delete. + /// Optional cancellation token. + /// A task representing the asynchronous delete operation. + /// + /// This permanently deletes the file from OneDrive (moves to recycle bin if available). + /// + Task DeleteFileAsync(string accountId, string itemId, CancellationToken cancellationToken = default); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ProgressReporterService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ProgressReporterService.cs new file mode 100644 index 0000000..3f293b3 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ProgressReporterService.cs @@ -0,0 +1,53 @@ +using System.Reactive.Subjects; +using AStar.Dev.OneDrive.Client.Core.Entities; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +public static class ProgressReporterService +{ + public static (int placeholder, BehaviorSubject progressSubject) ReportProgress(SyncState progress, BehaviorSubject progressSubject, DateTime lastProgressUpdate, long lastCompletedBytes, List<(DateTime Timestamp, long Bytes)> transferHistory) + { + DateTime now = DateTime.UtcNow; + var elapsedSeconds = (now - lastProgressUpdate).TotalSeconds; + + double megabytesPerSecond = 0; + if(elapsedSeconds > 0.1) + { + var bytesDelta = progress.CompletedBytes - lastCompletedBytes; + if(bytesDelta > 0) + { + var megabytesDelta = bytesDelta / (1024.0 * 1024.0); + megabytesPerSecond = megabytesDelta / elapsedSeconds; + + transferHistory.Add((now, progress.CompletedBytes)); + if(transferHistory.Count > 10) transferHistory.RemoveAt(0); + + if(transferHistory.Count >= 2) + { + var totalElapsed = (now - transferHistory[0].Timestamp).TotalSeconds; + var totalTransferred = progress.CompletedBytes - transferHistory[0].Bytes; + if(totalElapsed > 0) megabytesPerSecond = totalTransferred / (1024.0 * 1024.0) / totalElapsed; + } + } + } + + int? estimatedSecondsRemaining = null; + var bytesForEta = progress.TotalBytes; + if(megabytesPerSecond > 0.01 && progress.CompletedBytes < bytesForEta) + { + var remainingBytes = bytesForEta - progress.CompletedBytes; + var remainingMegabytes = remainingBytes / (1024.0 * 1024.0); + estimatedSecondsRemaining = (int)Math.Ceiling(remainingMegabytes / megabytesPerSecond); + } + + SyncState updatedProgress = progress with + { + MegabytesPerSecond = megabytesPerSecond, + EstimatedSecondsRemaining = estimatedSecondsRemaining, + LastUpdateUtc = now + }; + progressSubject.OnNext(updatedProgress); + + return (0, progressSubject); + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/RemoteChangeDetector.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/RemoteChangeDetector.cs new file mode 100644 index 0000000..ca7ae5b --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/RemoteChangeDetector.cs @@ -0,0 +1,253 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Models.Enums; +using AStar.Dev.OneDrive.Client.FromV3.OneDriveServices; +using Microsoft.Graph.Models; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for detecting changes on OneDrive using delta queries. +/// +/// +/// Note: Full delta query support requires Microsoft.Graph SDK capabilities. +/// This implementation provides a foundation that can be extended when delta APIs are integrated. +/// For now, it performs full scans and compares against known state. +/// +public sealed class RemoteChangeDetector : IRemoteChangeDetector +{ + private readonly IGraphApiClient _graphApiClient; + + public RemoteChangeDetector(IGraphApiClient graphApiClient) + { + ArgumentNullException.ThrowIfNull(graphApiClient); + _graphApiClient = graphApiClient; + } + + /// + public async Task<(IReadOnlyList Changes, string? NewDeltaLink)> DetectChangesAsync( + string accountId, + string folderPath, + string? previousDeltaLink, + CancellationToken cancellationToken = default) + { + await DebugLog.EntryAsync("RemoteChangeDetector.DetectChangesAsync", cancellationToken); + ArgumentNullException.ThrowIfNull(accountId); + ArgumentNullException.ThrowIfNull(folderPath); + + cancellationToken.ThrowIfCancellationRequested(); + + await DebugLog.InfoAsync("RemoteChangeDetector.DetectChangesAsync", $"Scanning folder: '{folderPath}'", cancellationToken); + + // Clean the folder path of Graph API prefixes before using it for file paths + var cleanedFolderPath = CleanGraphApiPathPrefix(folderPath); + if(cleanedFolderPath != folderPath) await DebugLog.InfoAsync("RemoteChangeDetector.DetectChangesAsync", $"Cleaned folder path from '{folderPath}' to '{cleanedFolderPath}'", cancellationToken); + + // For initial implementation, scan the folder tree + // Note: For large OneDrive accounts (100k+ files), this can take several minutes + // In future sprints, this will be enhanced with proper delta query support + var changes = new List(); + DriveItem? rootItem = await GetFolderItemAsync(accountId, folderPath, cancellationToken); + + if(rootItem is not null) + { + await DebugLog.InfoAsync("RemoteChangeDetector.DetectChangesAsync", $"Folder item found, starting recursive scan", cancellationToken); + // Add a practical limit for initial scan to prevent timeout + // This will be removed when we implement proper delta query support + const int maxFiles = 10000; // Limit initial scan to 10k files + // Use cleaned path for building file paths + await ScanFolderRecursiveAsync(accountId, rootItem, cleanedFolderPath, changes, cancellationToken, maxFiles); + System.Diagnostics.Debug.WriteLine($"[RemoteChangeDetector] Scan complete: {changes.Count} files found in {cleanedFolderPath}"); + await DebugLog.InfoAsync("RemoteChangeDetector.DetectChangesAsync", $"Scan complete: {changes.Count} files found", cancellationToken); + } + else + { + var errorMessage = $"Remote folder not found: '{folderPath}'. Please verify the folder exists and you have access to it."; + await DebugLog.ErrorAsync("RemoteChangeDetector.DetectChangesAsync", errorMessage, null, cancellationToken); + throw new InvalidOperationException(errorMessage); + } + + // Generate a simple delta token based on timestamp + // In production, this would be the actual deltaLink from Graph API + var newDeltaLink = $"delta_{DateTime.UtcNow:yyyyMMddHHmmss}"; + + await DebugLog.ExitAsync("RemoteChangeDetector.DetectChangesAsync", cancellationToken); + + return (changes, newDeltaLink); + } + + private async Task GetFolderItemAsync(string accountId, string folderPath, CancellationToken cancellationToken) + { + await DebugLog.EntryAsync("RemoteChangeDetector.GetFolderItemAsync", cancellationToken); + await DebugLog.InfoAsync("RemoteChangeDetector.GetFolderItemAsync", $"Looking for folder: '{folderPath}'", cancellationToken); + + // Defensive: Strip Graph API path prefixes that might be in stored paths + // This handles cases where paths like "/drive/root:/Documents" or "/drives/{id}/root:/Documents" were stored + if(folderPath.StartsWith("/drive/root:", StringComparison.OrdinalIgnoreCase)) + { + folderPath = folderPath["/drive/root:".Length..]; + await DebugLog.InfoAsync("RemoteChangeDetector.GetFolderItemAsync", $"Stripped '/drive/root:' prefix, new path: '{folderPath}'", cancellationToken); + } + else if(folderPath.StartsWith("/drives/", StringComparison.OrdinalIgnoreCase)) + { + var rootIndex = folderPath.IndexOf("/root:", StringComparison.OrdinalIgnoreCase); + if(rootIndex >= 0) + { + folderPath = folderPath[(rootIndex + "/root:".Length)..]; + await DebugLog.InfoAsync("RemoteChangeDetector.GetFolderItemAsync", $"Stripped '/drives/.../root:' prefix, new path: '{folderPath}'", cancellationToken); + } + } + + // For root or empty path, return the drive root + if(folderPath == "/" || string.IsNullOrEmpty(folderPath)) + { + await DebugLog.InfoAsync("RemoteChangeDetector.GetFolderItemAsync", "Returning drive root", cancellationToken); + return await _graphApiClient.GetDriveRootAsync(accountId, cancellationToken); + } + + // Clean up the path + folderPath = folderPath.Trim('/'); + await DebugLog.InfoAsync("RemoteChangeDetector.GetFolderItemAsync", $"Trimmed path: '{folderPath}'", cancellationToken); + + // Get root and traverse path segments + DriveItem? currentItem = await _graphApiClient.GetDriveRootAsync(accountId, cancellationToken); + if(currentItem?.Id is null) + { + await DebugLog.ErrorAsync("RemoteChangeDetector.GetFolderItemAsync", "Failed to get drive root", null, cancellationToken); + return null; + } + + var pathSegments = folderPath.Split('/', StringSplitOptions.RemoveEmptyEntries); + await DebugLog.InfoAsync("RemoteChangeDetector.GetFolderItemAsync", $"Path segments: [{string.Join(", ", pathSegments)}]", cancellationToken); + + foreach(var segment in pathSegments) + { + await DebugLog.InfoAsync("RemoteChangeDetector.GetFolderItemAsync", $"Looking for segment: '{segment}'", cancellationToken); + IEnumerable children = await _graphApiClient.GetDriveItemChildrenAsync(accountId, currentItem.Id, cancellationToken); + await DebugLog.InfoAsync("RemoteChangeDetector.GetFolderItemAsync", $"Found {children.Count()} children in current folder", cancellationToken); + + currentItem = children.FirstOrDefault(c => + c.Name?.Equals(segment, StringComparison.OrdinalIgnoreCase) == true && + c.Folder is not null); + + if(currentItem?.Id is null) + { + await DebugLog.ErrorAsync("RemoteChangeDetector.GetFolderItemAsync", $"Folder segment '{segment}' not found", null, cancellationToken); + return null; + } + + await DebugLog.InfoAsync("RemoteChangeDetector.GetFolderItemAsync", $"Found folder: '{currentItem.Name}' (ID: {currentItem.Id})", cancellationToken); + } + + await DebugLog.ExitAsync("RemoteChangeDetector.GetFolderItemAsync", cancellationToken); + + return currentItem; + } + + private async Task ScanFolderRecursiveAsync( + string accountId, + DriveItem parentItem, + string currentPath, + List changes, + CancellationToken cancellationToken, + int maxFiles = int.MaxValue) + { + await DebugLog.EntryAsync("RemoteChangeDetector.ScanFolderRecursiveAsync", cancellationToken); + await DebugLog.InfoAsync("RemoteChangeDetector.ScanFolderRecursiveAsync", $"Scanning folder: '{currentPath}' (ID: {parentItem.Id})", cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + + if(changes.Count >= maxFiles) + { + await DebugLog.InfoAsync("RemoteChangeDetector.ScanFolderRecursiveAsync", $"Max files limit ({maxFiles}) reached", cancellationToken); + return; // Reached the limit + } + + if(parentItem.Id is null) + { + await DebugLog.ErrorAsync("RemoteChangeDetector.ScanFolderRecursiveAsync", "Parent item ID is null", null, cancellationToken); + return; + } + + IEnumerable children = await _graphApiClient.GetDriveItemChildrenAsync(accountId, parentItem.Id, cancellationToken); + await DebugLog.InfoAsync("RemoteChangeDetector.ScanFolderRecursiveAsync", $"Found {children.Count()} items in '{currentPath}'", cancellationToken); + + var fileCount = 0; + var folderCount = 0; + + foreach(DriveItem item in children) + { + cancellationToken.ThrowIfCancellationRequested(); + + if(changes.Count >= maxFiles) return; // Reached the limit + + if(item.File is not null && item.Id is not null && item.Name is not null) + { + // It's a file + fileCount++; + var itemPath = CombinePaths(currentPath, item.Name); + FileMetadata metadata = ConvertToFileMetadata(accountId, item, itemPath); + changes.Add(metadata); + if(changes.Count % 500 == 0) System.Diagnostics.Debug.WriteLine($"[RemoteChangeDetector] Progress: {changes.Count} files scanned"); + } + else if(item.Folder is not null && item.Id is not null && item.Name is not null) + { + // It's a folder - scan recursively + folderCount++; + var itemPath = CombinePaths(currentPath, item.Name); + await DebugLog.InfoAsync("RemoteChangeDetector.ScanFolderRecursiveAsync", $"Recursing into subfolder: '{item.Name}'", cancellationToken); + await ScanFolderRecursiveAsync(accountId, item, itemPath, changes, cancellationToken, maxFiles); + } + } + + await DebugLog.InfoAsync("RemoteChangeDetector.ScanFolderRecursiveAsync", $"Completed '{currentPath}': {fileCount} files, {folderCount} subfolders", cancellationToken); + + await DebugLog.ExitAsync("RemoteChangeDetector.ScanFolderRecursiveAsync", cancellationToken); + } + + private static FileMetadata ConvertToFileMetadata(string accountId, DriveItem item, string path) => new( + Id: item.Id ?? string.Empty, + AccountId: accountId, + Name: item.Name ?? string.Empty, + Path: path, + Size: item.Size ?? 0, + LastModifiedUtc: item.LastModifiedDateTime?.UtcDateTime ?? DateTime.UtcNow, + LocalPath: string.Empty, // Will be set during download + CTag: item.CTag, + ETag: item.ETag, + LocalHash: null, // Will be computed after download + SyncStatus: FileSyncStatus.PendingDownload, + LastSyncDirection: SyncDirection.Download); + + private static string CombinePaths(string basePath, string name) + { + basePath = basePath.Replace('\\', '/'); + name = name.Replace('\\', '/'); + + if(!basePath.EndsWith('/')) basePath += '/'; + + if(name.StartsWith('/')) name = name[1..]; + + return basePath + name; + } + + /// + /// Cleans Graph API path prefixes from folder paths. + /// + /// The path that may contain Graph API prefixes. + /// The cleaned path without Graph API prefixes. + private static string CleanGraphApiPathPrefix(string path) + { + if(string.IsNullOrEmpty(path)) return path; + + // Strip /drive/root: prefix + if(path.StartsWith("/drive/root:", StringComparison.OrdinalIgnoreCase)) return path["/drive/root:".Length..]; + + // Strip /drives/{drive-id}/root: prefix + if(path.StartsWith("/drives/", StringComparison.OrdinalIgnoreCase)) + { + var rootIndex = path.IndexOf("/root:", StringComparison.OrdinalIgnoreCase); + if(rootIndex >= 0) return path[(rootIndex + "/root:".Length)..]; + } + + return path; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ServiceConfiguration.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ServiceConfiguration.cs new file mode 100644 index 0000000..ffbd316 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/ServiceConfiguration.cs @@ -0,0 +1,109 @@ +using System.IO.Abstractions; +using AStar.Dev.OneDrive.Client.FromV3.Authentication; +using AStar.Dev.OneDrive.Client.FromV3.OneDriveServices; +using AStar.Dev.OneDrive.Client.Infrastructure.Data; +using AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; +using AStar.Dev.OneDrive.Client.SyncConflicts; +using AStar.Dev.OneDrive.Client.ViewModels; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Testably.Abstractions; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Configures dependency injection services for the application. +/// +public static class ServiceConfiguration +{ + /// + /// Configures and returns the service provider with all application services. + /// + /// Configured service provider. + public static ServiceProvider ConfigureServices() + { + var services = new ServiceCollection(); + + // Database + _ = services.AddDbContext(options => + options.UseSqlite(DatabaseConfiguration.ConnectionString)); + + // Repositories + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + + // Load authentication configuration + IConfigurationRoot configuration = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: false) + .Build(); + + var authConfig = AuthConfiguration.LoadFromConfiguration(configuration); + + // Authentication - registered as singleton with factory + _ = services.AddSingleton(provider => + // AuthService.CreateAsync must be called synchronously during startup + // This is acceptable as it's a one-time initialization cost + AuthService.CreateAsync(authConfig).GetAwaiter().GetResult()); + + // Services + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + + // ViewModels + _ = services.AddTransient(); + _ = services.AddTransient(); + _ = services.AddTransient(); + _ = services.AddTransient(); + _ = services.AddTransient(); + _ = services.AddTransient(); + + // Logging + _ = services.AddLogging(builder => + { + _ = builder.AddConsole(); + _ = builder.SetMinimumLevel(LogLevel.Information); + }); + + // Background Services + _ = services.AddHostedService(); + + return services.BuildServiceProvider(); + } + + /// + /// Ensures the database is created and migrations are applied. + /// + /// The service provider. + public static void EnsureDatabaseCreated(ServiceProvider serviceProvider) + { + using IServiceScope scope = serviceProvider.CreateScope(); + AppDbContext context = scope.ServiceProvider.GetRequiredService(); + try + { + context.Database.Migrate(); + } + catch + { + // If EnsureCreated fails (e.g. due to existing but outdated database), apply migrations + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/SyncEngine.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/SyncEngine.cs new file mode 100644 index 0000000..23b1643 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/SyncEngine.cs @@ -0,0 +1,900 @@ +using System.Reactive.Subjects; +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Entities.Enums; +using AStar.Dev.OneDrive.Client.Core.Models.Enums; +using AStar.Dev.OneDrive.Client.FromV3.OneDriveServices; +using AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; +using Microsoft.Graph.Models; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for synchronizing files between local storage and OneDrive. +/// +/// +/// Supports bidirectional sync with conflict detection and resolution. +/// Uses LastWriteWins strategy: when both local and remote files change, the newer timestamp wins. +/// +public sealed partial class SyncEngine : ISyncEngine, IDisposable +{ + private const double AllowedTimeDifference = 60.0; + private readonly ILocalFileScanner _localFileScanner; + private readonly IRemoteChangeDetector _remoteChangeDetector; + private readonly IFileMetadataRepository _fileMetadataRepository; + private readonly ISyncConfigurationRepository _syncConfigurationRepository; + private readonly IAccountRepository _accountRepository; + private readonly IGraphApiClient _graphApiClient; + private readonly ISyncConflictRepository _syncConflictRepository; + private readonly ISyncSessionLogRepository _syncSessionLogRepository; + private readonly IFileOperationLogRepository _fileOperationLogRepository; + private readonly BehaviorSubject _progressSubject; + private CancellationTokenSource? _syncCancellation; + private string? _currentSessionId; + private int _syncInProgress; + private DateTime _lastProgressUpdate = DateTime.UtcNow; + private long _lastCompletedBytes; + private readonly List<(DateTime Timestamp, long Bytes)> _transferHistory = []; + + public SyncEngine( + ILocalFileScanner localFileScanner, + IRemoteChangeDetector remoteChangeDetector, + IFileMetadataRepository fileMetadataRepository, + ISyncConfigurationRepository syncConfigurationRepository, + IAccountRepository accountRepository, + IGraphApiClient graphApiClient, + ISyncConflictRepository syncConflictRepository, + ISyncSessionLogRepository syncSessionLogRepository, + IFileOperationLogRepository fileOperationLogRepository) + { + _localFileScanner = localFileScanner ?? throw new ArgumentNullException(nameof(localFileScanner)); + _remoteChangeDetector = remoteChangeDetector ?? throw new ArgumentNullException(nameof(remoteChangeDetector)); + _fileMetadataRepository = fileMetadataRepository ?? throw new ArgumentNullException(nameof(fileMetadataRepository)); + _syncConfigurationRepository = syncConfigurationRepository ?? throw new ArgumentNullException(nameof(syncConfigurationRepository)); + _accountRepository = accountRepository ?? throw new ArgumentNullException(nameof(accountRepository)); + _graphApiClient = graphApiClient ?? throw new ArgumentNullException(nameof(graphApiClient)); + _syncConflictRepository = syncConflictRepository ?? throw new ArgumentNullException(nameof(syncConflictRepository)); + _syncSessionLogRepository = syncSessionLogRepository ?? throw new ArgumentNullException(nameof(syncSessionLogRepository)); + _fileOperationLogRepository = fileOperationLogRepository ?? throw new ArgumentNullException(nameof(fileOperationLogRepository)); + + var initialState = Core.Entities.SyncState.CreateInitial(string.Empty); + + _progressSubject = new BehaviorSubject(initialState); + } + + /// + public IObservable Progress => _progressSubject; + + /// + public async Task StartSyncAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + DebugLogContext.SetAccountId(accountId); + + if(SyncIsAlreadyRunning()) + { + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Sync already in progress for account {accountId}, ignoring duplicate request", cancellationToken); + await DebugLog.ExitAsync("SyncEngine.StartSyncAsync", cancellationToken); + return; + } + + _syncCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + try + { + ResetTrackingDetails(); + + await DebugLog.EntryAsync("SyncEngine.StartSyncAsync", cancellationToken); + + ReportProgress(accountId, SyncStatus.Running); + + IReadOnlyList selectedFolders = await _syncConfigurationRepository.GetSelectedFoldersAsync(accountId, cancellationToken); + + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Starting sync with {selectedFolders.Count} selected folders: {string.Join(", ", selectedFolders)}", cancellationToken); + + if(selectedFolders.Count == 0) + { + ReportProgress(accountId, SyncStatus.Idle); + return; + } + + AccountInfo? account = await _accountRepository.GetByIdAsync(accountId, cancellationToken); + if(account is null) + { + ReportProgress(accountId, SyncStatus.Failed); + return; + } + + if(account.EnableDetailedSyncLogging) + { + var sessionLog = SyncSessionLog.CreateInitialRunning(accountId); + await _syncSessionLogRepository.AddAsync(sessionLog, cancellationToken); + _currentSessionId = sessionLog.Id; + } + else + _currentSessionId = null; + + List allLocalFiles = await GetAllLocalFiles(accountId, selectedFolders, account); + IReadOnlyList existingFiles = await _fileMetadataRepository.GetByAccountIdAsync(accountId, cancellationToken); + var existingFilesDict = existingFiles.ToDictionary(f => f.Path ?? "", f => f); + + // Detect remote changes in selected folders FIRST (before deciding what to upload) + var allRemoteFiles = new List(); + foreach(var folder in selectedFolders) + { + if(string.IsNullOrEmpty(folder)) + continue; + var displayFolder = FormatScanningFolderForDisplay(folder); + ReportProgress(accountId, SyncStatus.Running, 0, 0, 0, 0, currentScanningFolder: displayFolder); + + (IReadOnlyList? remoteFiles, _) = await _remoteChangeDetector.DetectChangesAsync(accountId, folder, previousDeltaLink: null, _syncCancellation!.Token); + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Folder '{folder}' returned {remoteFiles?.Count ?? 0} remote files", cancellationToken); + if(remoteFiles?.Count > 0) allRemoteFiles.AddRange(remoteFiles); + } + + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Total remote files before deduplication: {allRemoteFiles.Count}", cancellationToken); + + allRemoteFiles = [.. allRemoteFiles + .GroupBy(f => f.Path ?? "") + .Select(g => g.First())]; + + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Total remote files after deduplication: {allRemoteFiles.Count}", cancellationToken); + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Remote file paths: {string.Join(", ", allRemoteFiles.Select(f => f.Path))}", cancellationToken); + + var remoteFilesDict = allRemoteFiles.ToDictionary(f => f.Path ?? "", f => f); + var localFilesDict = allLocalFiles.ToDictionary(f => f.Path ?? "", f => f); + + var filesToUpload = new List(); + foreach(FileMetadata localFile in allLocalFiles) + { + if(existingFilesDict.TryGetValue(localFile.Path, out FileMetadata? existingFile)) + { + if(existingFile.SyncStatus is FileSyncStatus.PendingUpload or FileSyncStatus.Failed) + { + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"File needs upload (status={existingFile.SyncStatus}): {localFile.Name}", cancellationToken); + FileMetadata fileToUpload = existingFile with + { + LocalPath = localFile.LocalPath, + LocalHash = localFile.LocalHash, + Size = localFile.Size, + LastModifiedUtc = localFile.LastModifiedUtc + }; + filesToUpload.Add(fileToUpload); + } + else + { + var bothHaveHashes = !string.IsNullOrEmpty(existingFile.LocalHash) && !string.IsNullOrEmpty(localFile.LocalHash); + + bool hasChanged; + if(bothHaveHashes) + { + hasChanged = existingFile.LocalHash != localFile.LocalHash; + if(hasChanged) await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"File marked as changed: {localFile.Name} - Hash changed (DB: {existingFile.LocalHash}, Local: {localFile.LocalHash})", cancellationToken); + } + else + { + hasChanged = existingFile.Size != localFile.Size; + if(hasChanged) await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"File marked as changed: {localFile.Name} - Size changed (DB: {existingFile.Size}, Local: {localFile.Size})", cancellationToken); + } + + if(hasChanged) filesToUpload.Add(localFile); + } + } + else if(!remoteFilesDict.ContainsKey(localFile.Path)) + { + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"New local file to upload: {localFile.Name}", cancellationToken); + filesToUpload.Add(localFile); + } + } + + var filesToDownload = new List(); + var remotePathsSet = allRemoteFiles.Select(f => f.Path).ToHashSet(); + var conflictCount = 0; + var conflictPaths = new HashSet(); + var filesToRecordWithoutTransfer = new List(); + + foreach(FileMetadata remoteFile in allRemoteFiles) + { + if(existingFilesDict.TryGetValue(remoteFile.Path, out FileMetadata? existingFile)) + { + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Found file in DB: {remoteFile.Path}, DB Status={existingFile.SyncStatus}", cancellationToken); + var timeDiff = Math.Abs((existingFile.LastModifiedUtc - remoteFile.LastModifiedUtc).TotalSeconds); + var remoteHasChanged = (!string.IsNullOrWhiteSpace(existingFile.CTag) || + timeDiff > 3600.0 || + existingFile.Size != remoteFile.Size) && (existingFile.CTag != remoteFile.CTag); + + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Remote file check: {remoteFile.Path} - DB CTag={existingFile.CTag}, Remote CTag={remoteFile.CTag}, DB Time={existingFile.LastModifiedUtc:yyyy-MM-dd HH:mm:ss}, Remote Time={remoteFile.LastModifiedUtc:yyyy-MM-dd HH:mm:ss}, Diff={timeDiff:F1}s, DB Size={existingFile.Size}, Remote Size={remoteFile.Size}, RemoteHasChanged={remoteHasChanged}", cancellationToken); + + if(remoteHasChanged) + { + var localFileHasChanged = false; + + if(localFilesDict.TryGetValue(remoteFile.Path, out FileMetadata? localFile)) + { + var localTimeDiff = Math.Abs((existingFile.LastModifiedUtc - localFile.LastModifiedUtc).TotalSeconds); + localFileHasChanged = localTimeDiff > 1.0 || existingFile.Size != localFile.Size; + } + + if(localFileHasChanged) + { + FileMetadata localFileFromDict = localFilesDict[remoteFile.Path]; + var conflict = SyncConflict.CreateUnresolvedConflict(accountId, remoteFile.Path, localFileFromDict.LastModifiedUtc, remoteFile.LastModifiedUtc, localFileFromDict.Size, remoteFile.Size); + + SyncConflict? existingConflict = await _syncConflictRepository.GetByFilePathAsync(accountId, remoteFile.Path, cancellationToken); + if(existingConflict is null) + { + await _syncConflictRepository.AddAsync(conflict, cancellationToken); + conflictCount++; + } + else + conflictCount++; + + _ = conflictPaths.Add(remoteFile.Path); + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"CONFLICT detected for {remoteFile.Path}: local and remote both changed", cancellationToken); + + if(_currentSessionId is not null) + { + var operationLog = FileOperationLog.CreateSyncConflictLog(_currentSessionId, accountId, remoteFile.Path, localFileFromDict.LocalPath, remoteFile.Id, + localFileFromDict.LocalHash, localFileFromDict.Size, localFileFromDict.LastModifiedUtc, remoteFile.LastModifiedUtc); + await _fileOperationLogRepository.AddAsync(operationLog, cancellationToken); + } + + continue; + } + + var localFilePath = Path.Combine(account.LocalSyncPath, remoteFile.Path.TrimStart('/')); + FileMetadata fileWithLocalPath = remoteFile with { LocalPath = localFilePath }; + filesToDownload.Add(fileWithLocalPath); + } + } + else + { + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"File NOT in DB: {remoteFile.Path} - first sync or new file", cancellationToken); + if(localFilesDict.TryGetValue(remoteFile.Path, out FileMetadata? localFile)) + { + var timeDiff = Math.Abs((localFile.LastModifiedUtc - remoteFile.LastModifiedUtc).TotalSeconds); + var filesMatch = localFile.Size == remoteFile.Size && timeDiff <= AllowedTimeDifference; + + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"First sync compare: {remoteFile.Path} - Local: Size={localFile.Size}, Time={localFile.LastModifiedUtc:yyyy-MM-dd HH:mm:ss}, Remote: Size={remoteFile.Size}, Time={remoteFile.LastModifiedUtc:yyyy-MM-dd HH:mm:ss}, TimeDiff={timeDiff:F1}s, Match={filesMatch}", cancellationToken); + + if(filesMatch) + { + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"File exists both places and matches: {remoteFile.Path} - recording in DB", cancellationToken); + FileMetadata matchedFile = localFile with + { + Id = remoteFile.Id, + CTag = remoteFile.CTag, + ETag = remoteFile.ETag, + SyncStatus = FileSyncStatus.Synced, + LastSyncDirection = null + }; + filesToRecordWithoutTransfer.Add(matchedFile); + } + else + { + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"First sync CONFLICT: {remoteFile.Path} - files differ (TimeDiff={timeDiff:F1}s, SizeMatch={localFile.Size == remoteFile.Size})", cancellationToken); + var conflict = SyncConflict.CreateUnresolvedConflict(accountId, remoteFile.Path, localFile.LastModifiedUtc, remoteFile.LastModifiedUtc, localFile.Size, remoteFile.Size); + + await _syncConflictRepository.AddAsync(conflict, cancellationToken); + conflictCount++; + _ = conflictPaths.Add(remoteFile.Path); + + if(_currentSessionId is not null) + { + var operationLog = FileOperationLog.CreateSyncConflictLog(_currentSessionId, accountId, remoteFile.Path, localFile.LocalPath, remoteFile.Id, + localFile.LocalHash, localFile.Size, localFile.LastModifiedUtc, remoteFile.LastModifiedUtc); + await _fileOperationLogRepository.AddAsync(operationLog, cancellationToken); + } + } + } + else + { + var localFilePath = Path.Combine(account.LocalSyncPath, remoteFile.Path.TrimStart('/')); + FileMetadata fileWithLocalPath = remoteFile with { LocalPath = localFilePath }; + filesToDownload.Add(fileWithLocalPath); + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"New remote file to download: {remoteFile.Path}", cancellationToken); + } + } + } + + var localPathsSet = allLocalFiles.Select(f => f.Path).ToHashSet(); + List deletedFromOneDrive = SelectFilesDeletedFromOneDriveButSyncedLocally(existingFiles, remotePathsSet, localPathsSet); + + foreach(FileMetadata file in deletedFromOneDrive) + { + try + { + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"File deleted from OneDrive: {file.Path} - deleting local copy at {file.LocalPath}", cancellationToken); + if(File.Exists(file.LocalPath)) File.Delete(file.LocalPath); + + await _fileMetadataRepository.DeleteAsync(file.Id, cancellationToken); + } + catch(Exception ex) + { + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Failed to delete local file {file.Path}: {ex.Message}. Continuing with other deletions.", cancellationToken); + } + } + + List deletedLocally = GetFilesDeletedLocally(allLocalFiles, remotePathsSet, localPathsSet); + + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Local deletion detection: {deletedLocally.Count} files to delete from OneDrive.", cancellationToken); + foreach(FileMetadata file in deletedLocally) await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Candidate for remote deletion: Path={file.Path}, Id={file.Id}, SyncStatus={file.SyncStatus}, ExistsLocally={File.Exists(file.LocalPath)}, ExistsRemotely={remotePathsSet.Contains(file.Path)}", cancellationToken); + + foreach(FileMetadata file in deletedLocally) + { + try + { + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Deleting from OneDrive: Path={file.Path}, Id={file.Id}, SyncStatus={file.SyncStatus}", cancellationToken); + await _graphApiClient.DeleteFileAsync(accountId, file.Id, cancellationToken); + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Deleted from OneDrive: Path={file.Path}, Id={file.Id}", cancellationToken); + await _fileMetadataRepository.DeleteAsync(file.Id, cancellationToken); + } + catch(Exception ex) + { + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Failed to delete from OneDrive {file.Path}: {ex.Message}, continuing the sync...", cancellationToken); + } + } + + var alreadyProcessedDeletions = deletedFromOneDrive.Select(f => f.Id) + .Concat(deletedLocally.Select(f => f.Id)) + .ToHashSet(); + List filesToDelete = GetFilesToDelete(existingFiles, remotePathsSet, localPathsSet, alreadyProcessedDeletions); + + var uploadPathsSet = filesToUpload.Select(f => f.Path).ToHashSet(); + var deletedPaths = deletedFromOneDrive.Select(f => f.Path).ToHashSet(); + filesToUpload = [.. filesToUpload.Where(f => !deletedPaths.Contains(f.Path) && !conflictPaths.Contains(f.Path))]; + + var totalFiles = filesToUpload.Count + filesToDownload.Count; + var totalBytes = filesToUpload.Sum(f => f.Size) + filesToDownload.Sum(f => f.Size); + var uploadBytes = filesToUpload.Sum(f => f.Size); + var downloadBytes = filesToDownload.Sum(f => f.Size); + + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Sync summary: {filesToDownload.Count} to download, {filesToUpload.Count} to upload, {filesToDelete.Count} to delete", cancellationToken); + + (filesToDownload, totalFiles, totalBytes, downloadBytes) = await RemoveDuplicatesFromDownloadList(filesToUpload, filesToDownload, totalFiles, totalBytes, downloadBytes, cancellationToken); + + ReportProgress(accountId, SyncStatus.Running, totalFiles, 0, totalBytes, 0, filesDeleted: filesToDelete.Count, conflictsDetected: conflictCount); + + var completedFiles = 0; + long completedBytes = 0; + + var maxParallelUploads = Math.Max(1, account.MaxParallelUpDownloads); + using var uploadSemaphore = new SemaphoreSlim(maxParallelUploads, maxParallelUploads); + var activeUploads = 0; + (activeUploads, completedBytes, completedFiles, List? uploadTasks) = CreateUploadTasks(accountId, existingFilesDict, filesToUpload, conflictCount, totalFiles, totalBytes, uploadBytes, completedFiles, completedBytes, uploadSemaphore, activeUploads, cancellationToken); + + await Task.WhenAll(uploadTasks); + + ResetTrackingDetails(completedBytes); + + var maxParallelDownloads = Math.Max(1, account.MaxParallelUpDownloads); + using var downloadSemaphore = new SemaphoreSlim(maxParallelDownloads, maxParallelDownloads); + var activeDownloads = 0; + (activeDownloads, completedBytes, completedFiles, List? downloadTasks) = CreateDownloadTasks(accountId, existingFilesDict, filesToDownload, conflictCount, totalFiles, totalBytes, uploadBytes, downloadBytes, completedFiles, completedBytes, downloadSemaphore, activeDownloads, cancellationToken); + + await Task.WhenAll(downloadTasks); + + await DeleteDeletedFilesFromDatabase(filesToDelete, cancellationToken); + + ReportProgress(accountId, SyncStatus.Completed, totalFiles, completedFiles, totalBytes, completedBytes, filesDeleted: filesToDelete.Count, conflictsDetected: conflictCount); + + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Sync completed: {totalFiles} files, {completedBytes} bytes", cancellationToken); + await DebugLog.ExitAsync("SyncEngine.StartSyncAsync", cancellationToken); + + await FinalizeSyncSessionAsync(_currentSessionId, filesToUpload.Count, filesToDownload.Count, filesToDelete.Count, conflictCount, completedBytes, account, cancellationToken); + } + catch(OperationCanceledException) + { + await HandleSyncCancelledAsync(_currentSessionId, cancellationToken); + ReportProgress(accountId, SyncStatus.Paused); + throw; + } + catch(Exception ex) + { + await DebugLog.ErrorAsync("SyncEngine.StartSyncAsync", $"Sync failed: {ex.Message}", ex, cancellationToken); + await HandleSyncFailedAsync(_currentSessionId, cancellationToken); + ReportProgress(accountId, SyncStatus.Failed); + throw; + } + finally + { + DebugLogContext.Clear(); + _ = Interlocked.Exchange(ref _syncInProgress, 0); + } + } + + private async Task DeleteDeletedFilesFromDatabase(List filesToDelete, CancellationToken cancellationToken) + { + foreach(FileMetadata fileToDelete in filesToDelete) await _fileMetadataRepository.DeleteAsync(fileToDelete.Id, cancellationToken); + } + + private (int activeDownloads, long completedBytes, int completedFiles, List downloadTasks) CreateDownloadTasks(string accountId, Dictionary existingFilesDict, List filesToDownload, int conflictCount, int totalFiles, long totalBytes, long uploadBytes, long downloadBytes, int completedFiles, long completedBytes, SemaphoreSlim downloadSemaphore, int activeDownloads, CancellationToken cancellationToken) + { + var batchSize = 50; + var batch = new List(batchSize); + var downloadTasks = filesToDownload.Select(async file => + { + await downloadSemaphore.WaitAsync(_syncCancellation!.Token); + _ = Interlocked.Increment(ref activeDownloads); + + try + { + _syncCancellation!.Token.ThrowIfCancellationRequested(); + + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Starting download: {file.Name} (ID: {file.Id}) to {file.LocalPath}", cancellationToken); + await DebugLog.InfoAsync("SyncEngine.DownloadFile", $"Starting download: {file.Name} (ID: {file.Id}) to {file.LocalPath}", _syncCancellation!.Token); + + var directory = Path.GetDirectoryName(file.LocalPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + await DebugLog.InfoAsync("SyncEngine.DownloadFile", $"Creating directory: {directory}", _syncCancellation!.Token); + _ = Directory.CreateDirectory(directory); + } + + if (_currentSessionId is not null) + { + var existingLocal = existingFilesDict.TryGetValue(file.Path, out FileMetadata? existingFile); + var reason = existingLocal ? "Remote file changed" : "New remote file"; + var operationLog = FileOperationLog.CreateDownloadLog( + syncSessionId: _currentSessionId, accountId: accountId, filePath: file.Path, localPath: file.LocalPath, oneDriveId: file.Id, + localHash: existingFile?.LocalHash, fileSize: file.Size, lastModifiedUtc: file.LastModifiedUtc, reason: reason); + await _fileOperationLogRepository.AddAsync(operationLog, cancellationToken); + } + + await _graphApiClient.DownloadFileAsync(accountId, file.Id, file.LocalPath, _syncCancellation!.Token); + + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Download complete: {file.Name}, computing hash...", cancellationToken); + await DebugLog.InfoAsync("SyncEngine.DownloadFile", $"Download complete: {file.Name}, computing hash...", _syncCancellation!.Token); + + var downloadedHash = await _localFileScanner.ComputeFileHashAsync(file.LocalPath, _syncCancellation!.Token); + + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Hash computed for {file.Name}: {downloadedHash}", cancellationToken); + + FileMetadata downloadedFile = file with + { + SyncStatus = FileSyncStatus.Synced, + LastSyncDirection = SyncDirection.Download, + LocalHash = downloadedHash + }; + + batch.Add(downloadedFile); + if (batch.Count >= batchSize) + { + await _fileMetadataRepository.SaveBatchAsync(batch, cancellationToken); + batch.Clear(); + } + + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Successfully synced: {file.Name}", cancellationToken); + + _ = Interlocked.Increment(ref completedFiles); + _ = Interlocked.Add(ref completedBytes, file.Size); + var finalCompleted = Interlocked.CompareExchange(ref completedFiles, 0, 0); + var finalBytes = Interlocked.Read(ref completedBytes); + var finalActiveDownloads = Interlocked.CompareExchange(ref activeDownloads, 0, 0); + ReportProgress(accountId, SyncStatus.Running, totalFiles, finalCompleted, totalBytes, finalBytes, finalActiveDownloads, conflictsDetected: conflictCount, phaseTotalBytes: uploadBytes + downloadBytes); + } + catch (Exception ex) + { + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"ERROR downloading {file.Name}: {ex.GetType().Name} - {ex.Message}", cancellationToken); + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Stack trace: {ex.StackTrace}", cancellationToken); + await DebugLog.ErrorAsync("SyncEngine.DownloadFile", $"ERROR downloading {file.Name}: {ex.Message}", ex, _syncCancellation!.Token); + + FileMetadata failedFile = file with { SyncStatus = FileSyncStatus.Failed }; + batch.Add(failedFile); + if (batch.Count >= batchSize) + { + await _fileMetadataRepository.SaveBatchAsync(batch, cancellationToken); + batch.Clear(); + } + + _ = Interlocked.Increment(ref completedFiles); + _ = Interlocked.Add(ref completedBytes, file.Size); + var finalCompleted = Interlocked.CompareExchange(ref completedFiles, 0, 0); + var finalBytes = Interlocked.Read(ref completedBytes); + ReportProgress(accountId, SyncStatus.Running, totalFiles, finalCompleted, totalBytes, finalBytes, conflictsDetected: conflictCount, phaseTotalBytes: uploadBytes + downloadBytes); + } + finally + { + _ = Interlocked.Decrement(ref activeDownloads); + _ = downloadSemaphore.Release(); + } + }).ToList(); + // Save any remaining files in the batch after all downloads complete + if(batch.Count > 0) + { + _fileMetadataRepository.SaveBatchAsync(batch, CancellationToken.None).GetAwaiter().GetResult(); + batch.Clear(); + } + + return (activeDownloads, completedBytes, completedFiles, downloadTasks); + } + + private (int activeUploads, long completedBytes, int completedFiles, List uploadTasks) CreateUploadTasks(string accountId, Dictionary existingFilesDict, List filesToUpload, int conflictCount, int totalFiles, long totalBytes, long uploadBytes, int completedFiles, long completedBytes, SemaphoreSlim uploadSemaphore, int activeUploads, CancellationToken cancellationToken) + { + var batchSize = 50; + var batch = new List(batchSize); + var uploadTasks = filesToUpload.Select(async file => + { + await uploadSemaphore.WaitAsync(_syncCancellation!.Token); + _ = Interlocked.Increment(ref activeUploads); + + try + { + _syncCancellation!.Token.ThrowIfCancellationRequested(); + + var isExistingFile = existingFilesDict.TryGetValue(file.Path, out FileMetadata? existingFile) && + (!string.IsNullOrEmpty(existingFile.Id) || + existingFile.SyncStatus == FileSyncStatus.PendingUpload || + existingFile.SyncStatus == FileSyncStatus.Failed); + + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Uploading {file.Name}: Path={file.Path}, IsExisting={isExistingFile}, LocalPath={file.LocalPath}", cancellationToken); + + if (!isExistingFile) + { + FileMetadata pendingFile = file with + { + SyncStatus = FileSyncStatus.PendingUpload + }; + await _fileMetadataRepository.AddAsync(pendingFile, cancellationToken); + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Added pending upload record to database: {file.Name}", cancellationToken); + } + + if (_currentSessionId is not null) + { + var reason = isExistingFile ? "File changed locally" : "New file"; + var operationLog = FileOperationLog.CreateUploadLog(syncSessionId: _currentSessionId, accountId: accountId, filePath: file.Path, localPath: file.LocalPath, oneDriveId: existingFile?.Id, + localHash: existingFile?.LocalHash, fileSize: file.Size, lastModifiedUtc: file.LastModifiedUtc, reason); + await _fileOperationLogRepository.AddAsync(operationLog, cancellationToken); + } + + var baseCompletedBytes = Interlocked.Read(ref completedBytes); + var currentActiveUploads = Interlocked.CompareExchange(ref activeUploads, 0, 0); + var uploadProgress = new Progress(bytesUploaded => + { + var currentCompletedBytes = baseCompletedBytes + bytesUploaded; + var currentCompleted = Interlocked.CompareExchange(ref completedFiles, 0, 0); + ReportProgress(accountId, SyncStatus.Running, totalFiles, currentCompleted, totalBytes, currentCompletedBytes, filesUploading: currentActiveUploads, conflictsDetected: conflictCount, phaseTotalBytes: uploadBytes); + }); + + DriveItem uploadedItem = await _graphApiClient.UploadFileAsync( + accountId, + file.LocalPath, + file.Path, + uploadProgress, + _syncCancellation!.Token); + + if (uploadedItem.LastModifiedDateTime.HasValue && File.Exists(file.LocalPath)) + { + File.SetLastWriteTimeUtc(file.LocalPath, uploadedItem.LastModifiedDateTime.Value.UtcDateTime); + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Synchronized local timestamp to OneDrive: {file.Name}, OldTime={file.LastModifiedUtc:yyyy-MM-dd HH:mm:ss}, NewTime={uploadedItem.LastModifiedDateTime.Value.UtcDateTime:yyyy-MM-dd HH:mm:ss}", cancellationToken); + } + + DateTime oneDriveTimestamp = uploadedItem.LastModifiedDateTime?.UtcDateTime ?? file.LastModifiedUtc; + + FileMetadata uploadedFile = isExistingFile + ? existingFile! with + { + Id = uploadedItem.Id ?? existingFile.Id, + CTag = uploadedItem.CTag, + ETag = uploadedItem.ETag, + LocalPath = file.LocalPath, + LocalHash = file.LocalHash, + Size = file.Size, + LastModifiedUtc = oneDriveTimestamp, + SyncStatus = FileSyncStatus.Synced, + LastSyncDirection = SyncDirection.Upload + } + : file with + { + Id = uploadedItem.Id ?? throw new InvalidOperationException($"Upload succeeded but no ID returned for {file.Name}"), + CTag = uploadedItem.CTag, + ETag = uploadedItem.ETag, + LastModifiedUtc = oneDriveTimestamp, + SyncStatus = FileSyncStatus.Synced, + LastSyncDirection = SyncDirection.Upload + }; + + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Upload successful: {file.Name}, OneDrive ID={uploadedFile.Id}, CTag={uploadedFile.CTag}", cancellationToken); + + batch.Add(uploadedFile); + if (batch.Count >= batchSize) + { + await _fileMetadataRepository.SaveBatchAsync(batch, cancellationToken); + batch.Clear(); + } + + _ = Interlocked.Increment(ref completedFiles); + _ = Interlocked.Add(ref completedBytes, file.Size); + var finalCompleted = Interlocked.CompareExchange(ref completedFiles, 0, 0); + var finalBytes = Interlocked.Read(ref completedBytes); + var finalActiveUploads = Interlocked.CompareExchange(ref activeUploads, 0, 0); + ReportProgress(accountId, SyncStatus.Running, totalFiles, finalCompleted, totalBytes, finalBytes, filesUploading: finalActiveUploads, conflictsDetected: conflictCount, phaseTotalBytes: uploadBytes); + } + catch (Exception ex) + { + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"Upload failed for {file.Name}: {ex.Message}", cancellationToken); + + FileMetadata failedFile = file with + { + SyncStatus = FileSyncStatus.Failed + }; + + FileMetadata? existingDbFile = !string.IsNullOrEmpty(failedFile.Id) + ? await _fileMetadataRepository.GetByIdAsync(failedFile.Id, cancellationToken) + : await _fileMetadataRepository.GetByPathAsync(accountId, failedFile.Path, cancellationToken); + + if (existingDbFile is not null) + batch.Add(failedFile); + else + await _fileMetadataRepository.AddAsync(failedFile, cancellationToken); + + if (batch.Count >= batchSize) + { + await _fileMetadataRepository.SaveBatchAsync(batch, cancellationToken); + batch.Clear(); + } + + _ = Interlocked.Increment(ref completedFiles); + var finalCompleted = Interlocked.CompareExchange(ref completedFiles, 0, 0); + var finalBytes = Interlocked.Read(ref completedBytes); + ReportProgress(accountId, SyncStatus.Running, totalFiles, finalCompleted, totalBytes, finalBytes, conflictsDetected: conflictCount, phaseTotalBytes: uploadBytes); + } + finally + { + _ = Interlocked.Decrement(ref activeUploads); + _ = uploadSemaphore.Release(); + } + }).ToList(); + // Save any remaining files in the batch after all uploads complete + if(batch.Count > 0) + { + _fileMetadataRepository.SaveBatchAsync(batch, CancellationToken.None).GetAwaiter().GetResult(); + batch.Clear(); + } + + return (activeUploads, completedBytes, completedFiles, uploadTasks); + } + + private static async Task<(List filesToDownload, int totalFiles, long totalBytes, long downloadBytes)> RemoveDuplicatesFromDownloadList(List filesToUpload, List filesToDownload, int totalFiles, long totalBytes, long downloadBytes, CancellationToken cancellationToken) + { + var duplicateDownloads = filesToDownload.GroupBy(f => f.Path).Where(g => g.Count() > 1).ToList(); + if(duplicateDownloads.Count > 0) + { + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"WARNING: Found {duplicateDownloads.Count} duplicate paths in download list!", cancellationToken); + foreach(IGrouping? dup in duplicateDownloads) await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $" Duplicate: {dup.Key} appears {dup.Count()} times", cancellationToken); + + filesToDownload = [.. filesToDownload.GroupBy(f => f.Path).Select(g => g.First())]; + await DebugLog.InfoAsync("SyncEngine.StartSyncAsync", $"After deduplication: {filesToDownload.Count} files to download", cancellationToken); + + totalFiles = filesToUpload.Count + filesToDownload.Count; + totalBytes = filesToUpload.Sum(f => f.Size) + filesToDownload.Sum(f => f.Size); + downloadBytes = filesToDownload.Sum(f => f.Size); + } + + return (filesToDownload, totalFiles, totalBytes, downloadBytes); + } + + private static List GetFilesToDelete(IReadOnlyList existingFiles, HashSet remotePathsSet, HashSet localPathsSet, HashSet alreadyProcessedDeletions) => [.. existingFiles + .Where(f => !remotePathsSet.Contains(f.Path) && + !localPathsSet.Contains(f.Path) && + !string.IsNullOrWhiteSpace(f.Id) && + !alreadyProcessedDeletions.Contains(f.Id)) + .Where(f => f.Id is not null)]; + + private static List GetFilesDeletedLocally(List allLocalFiles, HashSet remotePathsSet, HashSet localPathsSet) => [.. allLocalFiles + .Where(f => !localPathsSet.Contains(f.Path) && + (remotePathsSet.Contains(f.Path) || f.SyncStatus == FileSyncStatus.Synced) && + !string.IsNullOrEmpty(f.Id))]; + + private static List SelectFilesDeletedFromOneDriveButSyncedLocally(IReadOnlyList existingFiles, HashSet remotePathsSet, HashSet localPathsSet) => [.. existingFiles + .Where(f => !remotePathsSet.Contains(f.Path) && + localPathsSet.Contains(f.Path) && + f.SyncStatus == FileSyncStatus.Synced)]; + + private async Task> GetAllLocalFiles(string accountId, IReadOnlyList selectedFolders, AccountInfo account) + { + var allLocalFiles = new List(); + foreach(var folder in selectedFolders) + { + if(string.IsNullOrEmpty(folder)) + continue; + var localFolderPath = Path.Combine(account.LocalSyncPath, folder.TrimStart('/')); + IReadOnlyList localFiles = await _localFileScanner.ScanFolderAsync( + accountId, + localFolderPath, + folder, + _syncCancellation?.Token ?? CancellationToken.None); + if(localFiles?.Count > 0) allLocalFiles.AddRange(localFiles); + } + + return allLocalFiles; + } + + private void ResetTrackingDetails(long completedBytes = 0) + { + _transferHistory.Clear(); + _lastProgressUpdate = DateTime.UtcNow; + _lastCompletedBytes = completedBytes; + } + + private bool SyncIsAlreadyRunning() => Interlocked.CompareExchange(ref _syncInProgress, 1, 0) != 0; + + private async Task FinalizeSyncSessionAsync(string? sessionId, int uploadCount, int downloadCount, int deleteCount, int conflictCount, long completedBytes, AccountInfo account, CancellationToken cancellationToken) + { + if(sessionId is null) + return; + + try + { + SyncSessionLog? session = await _syncSessionLogRepository.GetByIdAsync(sessionId, cancellationToken); + if(session is not null) + { + SyncSessionLog updatedSession = session with + { + CompletedUtc = DateTime.UtcNow, + Status = SyncStatus.Completed, + FilesUploaded = uploadCount, + FilesDownloaded = downloadCount, + FilesDeleted = deleteCount, + ConflictsDetected = conflictCount, + TotalBytes = completedBytes + }; + await _syncSessionLogRepository.UpdateAsync(updatedSession, cancellationToken); + } + + await UpdateLastAccountSyncAsync(account, cancellationToken); + } + catch(Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[SyncEngine] Failed to finalize sync session log: {ex.Message}"); + } + finally + { + _currentSessionId = null; + } + } + + private async Task HandleSyncCancelledAsync(string? sessionId, CancellationToken cancellationToken) + { + if(sessionId is null) + return; + + SyncSessionLog? session = await _syncSessionLogRepository.GetByIdAsync(sessionId, cancellationToken); + if(session is not null) + { + SyncSessionLog updatedSession = session with { CompletedUtc = DateTime.UtcNow, Status = SyncStatus.Paused }; + await _syncSessionLogRepository.UpdateAsync(updatedSession, cancellationToken); + } + } + + private async Task HandleSyncFailedAsync(string? sessionId, CancellationToken cancellationToken) + { + if(sessionId is null) + return; + + SyncSessionLog? session = await _syncSessionLogRepository.GetByIdAsync(sessionId, cancellationToken); + if(session is not null) + { + SyncSessionLog updatedSession = session with { CompletedUtc = DateTime.UtcNow, Status = SyncStatus.Failed }; + await _syncSessionLogRepository.UpdateAsync(updatedSession, cancellationToken); + } + } + + private async Task UpdateLastAccountSyncAsync(AccountInfo account, CancellationToken cancellationToken) + { + AccountInfo lastSyncUpdate = account with + { + LastSyncUtc = DateTime.UtcNow + }; + + await _accountRepository.UpdateAsync(lastSyncUpdate, cancellationToken); + } + + /// + public Task StopSyncAsync() + { + _syncCancellation?.Cancel(); + return Task.CompletedTask; + } + + /// + public async Task> GetConflictsAsync(string accountId, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + return await _syncConflictRepository.GetUnresolvedByAccountIdAsync(accountId, cancellationToken); + } + + public void ReportProgress( + string accountId, + SyncStatus status, + int totalFiles = 0, + int completedFiles = 0, + long totalBytes = 0, + long completedBytes = 0, + int filesDownloading = 0, + int filesUploading = 0, + int filesDeleted = 0, + int conflictsDetected = 0, + string? currentScanningFolder = null, + long? phaseTotalBytes = null) + { + + DateTime now = DateTime.UtcNow; + var elapsedSeconds = (now - _lastProgressUpdate).TotalSeconds; + + double megabytesPerSecond = 0; + if(elapsedSeconds > 0.1) + { + var bytesDelta = completedBytes - _lastCompletedBytes; + if(bytesDelta > 0) + { + var megabytesDelta = bytesDelta / (1024.0 * 1024.0); + megabytesPerSecond = megabytesDelta / elapsedSeconds; + + _transferHistory.Add((now, completedBytes)); + if(_transferHistory.Count > 10) _transferHistory.RemoveAt(0); + + if(_transferHistory.Count >= 2) + { + var totalElapsed = (now - _transferHistory[0].Timestamp).TotalSeconds; + var totalTransferred = completedBytes - _transferHistory[0].Bytes; + if(totalElapsed > 0) megabytesPerSecond = totalTransferred / (1024.0 * 1024.0) / totalElapsed; + } + + _lastProgressUpdate = now; + _lastCompletedBytes = completedBytes; + } + } + + int? estimatedSecondsRemaining = null; + var bytesForEta = phaseTotalBytes ?? totalBytes; + if(megabytesPerSecond > 0.01 && completedBytes < bytesForEta) + { + var remainingBytes = bytesForEta - completedBytes; + var remainingMegabytes = remainingBytes / (1024.0 * 1024.0); + estimatedSecondsRemaining = (int)Math.Ceiling(remainingMegabytes / megabytesPerSecond); + } + + var progress = new Core.Entities.SyncState( + AccountId: accountId, + Status: status, + TotalFiles: totalFiles, + CompletedFiles: completedFiles, + TotalBytes: totalBytes, + CompletedBytes: completedBytes, + FilesDownloading: filesDownloading, + FilesUploading: filesUploading, + FilesDeleted: filesDeleted, + ConflictsDetected: conflictsDetected, + MegabytesPerSecond: megabytesPerSecond, + EstimatedSecondsRemaining: estimatedSecondsRemaining, + CurrentScanningFolder: currentScanningFolder, + LastUpdateUtc: now); + + _progressSubject.OnNext(progress); + } + + /// + /// Formats a folder path for display by removing Graph API prefixes. + /// + public static string? FormatScanningFolderForDisplay(string? folderPath) + { + if(string.IsNullOrEmpty(folderPath)) return folderPath; + + var cleaned = MyRegex().Replace(folderPath, string.Empty); + + if(cleaned.StartsWith("/drive/root:", StringComparison.OrdinalIgnoreCase)) cleaned = cleaned["/drive/root:".Length..]; + + if(!string.IsNullOrEmpty(cleaned) && !cleaned.StartsWith('/')) cleaned = "/" + cleaned; + + return $"OneDrive: {cleaned}"; + } + + public void Dispose() + { + _syncCancellation?.Dispose(); + _progressSubject.Dispose(); + } + + [System.Text.RegularExpressions.GeneratedRegex(@"^/drives/[^/]+/root:")] + private static partial System.Text.RegularExpressions.Regex MyRegex(); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/SyncSelectionService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/SyncSelectionService.cs new file mode 100644 index 0000000..fc3515f --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/SyncSelectionService.cs @@ -0,0 +1,362 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Core.Entities.Enums; +using AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; +using AStar.Dev.OneDrive.Client.Models; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for managing folder selection state in the sync tree. +/// +public sealed partial class SyncSelectionService : ISyncSelectionService +{ + private const string Delimiter = "/"; + private readonly ISyncConfigurationRepository? _configurationRepository; + + /// + /// Initializes a new instance of the class. + /// + public SyncSelectionService() + { + // Parameterless constructor for backward compatibility with existing tests + } + + /// + /// Initializes a new instance of the class with database persistence. + /// + /// The sync configuration repository. + public SyncSelectionService(ISyncConfigurationRepository configurationRepository) => _configurationRepository = configurationRepository; + /// + public void SetSelection(OneDriveFolderNode folder, bool isSelected) + { + ArgumentNullException.ThrowIfNull(folder); + + SelectionState selectionState = isSelected ? SelectionState.Checked : SelectionState.Unchecked; + folder.SelectionState = selectionState; + folder.IsSelected = isSelected; + + // Cascade to all children + CascadeSelectionToChildren(folder, selectionState); + } + + /// + public void UpdateParentStates(OneDriveFolderNode folder, List rootFolders) + { + ArgumentNullException.ThrowIfNull(folder); + ArgumentNullException.ThrowIfNull(rootFolders); + + if(folder.ParentId is null) return; + + OneDriveFolderNode? parent = FindNodeById(rootFolders, folder.ParentId); + if(parent is null) return; + + SelectionState calculatedState = CalculateStateFromChildren(parent); + parent.SelectionState = calculatedState; + parent.IsSelected = calculatedState switch + { + SelectionState.Checked => true, + SelectionState.Unchecked => false, + SelectionState.Indeterminate => null, + _ => null + }; + + // Continue propagating upward + UpdateParentStates(parent, rootFolders); + } + + /// + public List GetSelectedFolders(List rootFolders) + { + ArgumentNullException.ThrowIfNull(rootFolders); + + var selectedFolders = new List(); + CollectSelectedFolders(rootFolders, selectedFolders); + return selectedFolders; + } + + /// + public void ClearAllSelections(List rootFolders) + { + ArgumentNullException.ThrowIfNull(rootFolders); + + foreach(OneDriveFolderNode folder in rootFolders) SetSelection(folder, false); + } + + /// + public SelectionState CalculateStateFromChildren(OneDriveFolderNode folder) + { + ArgumentNullException.ThrowIfNull(folder); + + if(folder.Children.Count == 0) return folder.SelectionState; + + var checkedCount = 0; + var uncheckedCount = 0; + var indeterminateCount = 0; + + foreach(OneDriveFolderNode child in folder.Children) + { + switch(child.SelectionState) + { + case SelectionState.Checked: + checkedCount++; + break; + case SelectionState.Unchecked: + uncheckedCount++; + break; + case SelectionState.Indeterminate: + indeterminateCount++; + break; + default: + break; + } + } + + // If any child is indeterminate, parent is indeterminate + if(indeterminateCount > 0) return SelectionState.Indeterminate; + + // If all children are checked, parent is checked + if(checkedCount == folder.Children.Count) return SelectionState.Checked; + + // If all children are unchecked, parent is unchecked + if(uncheckedCount == folder.Children.Count && folder.IsSelected == false) return SelectionState.Unchecked; + + // Mixed state = indeterminate + return SelectionState.Indeterminate; + } + + private static void CascadeSelectionToChildren(OneDriveFolderNode folder, SelectionState state) + { + foreach(OneDriveFolderNode child in folder.Children) + { + child.SelectionState = state; + child.IsSelected = state switch + { + SelectionState.Checked => true, + SelectionState.Unchecked => false, + SelectionState.Indeterminate => throw new NotImplementedException(), + _ => null + }; + + CascadeSelectionToChildren(child, state); + } + } + + private static void CollectSelectedFolders(List folders, List result) + { + foreach(OneDriveFolderNode folder in folders) + { + if(folder.SelectionState == SelectionState.Checked && + !string.IsNullOrEmpty(folder.Path) && + !string.IsNullOrEmpty(folder.Name)) + result.Add(folder); + + CollectSelectedFolders([.. folder.Children], result); + } + } + + private static OneDriveFolderNode? FindNodeById(List folders, string nodeId) + { + foreach(OneDriveFolderNode folder in folders) + { + if(folder.Id == nodeId) return folder; + + OneDriveFolderNode? foundInChildren = FindNodeById([.. folder.Children], nodeId); + if(foundInChildren is not null) return foundInChildren; + } + + return null; + } + + /// + public async Task SaveSelectionsToDatabaseAsync(string accountId, List rootFolders, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + ArgumentNullException.ThrowIfNull(rootFolders); + + if(_configurationRepository is null) return; // No persistence configured + + // Get all checked folders + List selectedFolders = GetSelectedFolders(rootFolders); + + // Convert to SyncConfiguration records + IEnumerable configurations = selectedFolders.Select(folder => new SyncConfiguration( + 0, // Will be auto-generated + accountId, + folder.Path, + true, + DateTime.UtcNow + )); + + // Save batch (replaces all existing selections for this account) + await _configurationRepository.SaveBatchAsync(accountId, configurations, cancellationToken); + } + + /// + public async Task LoadSelectionsFromDatabaseAsync(string accountId, List rootFolders, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(accountId); + ArgumentNullException.ThrowIfNull(rootFolders); + + // Set account context for debug logging + DebugLogContext.SetAccountId(accountId); + + if(_configurationRepository is null) return; // No persistence configured + + // Get saved folder paths + IReadOnlyList savedFolderPaths = await _configurationRepository.GetSelectedFoldersAsync(accountId, cancellationToken); + + await DebugLog.InfoAsync("SyncSelectionService.LoadSelectionsFromDatabaseAsync", $"Loading selections for account {accountId}", cancellationToken); + await DebugLog.InfoAsync("SyncSelectionService.LoadSelectionsFromDatabaseAsync", $"Found {savedFolderPaths.Count} saved paths in database", cancellationToken); + foreach(var path in savedFolderPaths) await DebugLog.InfoAsync("SyncSelectionService.LoadSelectionsFromDatabaseAsync", $"DB Path: {path}", cancellationToken); + + // Normalize paths by removing Graph API prefixes for comparison + var normalizedSavedPaths = savedFolderPaths + .Select(NormalizePathForComparison) + .ToList(); + + await DebugLog.InfoAsync("SyncSelectionService.LoadSelectionsFromDatabaseAsync", "Normalized paths:", cancellationToken); + foreach(var path in normalizedSavedPaths) await DebugLog.InfoAsync("SyncSelectionService.LoadSelectionsFromDatabaseAsync", $"Normalized: {path}", cancellationToken); + + // Build lookup dictionary for fast path-to-node resolution, using normalized paths + var pathToNodeMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + BuildPathLookup(rootFolders, pathToNodeMap); + + // Build a normalized path-to-node map for robust matching + var normalizedPathToNodeMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach(KeyValuePair kvp in pathToNodeMap) + { + var normalized = NormalizePathForComparison(kvp.Key); + if(!normalizedPathToNodeMap.ContainsKey(normalized)) normalizedPathToNodeMap[normalized] = kvp.Value; + } + + // Initialize ALL folders to Unchecked first + foreach(OneDriveFolderNode folder in pathToNodeMap.Values) + { + folder.SelectionState = SelectionState.Unchecked; + folder.IsSelected = false; + } + + // Then set saved selections to Checked using normalized matching + if(savedFolderPaths.Count > 0) + { + for(var i = 0; i < savedFolderPaths.Count; i++) + { + var normalizedPath = normalizedSavedPaths[i]; + if(normalizedPathToNodeMap.TryGetValue(normalizedPath, out OneDriveFolderNode? folder)) SetSelection(folder, true); + // Silently ignore folders that no longer exist (deleted or renamed) + } + + // For root folders that aren't loaded: check if they have selected descendants + // This handles the case where only a deep subfolder is selected + await DebugLog.InfoAsync("SyncSelectionService.LoadSelectionsFromDatabaseAsync", "Checking root folders for selected descendants", cancellationToken); + foreach(OneDriveFolderNode rootFolder in rootFolders) + { + await DebugLog.InfoAsync("SyncSelectionService.LoadSelectionsFromDatabaseAsync", $"Checking root: {rootFolder.Path} (State: {rootFolder.SelectionState})", cancellationToken); + + // Skip if root is already explicitly selected + if(rootFolder.SelectionState == SelectionState.Checked) + { + await DebugLog.InfoAsync("SyncSelectionService.LoadSelectionsFromDatabaseAsync", "Already checked, skipping", cancellationToken); + continue; + } + + // Normalize root path for comparison + var normalizedRootPath = NormalizePathForComparison(rootFolder.Path); + await DebugLog.InfoAsync("SyncSelectionService.LoadSelectionsFromDatabaseAsync", $"Root normalized to: {normalizedRootPath}", cancellationToken); + + // Check if any saved path is a descendant of this root + var hasSelectedDescendants = normalizedSavedPaths.Any(path => + path.StartsWith(normalizedRootPath + Delimiter, StringComparison.OrdinalIgnoreCase) || + (normalizedRootPath == Delimiter && path.StartsWith(Delimiter, StringComparison.OrdinalIgnoreCase) && path != Delimiter)); + + await DebugLog.InfoAsync("SyncSelectionService.LoadSelectionsFromDatabaseAsync", $"Has selected descendants: {hasSelectedDescendants}", cancellationToken); + + if(hasSelectedDescendants) + { + // Set root to indeterminate since it has selected descendants + await DebugLog.InfoAsync("SyncSelectionService.LoadSelectionsFromDatabaseAsync", $"Setting {rootFolder.Path} to Indeterminate", cancellationToken); + rootFolder.SelectionState = SelectionState.Indeterminate; + rootFolder.IsSelected = null; + await DebugLog.InfoAsync("SyncSelectionService.LoadSelectionsFromDatabaseAsync", $"After setting - State: {rootFolder.SelectionState}, IsSelected: {rootFolder.IsSelected}", cancellationToken); + } + } + } + + // NOTE: We don't call RecalculateParentStates here because: + // 1. Root folders don't have parents + // 2. Their children aren't loaded yet (just placeholder nodes) + // 3. We've already manually set indeterminate state for roots with selected descendants + } + + private static void BuildPathLookup(List folders, Dictionary pathMap) + { + foreach(OneDriveFolderNode folder in folders) + { + if(string.IsNullOrEmpty(folder.Path)) continue; + + pathMap[folder.Path] = folder; + + if(folder.Children.Count > 0) BuildPathLookup([.. folder.Children], pathMap); + } + } + + /// + public void UpdateParentState(OneDriveFolderNode folder) + { + ArgumentNullException.ThrowIfNull(folder); + + if(folder.Children.Count == 0) return; // No children, nothing to calculate + + SelectionState calculatedState = CalculateStateFromChildren(folder); + folder.SelectionState = calculatedState; + folder.IsSelected = calculatedState switch + { + SelectionState.Checked => true, + SelectionState.Unchecked => false, + SelectionState.Indeterminate => null, + _ => null + }; + } + + /// + /// Normalizes a path by removing Graph API prefixes for comparison. + /// + /// The path to normalize. + /// The normalized path without Graph API prefixes. + private static string NormalizePathForComparison(string path) + { + if(string.IsNullOrEmpty(path)) return path; + + // Remove /drives/{id}/root: prefix + var drivesPattern = @"^/drives/[^/]+/root:"; + if(System.Text.RegularExpressions.Regex.IsMatch(path, drivesPattern)) path = System.Text.RegularExpressions.Regex.Replace(path, drivesPattern, string.Empty); + + // Remove /drive/root: prefix + if(path.StartsWith("/drive/root:", StringComparison.OrdinalIgnoreCase)) path = path["/drive/root:".Length..]; + + // Ensure path starts with / + if(!path.StartsWith('/')) path = Delimiter + path; + + return path; + } + + private void RecalculateParentStates(OneDriveFolderNode folder) + { + if(folder.Children.Count > 0) + { + // Recursively update children first + foreach(OneDriveFolderNode child in folder.Children) RecalculateParentStates(child); + + // Then update this folder's state based on children + SelectionState calculatedState = CalculateStateFromChildren(folder); + folder.SelectionState = calculatedState; + folder.IsSelected = calculatedState switch + { + SelectionState.Checked => true, + SelectionState.Unchecked => false, + SelectionState.Indeterminate => null, + _ => null + }; + } + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/WindowPreferencesService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/WindowPreferencesService.cs new file mode 100644 index 0000000..cfbfe71 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/FromV3/WindowPreferencesService.cs @@ -0,0 +1,69 @@ +using AStar.Dev.OneDrive.Client.Core.Entities; +using AStar.Dev.OneDrive.Client.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace AStar.Dev.OneDrive.Client.FromV3; + +/// +/// Service for managing window position and size preferences using database storage. +/// +public sealed class WindowPreferencesService : IWindowPreferencesService +{ + private readonly AppDbContext _context; + + public WindowPreferencesService(AppDbContext context) + { + ArgumentNullException.ThrowIfNull(context); + _context = context; + } + + /// + public async Task LoadAsync(CancellationToken cancellationToken = default) + { + WindowPreferencesEntity? entity = await _context.WindowPreferences + .FirstOrDefaultAsync(cancellationToken); + + return entity is null ? null : MapToModel(entity); + } + + /// + public async Task SaveAsync(WindowPreferences preferences, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(preferences); + + WindowPreferencesEntity? entity = await _context.WindowPreferences.FirstOrDefaultAsync(cancellationToken); + + if(entity is null) + { + entity = new WindowPreferencesEntity + { + X = preferences.X, + Y = preferences.Y, + Width = preferences.Width, + Height = preferences.Height, + IsMaximized = preferences.IsMaximized + }; + _ = _context.WindowPreferences.Add(entity); + } + else + { + entity.X = preferences.X; + entity.Y = preferences.Y; + entity.Width = preferences.Width; + entity.Height = preferences.Height; + entity.IsMaximized = preferences.IsMaximized; + } + + _ = await _context.SaveChangesAsync(cancellationToken); + } + + private static WindowPreferences MapToModel(WindowPreferencesEntity entity) + => new( + entity.Id, + entity.X, + entity.Y, + entity.Width, + entity.Height, + entity.IsMaximized + ); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/HostExtensions.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/HostExtensions.cs new file mode 100644 index 0000000..b281cfb --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/HostExtensions.cs @@ -0,0 +1,130 @@ +using System.IO.Abstractions; +using AStar.Dev.Functional.Extensions; +using AStar.Dev.Logging.Extensions.Messages; +using AStar.Dev.OneDrive.Client.Common; +using AStar.Dev.OneDrive.Client.Core.ConfigurationSettings; +using AStar.Dev.OneDrive.Client.FromV3; +using AStar.Dev.OneDrive.Client.FromV3.Authentication; +using AStar.Dev.OneDrive.Client.FromV3.OneDriveServices; +using AStar.Dev.OneDrive.Client.Infrastructure.Data.Repositories; +using AStar.Dev.OneDrive.Client.Infrastructure.DependencyInjection; +using AStar.Dev.OneDrive.Client.Services.ConfigurationSettings; +using AStar.Dev.OneDrive.Client.Services.DependencyInjection; +using AStar.Dev.OneDrive.Client.SettingsAndPreferences; +using AStar.Dev.OneDrive.Client.SyncConflicts; +using AStar.Dev.OneDrive.Client.Theme; +using AStar.Dev.OneDrive.Client.ViewModels; +using AStar.Dev.Source.Generators.OptionsBindingGeneration; +using AStar.Dev.Utilities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Testably.Abstractions; + +namespace AStar.Dev.OneDrive.Client; + +internal static class HostExtensions +{ + internal static void ConfigureApplicationServices(HostBuilderContext context, IServiceCollection services) + { + _ = services.AddLogging(); + IConfiguration config = context.Configuration; + + _ = services.AddAutoRegisteredOptions(config); + + var connectionString = string.Empty; + var localRoot = string.Empty; + var msalClientId = string.Empty; + using(IServiceScope scope = services.BuildServiceProvider().CreateScope()) + { + ILogger log = scope.ServiceProvider.GetRequiredService>(); + ApplicationSettings appSettings = scope.ServiceProvider.GetRequiredService>().Value; + CreateDirectoriesAndUserPreferencesIfRequired(appSettings, log); + + _ = services.AddSyncServices(config); + EntraIdSettings entraId = scope.ServiceProvider.GetRequiredService>().Value; + + connectionString = $"Data Source={appSettings.FullDatabasePath}"; + localRoot = appSettings.FullUserSyncPath; + msalClientId = entraId.ClientId; + + var msalConfigurationSettings = new MsalConfigurationSettings( + msalClientId, + appSettings.RedirectUri, + appSettings.GraphUri, + context.Configuration.GetSection("EntraId:Scopes").Get() ?? [], + appSettings.CachePrefix); + + _ = services.AddSingleton(msalConfigurationSettings); + _ = services.AddInfrastructure(connectionString, localRoot, msalConfigurationSettings); + } + + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + + var authConfig = AuthConfiguration.LoadFromConfiguration(config); + + // Authentication - registered as singleton with factory + _ = services.AddSingleton(provider => + // AuthService.CreateAsync must be called synchronously during startup + // This is acceptable as it's a one-time initialization cost + AuthService.CreateAsync(authConfig).GetAwaiter().GetResult()); + + // Repositories + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + // Services + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddSingleton(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + _ = services.AddScoped(); + + // ViewModels + _ = services.AddTransient(); + _ = services.AddTransient(); + _ = services.AddTransient(); + _ = services.AddTransient(); + _ = services.AddTransient(); + + ServiceProvider servicesProvider = services.BuildServiceProvider(); + Action initializer = servicesProvider.GetRequiredService>(); + initializer(servicesProvider); + } + + private static void CreateDirectoriesAndUserPreferencesIfRequired(ApplicationSettings appSettings, ILogger log) + => _ = Try.Run(() => + { + _ = Directory.CreateDirectory(appSettings.FullUserSyncPath); + _ = Directory.CreateDirectory(ApplicationSettings.FullDatabaseDirectory); + _ = Directory.CreateDirectory(ApplicationSettings.FullUserPreferencesDirectory); + if(!File.Exists(appSettings.FullUserPreferencesPath)) File.WriteAllText(appSettings.FullUserPreferencesPath, new UserPreferences().ToJson()); + }) + .TapError(ex => AStarLog.Application.ApplicationFailedToStart(log, ApplicationMetadata.ApplicationName, ex.Message)); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Models/OneDriveFolderNode.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Models/OneDriveFolderNode.cs new file mode 100644 index 0000000..e88c0af --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Models/OneDriveFolderNode.cs @@ -0,0 +1,127 @@ +using System.Collections.ObjectModel; +using AStar.Dev.OneDrive.Client.Core.Entities.Enums; +using ReactiveUI; + +namespace AStar.Dev.OneDrive.Client.Models; + +/// +/// Represents a folder or file node in the OneDrive folder hierarchy. +/// +/// +/// This model is used for building the folder tree UI and managing sync selection. +/// The Children collection supports lazy loading for efficient tree rendering. +/// +public sealed class OneDriveFolderNode : ReactiveObject +{ + /// + /// Gets or sets the unique identifier for this item (OneDrive DriveItem ID). + /// + public string Id { get; set; } = string.Empty; + + /// + /// Gets or sets the display name of the folder or file. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the full path from the OneDrive root. + /// + /// + /// Example: "/Documents/Work/Projects" + /// + public string Path { get; set; } = string.Empty; + + /// + /// Gets or sets the parent node's ID, or null if this is a root-level item. + /// + public string? ParentId { get; set; } + + /// + /// Gets or sets a value indicating whether this node represents a folder (true) or file (false). + /// + public bool IsFolder { get; set; } + + /// + /// Gets the collection of child nodes. + /// + /// + /// This collection supports lazy loading - children are populated when the node is expanded in the UI. + /// + public ObservableCollection Children { get; } = []; + + /// + /// Gets or sets a value indicating whether child nodes have been loaded from the API. + /// + /// + /// Used to prevent redundant API calls when expanding/collapsing tree nodes. + /// + public bool ChildrenLoaded { get; set; } + + /// + /// Gets or sets a value indicating whether this node is expanded in the tree view. + /// + public bool IsExpanded + { + get; + set => this.RaiseAndSetIfChanged(ref field, value); + } + + /// + /// Gets or sets a value indicating whether this node is currently loading its children. + /// + public bool IsLoading + { + get; + set => this.RaiseAndSetIfChanged(ref field, value); + } + + /// + /// Gets or sets the selection state for sync operations. + /// + /// + /// Use Checked/Unchecked for explicit user selection. + /// Indeterminate is calculated when some (but not all) children are selected. + /// + public SelectionState SelectionState + { + get; + set => this.RaiseAndSetIfChanged(ref field, value); + } + + /// + /// Gets or sets a nullable boolean representing the selection state. + /// + /// + /// true = Checked, false = Unchecked, null = Indeterminate. + /// This property provides checkbox-friendly binding for tri-state selection. + /// + public bool? IsSelected + { + get; + set => this.RaiseAndSetIfChanged(ref field, value); + } + + /// + /// Initializes a new instance of the class. + /// + public OneDriveFolderNode() + { + } + + /// + /// Initializes a new instance of the class with specified properties. + /// + /// The unique identifier. + /// The display name. + /// The full path. + /// The parent node ID. + /// Whether this is a folder. + public OneDriveFolderNode(string id, string name, string path, string? parentId, bool isFolder) + { + Id = id; + Name = name; + Path = path; + ParentId = parentId; + IsFolder = isFolder; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/OneDrive Notes and Issues.txt b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/OneDrive Notes and Issues.txt new file mode 100644 index 0000000..1427ed2 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/OneDrive Notes and Issues.txt @@ -0,0 +1,2 @@ +1st sync leaces the "Start Sync" button on the overlay as active with the "Start Sync" text when it should change to "Pause Sync" +"Scanning for changes..." is static whilst scanning the content of the folder. If a large folder, the scan could take a considerable amount of time and make the user think nothing is happening / app has hung. Can we add the spinner used elsewhere or is there a better option? \ No newline at end of file diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/OperationConstants.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/OperationConstants.cs new file mode 100644 index 0000000..d0abff3 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/OperationConstants.cs @@ -0,0 +1,8 @@ +namespace AStar.Dev.OneDrive.Client; + +internal static class OperationConstants +{ + public const string InitialFileSync = "Initial file sync"; + public const string IncrementalFileSync = "Incremental file sync"; + public const string LocalFileSync = "Local file sync"; +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Program.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Program.cs new file mode 100644 index 0000000..116c183 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Program.cs @@ -0,0 +1,72 @@ +using System.Diagnostics.CodeAnalysis; +using AStar.Dev.OneDrive.Client.Data; +using Avalonia; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using ReactiveUI.Avalonia; +using Serilog; +using static AStar.Dev.Logging.Extensions.Messages.AStarLog.Application; +using static AStar.Dev.Logging.Extensions.Serilog.SerilogExtensions; + +namespace AStar.Dev.OneDrive.Client; + +[ExcludeFromCodeCoverage] +internal class Program +{ + protected Program() { } + + private static ILogger LocalLogger = null!; + + [STAThread] + public static void Main(string[] args) + { + ApplicationName applicationName = SetApplicationName(); + Log.Logger = CreateMinimalLogger(); + + try + { + using IHost host = CreateHostBuilder(args).Build(); + host.Start(); + App.Services = host.Services; + LocalLogger = host.Services.GetRequiredService>(); + ApplicationStarted(LocalLogger, applicationName); + + _ = BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } + catch(Exception ex) + { + ApplicationFailedToStart(LocalLogger, applicationName, ex.GetBaseException().Message); + } + finally + { + ApplicationStopped(LocalLogger, applicationName); + Log.CloseAndFlush(); + } + } + + private static ApplicationName SetApplicationName() => new(ApplicationMetadata.ApplicationName); + + private static IHostBuilder CreateHostBuilder(string[] args) + => Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((ctx, cfg) => + { + _ = cfg.SetBasePath(AppContext.BaseDirectory); + _ = cfg.AddJsonFile("appsettings.json", false, false); + _ = cfg.AddUserSecrets(true); + }) + .ConfigureServices(HostExtensions.ConfigureApplicationServices) + .UseSerilog((context, services, configuration) => + configuration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + ); + + private static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace() + .UseReactiveUI(); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/ISettingsAndPreferencesService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/ISettingsAndPreferencesService.cs new file mode 100644 index 0000000..89a1d9a --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/ISettingsAndPreferencesService.cs @@ -0,0 +1,24 @@ +using AStar.Dev.OneDrive.Client.Services.ConfigurationSettings; + +namespace AStar.Dev.OneDrive.Client.SettingsAndPreferences; + +/// +/// Provides methods for loading and saving user settings and preferences. +/// +/// Implementations of this interface manage the persistence of user-specific configuration data, such as +/// application preferences or settings. Methods may throw exceptions if the underlying storage is unavailable or if the +/// data is invalid. Thread safety and persistence location may vary by implementation. +public interface ISettingsAndPreferencesService +{ + /// + /// Loads the user preferences from the file system. + /// + /// A object representing the loaded preferences. + UserPreferences Load(); + + /// + /// Saves the user preferences to the file system. + /// + /// The object to be saved. + void Save(UserPreferences userPreferences); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/SettingsAndPreferencesService.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/SettingsAndPreferencesService.cs new file mode 100644 index 0000000..7354760 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/SettingsAndPreferencesService.cs @@ -0,0 +1,16 @@ +using System.IO.Abstractions; +using AStar.Dev.OneDrive.Client.Services.ConfigurationSettings; +using AStar.Dev.Utilities; + +namespace AStar.Dev.OneDrive.Client.SettingsAndPreferences; + +/// +public class SettingsAndPreferencesService(IFileSystem fileSystem, ApplicationSettings appSettings) : ISettingsAndPreferencesService +{ + /// + public UserPreferences Load() + => fileSystem.File.ReadAllText(appSettings.FullUserPreferencesPath).FromJson(); + + /// + public void Save(UserPreferences userPreferences) => fileSystem.File.WriteAllText(appSettings.FullUserPreferencesPath, userPreferences.ToJson()); +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/UiSettings.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/UiSettings.cs new file mode 100644 index 0000000..54893d1 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/UiSettings.cs @@ -0,0 +1,103 @@ +// namespace AStar.Dev.OneDrive.Client.SettingsAndPreferences; + +// /// +// /// Represents the UI settings configured by the user. +// /// It includes options for managing application behavior, appearance, and state persistence. +// /// +// public class UiSettings +// { +// /// +// /// Gets or sets a value indicating whether files should be automatically downloaded +// /// after the synchronization process completes. +// /// +// /// +// /// When set to true, the application will attempt to download files immediately +// /// following a successful sync operation. This setting is primarily intended to enhance the +// /// user experience by ensuring that the most recent versions of files are readily available locally. +// /// The behavior of this property can be configured through user preferences. +// /// +// public bool DownloadFilesAfterSync { get; set; } + +// /// +// /// Gets or sets a value indicating whether files should be automatically uploaded +// /// after the synchronization process completes. +// /// +// /// +// /// When set to true, the application will attempt to upload files immediately +// /// following a successful sync operation. This setting is designed to ensure that local changes +// /// are promptly reflected in the cloud storage, maintaining up-to-date versions of files. +// /// Users can configure this behavior according to their preferences for seamless file updates. +// /// +// public bool UploadFilesAfterSync { get; set; } + +// /// +// /// Gets or sets a value indicating whether the application should retain the user's authentication state across sessions. +// /// +// /// +// /// When set to true, the user will remain signed in even after restarting the application, +// /// provided that the authentication token remains valid. This property is often used to enhance +// /// user convenience by avoiding repeated sign-in prompts. If false, the user will be signed out +// /// at the end of the session. +// /// +// public bool RememberMe { get; set; } = true; + +// /// +// /// Gets or sets the UI theme preference selected by the user. +// /// +// /// +// /// This property determines the visual appearance of the application's user interface. +// /// It supports values such as "Light", "Dark", and "Auto", where "Auto" allows the theme +// /// to dynamically adapt based on the system's theme settings. +// /// The selected theme is applied throughout the application and can be modified via user preferences. +// /// +// public string Theme { get; set; } = "Auto"; + +// /// +// /// Gets or sets the description of the most recent action performed by the user or system. +// /// +// /// +// /// This property is intended to store and reflect the last significant action taken within +// /// the application, such as a synchronization, data upload, or UI interaction. It provides +// /// context for the user or system regarding recent events and can be used to update status +// /// displays within the UI. +// /// Defaults to "No action yet" when no actions have been recorded. +// /// +// public string LastAction { get; set; } = "No action yet"; + +// /// +// /// Gets or sets the maximum number of parallel download operations that can be +// /// performed concurrently. This property is used to control the level of +// /// concurrency when retrieving files from OneDrive, helping to manage system +// /// resource usage effectively. +// /// +// public int MaxParallelDownloads { get; set; } = 8; + +// /// +// /// Gets or sets the maximum number of items to be retrieved or processed in a single batch during +// /// download operations. This value is used to optimize data retrieval by controlling the batch size +// /// for network requests or processing chunks. Adjusting this property can balance performance and resource usage. +// /// +// public int DownloadBatchSize { get; set; } = 100; + +// /// +// /// Updates the current instance of with values from another +// /// instance. +// /// +// /// +// /// The instance of whose values +// /// will be copied to the current instance. +// /// +// /// Returns the updated instance of . +// public UiSettings Update(UiSettings other) +// { +// MaxParallelDownloads = other.MaxParallelDownloads; +// DownloadBatchSize = other.DownloadBatchSize; +// LastAction = other.LastAction; +// Theme = other.Theme; +// RememberMe = other.RememberMe; +// DownloadFilesAfterSync = other.DownloadFilesAfterSync; +// UploadFilesAfterSync = other.UploadFilesAfterSync; + +// return this; +// } +// } diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/WindowSettings.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/WindowSettings.cs new file mode 100644 index 0000000..e6b1fdb --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SettingsAndPreferences/WindowSettings.cs @@ -0,0 +1,60 @@ +// using Avalonia; + +// namespace AStar.Dev.OneDrive.Client.SettingsAndPreferences; + +// /// +// /// Represents settings related to the configuration and positioning of a window. +// /// +// public class WindowSettings +// { +// /// +// /// Gets or sets the width of the window in pixels. +// /// This property defines or persists the horizontal size of the application window. +// /// +// public double WindowWidth { get; set; } = 1000; + +// /// +// /// Gets or sets the height of the window in pixels. +// /// This property helps to define or persist the vertical size of the application window. +// /// +// public double WindowHeight { get; set; } = 800; + +// /// +// /// Represents the horizontal position of the window relative to the screen. +// /// +// public int WindowX { get; set; } = 100; + +// /// +// /// Gets or sets the Y-coordinate of the window position. +// /// +// public int WindowY { get; set; } = 100; + +// /// +// /// Updates the current instance of with values from another +// /// instance. +// /// +// /// +// /// The instance of whose values +// /// will be copied to the current instance. +// /// +// /// Returns the updated instance of . +// public WindowSettings Update(WindowSettings other) +// { +// WindowHeight = other.WindowHeight; +// WindowWidth = other.WindowWidth; +// WindowX = other.WindowX; +// WindowY = other.WindowY; + +// return this; +// } + +// public WindowSettings Update(PixelPoint windowPosition, double windowWidth, double windowHeight) +// { +// WindowHeight = windowHeight; +// WindowWidth = windowWidth; +// WindowX = windowPosition.X; +// WindowY = windowPosition.Y; + +// return this; +// } +// } diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Styles/Theme.axaml b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Styles/Theme.axaml new file mode 100644 index 0000000..be1ffd6 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/Styles/Theme.axaml @@ -0,0 +1,19 @@ + + + + #FFFFFFFF + #FF000000 + + + + + + + + + diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SyncConflicts/ConflictItemViewModel.cs b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SyncConflicts/ConflictItemViewModel.cs new file mode 100644 index 0000000..eb55770 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SyncConflicts/ConflictItemViewModel.cs @@ -0,0 +1,114 @@ +using AStar.Dev.OneDrive.Client.Core.Models.Enums; +using ReactiveUI; + +namespace AStar.Dev.OneDrive.Client.SyncConflicts; + +/// +/// Represents a single sync conflict item in the conflict resolution UI. +/// +/// +/// This ViewModel wraps a and provides UI-friendly properties +/// for displaying conflict details and capturing user's resolution choice. +/// +public sealed class ConflictItemViewModel : ReactiveObject +{ + + /// + /// Gets the conflict ID. + /// + public string Id { get; } + + /// + /// Gets the account ID this conflict belongs to. + /// + public string AccountId { get; } + + /// + /// Gets the file path relative to the sync root. + /// + public string FilePath { get; } + + /// + /// Gets the local file's last modified timestamp (UTC). + /// + public DateTime LocalModifiedUtc { get; } + + /// + /// Gets the remote file's last modified timestamp (UTC). + /// + public DateTime RemoteModifiedUtc { get; } + + /// + /// Gets the local file size in bytes. + /// + public long LocalSize { get; } + + /// + /// Gets the remote file size in bytes. + /// + public long RemoteSize { get; } + + /// + /// Gets the timestamp when this conflict was detected (UTC). + /// + public DateTime DetectedUtc { get; } + + /// + /// Gets or sets the user's chosen resolution strategy for this conflict. + /// + public ConflictResolutionStrategy SelectedStrategy + { + get; + set => this.RaiseAndSetIfChanged(ref field, value); + } = ConflictResolutionStrategy.None; + + /// + /// Gets a UI-friendly display string for local file details. + /// + public string LocalDetailsDisplay + => $"{LocalModifiedUtc:yyyy-MM-dd HH:mm:ss} UTC • {FormatFileSize(LocalSize)}"; + + /// + /// Gets a UI-friendly display string for remote file details. + /// + public string RemoteDetailsDisplay + => $"{RemoteModifiedUtc:yyyy-MM-dd HH:mm:ss} UTC • {FormatFileSize(RemoteSize)}"; + + /// + /// Initializes a new instance of from a . + /// + /// The conflict to wrap. + /// Thrown if is null. + public ConflictItemViewModel(SyncConflict conflict) + { + ArgumentNullException.ThrowIfNull(conflict); + + Id = conflict.Id; + AccountId = conflict.AccountId; + FilePath = conflict.FilePath; + LocalModifiedUtc = conflict.LocalModifiedUtc; + RemoteModifiedUtc = conflict.RemoteModifiedUtc; + LocalSize = conflict.LocalSize; + RemoteSize = conflict.RemoteSize; + DetectedUtc = conflict.DetectedUtc; + SelectedStrategy = conflict.ResolutionStrategy; + } + + /// + /// Formats a file size in bytes to a human-readable string. + /// + private static string FormatFileSize(long bytes) + { + string[] suffixes = ["B", "KB", "MB", "GB", "TB"]; + var order = 0; + var size = (double)bytes; + + while(size >= 1024 && order < suffixes.Length - 1) + { + order++; + size /= 1024; + } + + return $"{size:0.##} {suffixes[order]}"; + } +} diff --git a/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SyncConflicts/ConflictResolutionView.axaml b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SyncConflicts/ConflictResolutionView.axaml new file mode 100644 index 0000000..a590f07 --- /dev/null +++ b/apps/astar-dev-onedrive-client/AStar.Dev.OneDrive.Client/SyncConflicts/ConflictResolutionView.axaml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +