Switched to dedicated sync service
This commit is contained in:
@@ -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"]!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
10
Models/ConfigData.cs
Normal file
10
Models/ConfigData.cs
Normal file
@@ -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"]!;
|
||||||
|
}
|
||||||
7
Models/OneDriveItem.cs
Normal file
7
Models/OneDriveItem.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
27
OneDrive/OneDriveClient.cs
Normal file
27
OneDrive/OneDriveClient.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,9 +9,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Azure.Identity" Version="1.17.1" />
|
|
||||||
<PackageReference Include="Cronos" Version="0.11.1" />
|
<PackageReference Include="Cronos" Version="0.11.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0"/>
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0"/>
|
||||||
<PackageReference Include="Microsoft.Graph" Version="5.100.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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<UploadResult<DriveItem>> 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<string, object> {
|
|
||||||
{ "@microsoft.graph.conflictBehavior", "replace" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, cancellationToken: token);
|
|
||||||
|
|
||||||
await using var stream = File.OpenRead(filePath);
|
|
||||||
var uploader = new LargeFileUploadTask<DriveItem>(uploadSession, stream);
|
|
||||||
var result = await uploader.UploadAsync(cancellationToken: token);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using OneDriveBackupService;
|
using OneDriveBackupService;
|
||||||
|
using OneDriveBackupService.Models;
|
||||||
|
using OneDriveBackupService.OneDrive;
|
||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
|
|||||||
20
README.md
20
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)
|
- Automatische Backups nach Cron-Schedule (`appsettings.json` / ENV)
|
||||||
- Upload zu OneDrive über Microsoft Graph SDK
|
- Upload zu OneDrive über Microsoft Graph SDK
|
||||||
- Behalten nur der letzten N Backups (`KeepLast`)
|
|
||||||
- Unterstützung für manuelles Backup via `docker exec`
|
- 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:
|
Um ein Backup manuell auf einem laufenden Container auszuführen:
|
||||||
|
|
||||||
```bash
|
```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
|
## Umgebungsvariablen
|
||||||
@@ -47,16 +40,5 @@ docker exec -it onedrive-backup-server dotnet OneDriveBackupService.dll --run-on
|
|||||||
| `LocalRoot` | Lokaler Datenpfad für Backups |
|
| `LocalRoot` | Lokaler Datenpfad für Backups |
|
||||||
| `IncludeFile` | Textdatei mit allen unterordnern, die mit ins Backup sollen |
|
| `IncludeFile` | Textdatei mit allen unterordnern, die mit ins Backup sollen |
|
||||||
| `KeepLast` | Anzahl zu behaltender Backups |
|
| `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) |
|
| `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
|
|
||||||
|
|||||||
28
Worker.cs
28
Worker.cs
@@ -1,15 +1,16 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Cronos;
|
using Cronos;
|
||||||
|
using OneDriveBackupService.Models;
|
||||||
|
using OneDriveBackupService.OneDrive;
|
||||||
|
|
||||||
namespace OneDriveBackupService;
|
namespace OneDriveBackupService;
|
||||||
|
|
||||||
public class Worker(ILogger<Worker> logger, ConfigData config, OneDriveClient client) : BackgroundService {
|
public class Worker(ILogger<Worker> logger, ConfigData config, OneDriveClient client) : BackgroundService {
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
|
||||||
await client.EnsureAuthenticated(stoppingToken);
|
|
||||||
|
|
||||||
if (Environment.GetCommandLineArgs().Contains("--run-once")) {
|
if (Environment.GetCommandLineArgs().Contains("--run-once")) {
|
||||||
logger.LogInformation("Manual backup triggered");
|
logger.LogInformation("Manual backup triggered");
|
||||||
await RunBackup(DateTime.Now, stoppingToken);
|
await RunBackup(DateTime.Now, stoppingToken);
|
||||||
|
Environment.Exit(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,12 +26,12 @@ public class Worker(ILogger<Worker> logger, ConfigData config, OneDriveClient cl
|
|||||||
delay = TimeSpan.Zero;
|
delay = TimeSpan.Zero;
|
||||||
|
|
||||||
if (!cronTime.HasValue) {
|
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);
|
delay = TimeSpan.FromHours(12);
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextRun = DateTime.Now + delay;
|
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 Task.Delay(delay, stoppingToken);
|
||||||
|
|
||||||
await RunBackup(nextRun, stoppingToken);
|
await RunBackup(nextRun, stoppingToken);
|
||||||
@@ -45,20 +46,10 @@ public class Worker(ILogger<Worker> logger, ConfigData config, OneDriveClient cl
|
|||||||
logger.LogError("Backup archive creation failed");
|
logger.LogError("Backup archive creation failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogInformation("Backup archive created, starting upload...");
|
|
||||||
|
|
||||||
var uploadResult = await client.UploadFile(file, stoppingToken);
|
client.UploadFile(file);
|
||||||
if (!uploadResult.UploadSucceeded) {
|
|
||||||
logger.LogError("Upload failed");
|
|
||||||
File.Delete(file);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation("Upload completed");
|
|
||||||
|
|
||||||
File.Delete(file);
|
var count = client.DeleteOldFiles();
|
||||||
var count = await client.DeleteOldFiles(stoppingToken);
|
|
||||||
logger.LogInformation("Deleted {count} old backups", count);
|
logger.LogInformation("Deleted {count} old backups", count);
|
||||||
|
|
||||||
logger.LogInformation("Backup completed");
|
logger.LogInformation("Backup completed");
|
||||||
@@ -77,7 +68,7 @@ public class Worker(ILogger<Worker> logger, ConfigData config, OneDriveClient cl
|
|||||||
"-C",
|
"-C",
|
||||||
config.LocalRoot,
|
config.LocalRoot,
|
||||||
"-T",
|
"-T",
|
||||||
Path.GetRelativePath(config.LocalRoot, config.IncludeFile)
|
Path.Combine(config.LocalRoot, config.IncludeFile)
|
||||||
},
|
},
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardError = true
|
RedirectStandardError = true
|
||||||
@@ -88,6 +79,9 @@ public class Worker(ILogger<Worker> logger, ConfigData config, OneDriveClient cl
|
|||||||
await process.WaitForExitAsync(stoppingToken);
|
await process.WaitForExitAsync(stoppingToken);
|
||||||
|
|
||||||
if (process.ExitCode != 0) {
|
if (process.ExitCode != 0) {
|
||||||
|
var error = await process.StandardError.ReadToEndAsync(stoppingToken);
|
||||||
|
logger.LogError(error);
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user