diff --git a/ConfigData.cs b/ConfigData.cs deleted file mode 100644 index c7df2f1..0000000 --- a/ConfigData.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace OneDriveBackupService; - -public sealed class ConfigData { - public string Schedule { get; } - public string BackupUploadRoot { get; } - public string LocalRoot { get; } - public string IncludeFile { get; } - public int KeepLast { get; } - - public ConfigData(IConfiguration config) { - Schedule = config["Schedule"]!; - BackupUploadRoot = config["UploadRoot"]!; - LocalRoot = config["LocalRoot"]!; - IncludeFile = config["IncludeFile"]!; - KeepLast = int.Parse(config["KeepLast"]!); - } -} \ No newline at end of file diff --git a/Models/ConfigData.cs b/Models/ConfigData.cs new file mode 100644 index 0000000..82b9987 --- /dev/null +++ b/Models/ConfigData.cs @@ -0,0 +1,10 @@ +namespace OneDriveBackupService.Models; + +public sealed class ConfigData(IConfiguration config) { + public string Schedule { get; } = config["Schedule"]!; + public string BackupUploadRoot { get; } = config["UploadRoot"]!; + public string LocalRoot { get; } = config["LocalRoot"]!; + public string IncludeFile { get; } = config["IncludeFile"]!; + public int KeepLast { get; } = int.Parse(config["KeepLast"]!); + public string OneDriveApiUrl { get; } = config["OneDriveApiUrl"]!; +} \ No newline at end of file diff --git a/Models/OneDriveItem.cs b/Models/OneDriveItem.cs new file mode 100644 index 0000000..0ffc9a1 --- /dev/null +++ b/Models/OneDriveItem.cs @@ -0,0 +1,7 @@ +namespace OneDriveBackupService.Models; + +public sealed class OneDriveItem { + public string Id { get; set; } = null!; + public string Name { get; set; } = null!; + public DateTime Created { get; set; } +} diff --git a/OneDrive/OneDriveClient.cs b/OneDrive/OneDriveClient.cs new file mode 100644 index 0000000..b11edeb --- /dev/null +++ b/OneDrive/OneDriveClient.cs @@ -0,0 +1,27 @@ +using OneDriveBackupService.Models; + +namespace OneDriveBackupService.OneDrive; + +public sealed class OneDriveClient(ConfigData config) { + public void UploadFile(string filePath) { + Directory.CreateDirectory(config.BackupUploadRoot); + var destFileName = Path.Combine(config.BackupUploadRoot, Path.GetFileName(filePath)); + File.Move(filePath, destFileName); + } + + public int DeleteOldFiles() { + var directory = new DirectoryInfo(config.BackupUploadRoot); + + var filesToDelete = directory.EnumerateFiles() + .Where(f => f.Name.StartsWith("backup_") && f.Name.EndsWith(".tar.gz")) + .OrderByDescending(f => f.CreationTimeUtc) + .Skip(config.KeepLast) + .ToArray(); + + foreach (var file in filesToDelete) { + file.Delete(); + } + + return filesToDelete.Length; + } +} \ No newline at end of file diff --git a/OneDriveBackupService.csproj b/OneDriveBackupService.csproj index b696821..9f91c7a 100644 --- a/OneDriveBackupService.csproj +++ b/OneDriveBackupService.csproj @@ -9,9 +9,7 @@ - - diff --git a/OneDriveClient.cs b/OneDriveClient.cs deleted file mode 100644 index 79e8186..0000000 --- a/OneDriveClient.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Azure.Identity; -using Microsoft.Graph; -using Microsoft.Graph.Drives.Item.Items.Item.CreateUploadSession; -using Microsoft.Graph.Models; - -namespace OneDriveBackupService; - -public class OneDriveClient { - - private readonly ConfigData _config; - private readonly GraphServiceClient _client; - - public OneDriveClient(ConfigData config) { - _config = config; - - var options = new DeviceCodeCredentialOptions { - TenantId = "consumers", - DeviceCodeCallback = (code, _) => { - Console.WriteLine(code.Message); - return Task.CompletedTask; - } - }; - - _client = new GraphServiceClient(new DeviceCodeCredential(options), ["Files.ReadWrite.All"]); - } - - public async Task EnsureAuthenticated(CancellationToken token) { - await _client.Me.Drive.GetAsync(cancellationToken: token); - } - - public async Task> UploadFile(string filePath, CancellationToken token) { - var fileName = Path.GetFileName(filePath); - var remoteFilePath = _config.BackupUploadRoot.Trim('/') + '/' + fileName; - - var defaultDrive = await _client.Me.Drive.GetAsync(cancellationToken: token); - - var driveFile = _client.Drives[defaultDrive!.Id].Items[$"root:/{remoteFilePath}:"]!; - var uploadSession = await driveFile.CreateUploadSession.PostAsync(new CreateUploadSessionPostRequestBody { - Item = new DriveItemUploadableProperties { - AdditionalData = new Dictionary { - { "@microsoft.graph.conflictBehavior", "replace" } - } - } - }, cancellationToken: token); - - await using var stream = File.OpenRead(filePath); - var uploader = new LargeFileUploadTask(uploadSession, stream); - var result = await uploader.UploadAsync(cancellationToken: token); - - return result; - } - - public async Task DeleteOldFiles(CancellationToken token) { - var defaultDrive = await _client.Me.Drive.GetAsync(cancellationToken: token); - - var remoteFolder = _config.BackupUploadRoot.Trim('/'); - var backupFiles = await _client.Drives[defaultDrive!.Id] - .Items[$"root:/{remoteFolder}:"] - .Children.GetAsync(cancellationToken: token); - - if (backupFiles == null || backupFiles.Value == null || backupFiles.Value.Count == 0) - return 0; - - var sortedFiles = backupFiles.Value - .Where(i => - i.File != null && - i.Name != null && - i.Name.StartsWith("backup_") && - i.Name.EndsWith(".tar.gz")) - .OrderByDescending(i => i.CreatedDateTime) - .ToList(); - - if (sortedFiles.Count < _config.KeepLast) - return 0; - - int deletedCount = 0; - foreach (var backupFile in sortedFiles.Skip(_config.KeepLast)) { - if (backupFile.Id == null) continue; - - await _client.Drives[defaultDrive.Id].Items[backupFile.Id].DeleteAsync(cancellationToken: token); - deletedCount++; - } - - return deletedCount; - } - -} \ No newline at end of file diff --git a/Program.cs b/Program.cs index b565dd5..ac04c1c 100644 --- a/Program.cs +++ b/Program.cs @@ -1,4 +1,6 @@ using OneDriveBackupService; +using OneDriveBackupService.Models; +using OneDriveBackupService.OneDrive; var builder = Host.CreateApplicationBuilder(args); diff --git a/README.md b/README.md index 55e2901..6f2aba9 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,7 @@ Unterstützt Cron-basierte Backups sowie manuelles Triggern über `docker exec`. - Automatische Backups nach Cron-Schedule (`appsettings.json` / ENV) - Upload zu OneDrive über Microsoft Graph SDK -- Behalten nur der letzten N Backups (`KeepLast`) - Unterstützung für manuelles Backup via `docker exec` -- Lokale Zeitzone für Logs und Backup-Zeitstempel -- Flexible Konfiguration über ENV oder `appsettings.json` --- @@ -29,13 +26,9 @@ Unterstützt Cron-basierte Backups sowie manuelles Triggern über `docker exec`. Um ein Backup manuell auf einem laufenden Container auszuführen: ```bash -docker exec -it onedrive-backup-server dotnet OneDriveBackupService.dll --run-once +docker exec -it backup-worker dotnet OneDriveBackupService.dll --run-once ``` -* Das löst **ein sofortiges Backup** aus -* Cron-Loop des Hauptcontainers bleibt ungestört -* Backup-Dateien werden wie üblich nach OneDrive hochgeladen - --- ## Umgebungsvariablen @@ -47,16 +40,5 @@ docker exec -it onedrive-backup-server dotnet OneDriveBackupService.dll --run-on | `LocalRoot` | Lokaler Datenpfad für Backups | | `IncludeFile` | Textdatei mit allen unterordnern, die mit ins Backup sollen | | `KeepLast` | Anzahl zu behaltender Backups | -| `TenantId` | Azure TenantId | -| `ClientId` | Azure ClientId | -| `ClientSecret` | Azure Client Secret | -| `UserId` | OneDrive User Id | | `TZ` | Zeitzone für Logs / DateTime.Now (optional) | ---- - -## Hinweise - -* Die Backup-Dateien erhalten eindeutige Namen mit Timestamp: `backup_YYYYMMDD_HHMMSS.tar.gz` -* Alte Backups werden automatisch gelöscht, basierend auf `KeepLast` -* `docker exec --run-once` löst ein manuelles Backup aus, ohne den Cron-Loop zu stoppen diff --git a/Worker.cs b/Worker.cs index 2e311d6..b890297 100644 --- a/Worker.cs +++ b/Worker.cs @@ -1,15 +1,16 @@ using System.Diagnostics; using Cronos; +using OneDriveBackupService.Models; +using OneDriveBackupService.OneDrive; namespace OneDriveBackupService; public class Worker(ILogger logger, ConfigData config, OneDriveClient client) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - await client.EnsureAuthenticated(stoppingToken); - if (Environment.GetCommandLineArgs().Contains("--run-once")) { logger.LogInformation("Manual backup triggered"); await RunBackup(DateTime.Now, stoppingToken); + Environment.Exit(0); return; } @@ -25,12 +26,12 @@ public class Worker(ILogger logger, ConfigData config, OneDriveClient cl delay = TimeSpan.Zero; if (!cronTime.HasValue) { - logger.LogError("Cron expression falied, falling back to default delay"); + logger.LogError("Cron expression failed, falling back to default delay"); delay = TimeSpan.FromHours(12); } var nextRun = DateTime.Now + delay; - logger.LogInformation("Next backup run: {time}", nextRun.ToString("f")); + logger.LogInformation("Next backup: {time}", nextRun.ToString("f")); await Task.Delay(delay, stoppingToken); await RunBackup(nextRun, stoppingToken); @@ -45,20 +46,10 @@ public class Worker(ILogger logger, ConfigData config, OneDriveClient cl logger.LogError("Backup archive creation failed"); return; } - - logger.LogInformation("Backup archive created, starting upload..."); - var uploadResult = await client.UploadFile(file, stoppingToken); - if (!uploadResult.UploadSucceeded) { - logger.LogError("Upload failed"); - File.Delete(file); - return; - } - - logger.LogInformation("Upload completed"); + client.UploadFile(file); - File.Delete(file); - var count = await client.DeleteOldFiles(stoppingToken); + var count = client.DeleteOldFiles(); logger.LogInformation("Deleted {count} old backups", count); logger.LogInformation("Backup completed"); @@ -77,7 +68,7 @@ public class Worker(ILogger logger, ConfigData config, OneDriveClient cl "-C", config.LocalRoot, "-T", - Path.GetRelativePath(config.LocalRoot, config.IncludeFile) + Path.Combine(config.LocalRoot, config.IncludeFile) }, RedirectStandardOutput = true, RedirectStandardError = true @@ -88,6 +79,9 @@ public class Worker(ILogger logger, ConfigData config, OneDriveClient cl await process.WaitForExitAsync(stoppingToken); if (process.ExitCode != 0) { + var error = await process.StandardError.ReadToEndAsync(stoppingToken); + logger.LogError(error); + return string.Empty; } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..011885b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +services: + backup: + image: registry.leon-hoppe.de/leon.hoppe/onedrivebackupservice:latest + container_name: backup-worker + user: root + environment: + TZ: Europe/Berlin + Schedule: 0 3 * * 1 + UploadRoot: /data/Server/Backups + LocalRoot: /backups + IncludeFile: include.txt + KeepLast: 2 + volumes: + - /home/leon:/backups:ro + - ./data:/data:rw + - ./cache:/tmp:rw + + sync: + image: driveone/onedrive:edge + container_name: backup-sync + volumes: + - ./config:/onedrive/conf:rw + - ./data:/onedrive/data:rw + environment: + - ONEDRIVE_UID=1000 + - ONEDRIVE_GID=1000 + - ONEDRIVE_UPLOADONLY=1 + - ONEDRIVE_NOREMOTEDELETE=1 + - ONEDRIVE_AUTHFILES=/onedrive/conf/auth-url:/onedrive/conf/auth-response \ No newline at end of file