using hassio_onedrive_backup.Contracts; using hassio_onedrive_backup.Graph; using hassio_onedrive_backup.Hass; using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Graph; using onedrive_backup.Graph; using System.Collections; using System.Diagnostics; using System.Security.Cryptography; namespace hassio_onedrive_backup.Sync { internal class SyncManager { private const string HashAlgo = "SHA256"; private const string OneDriveFileSyncRootDir = "FileSync"; private AddonOptions _addonOptions; private IGraphHelper _graphHelper; private IHassioClient _hassIoClient; private BitArray _allowedHours; private HassOnedriveFileSyncEntityState _hassEntityState; private IWebHostEnvironment? _environment; private TransferSpeedHelper _transferSpeedHelper; private Matcher _fileMatcher; public SyncManager(IServiceProvider serviceProvider, BitArray allowedHours, TransferSpeedHelper? transferSpeedHelper) { _addonOptions = serviceProvider.GetService(); _graphHelper = serviceProvider.GetService(); _hassIoClient = serviceProvider.GetService(); _hassEntityState = serviceProvider.GetService(); _environment = serviceProvider.GetService(); _transferSpeedHelper = transferSpeedHelper; _allowedHours = allowedHours; _fileMatcher = new(); _fileMatcher.AddIncludePatterns(_addonOptions.SyncPaths); } public async void SyncLoop(CancellationToken ct) { while (true && ct.IsCancellationRequested == false) { try { var now = DateTimeHelper.Instance!.Now; // Check if we are in the allowed hours if (_allowedHours[now.Hour] == false) { continue; } _hassEntityState.State = HassOnedriveFileSyncEntityState.FileState.Syncing; await _hassEntityState.UpdateBackupEntityInHass(); var matchingFiles = _fileMatcher.GetResultsInFullPath("/"); foreach (var matchingFile in matchingFiles) { try { await SyncFile(matchingFile); } catch (Exception ex) { ConsoleLogger.LogError($"Error syncing file {matchingFile}: {ex}"); } } if (_addonOptions.FileSyncRemoveDeleted) { await DeleteRemovedFilesFromOneDrive($"{OneDriveFileSyncRootDir}", "/"); } } catch (Exception ex) { ConsoleLogger.LogError($"Error Syncing: {ex}"); } finally { _hassEntityState.State = HassOnedriveFileSyncEntityState.FileState.Synced; await _hassEntityState.UpdateBackupEntityInHass(); await Task.Delay(TimeSpan.FromMinutes(5)); } } } private async Task SyncFile(string path) { var fileInfo = new FileInfo(path); if (fileInfo.Length == 0) { ConsoleLogger.LogWarning($"Skipping 0-byte file: {path}"); return; } var now = DateTimeHelper.Instance!.Now; string fileHash = CalculateFileHash(path); var localFileSyncData = new SyncFileData(path, fileHash, fileInfo.Length); string remotePath = $"/{OneDriveFileSyncRootDir}{path}".Replace("//", "/").Replace(@"\\", @"\"); if (_environment!.IsDevelopment()) { remotePath = remotePath.Replace(@"c:\", "/", StringComparison.OrdinalIgnoreCase); } DriveItem? remoteFile = await _graphHelper.GetItemInAppFolderAsync(remotePath); bool requiresUpload = remoteFile == null || remoteFile.Size != fileInfo.Length || remoteFile.File.Hashes.Sha256Hash.Equals(fileHash, StringComparison.OrdinalIgnoreCase) == false; if (requiresUpload == false) { return; } ConsoleLogger.LogInfo($"File {path} out of sync. Starting Upload"); var uploadSuccessful = await _graphHelper.UploadFileAsync( path, now, _addonOptions.InstanceName, _transferSpeedHelper, remotePath, async (prog, speed) => { _hassEntityState.UploadPercentage = prog; _hassEntityState.UploadSpeed = speed / 1024; await _hassEntityState.UpdateBackupEntityInHass(); Debug.WriteLine($"Progress: {prog}"); }, flatten: false ); } private string CalculateFileHash(string path) { using (var hasher = HashAlgorithm.Create(HashAlgo)) { using (var fileStream = System.IO.File.OpenRead(path)) { byte[] hash = hasher!.ComputeHash(fileStream); return BitConverter.ToString(hash).Replace("-", ""); } } } private async Task DeleteRemovedFilesFromOneDrive(string remotePath, string localPath) { var item = await _graphHelper.GetItemInAppFolderAsync(remotePath); if (item == null) { return; } ConsoleLogger.LogVerbose($"Evaulating {remotePath}"); localPath = Path.Combine(localPath, item.Name); if (localPath.StartsWith($"/{OneDriveFileSyncRootDir}")) { localPath = "/"; } if (item.Folder != null) { if (System.IO.Directory.Exists(localPath) == false) { ConsoleLogger.LogInfo($"{localPath} does not exist locally. Deleting from OneDrive"); await _graphHelper.DeleteItemFromAppFolderAsync(remotePath); } else { // Recursive call for files in Folder var folderItems = await _graphHelper.GetItemsInAppFolderAsync(remotePath); foreach (var folderItem in folderItems) { await DeleteRemovedFilesFromOneDrive(Path.Combine(remotePath, folderItem.Name), localPath); } // If no content left in folder then remove it folderItems = await _graphHelper.GetItemsInAppFolderAsync(remotePath); if (folderItems.Count == 0) { await _graphHelper.DeleteItemFromAppFolderAsync(remotePath); } } } else if (item.File != null) { if (System.IO.File.Exists(localPath) == false) { ConsoleLogger.LogInfo($"{localPath} does not exist locally. Deleting from OneDrive"); await _graphHelper.DeleteItemFromAppFolderAsync(remotePath); } else if (_fileMatcher.Match(localPath).HasMatches == false) { ConsoleLogger.LogInfo($"{localPath} not included in Sync Paths. Deleting from OneDrive"); await _graphHelper.DeleteItemFromAppFolderAsync(remotePath); } else { ConsoleLogger.LogVerbose($"{remotePath} in sync with {localPath}"); } } } } }