diff --git a/BackupJobOptions.cs b/BackupJobOptions.cs index c41eb43..2c83a52 100644 --- a/BackupJobOptions.cs +++ b/BackupJobOptions.cs @@ -1,5 +1,12 @@ namespace BackupService; +public enum BackupMode +{ + Full, + ArchivesOnly, + RemoteZip +} + public class BackupJobOptions { public string Name { get; set; } = "Backup"; @@ -16,5 +23,11 @@ public class BackupJobOptions public int? HistoryCopies { get; set; } public int RetentionDays { get; set; } = 7; public int OperationTimeoutMinutes { get; set; } = 10; - public int CompletionTimeoutMinutes { get; set; } = 180; -} + public int CompletionTimeoutMinutes { get; set; } = 180; + public BackupMode Mode { get; set; } = BackupMode.Full; + public string? RemoteTriggerUrl { get; set; } + + public int RemoteTriggerPollIntervalSeconds { get; set; } = 5; + + public int RemoteTriggerTimeoutSeconds { get; set; } = 600; +} \ No newline at end of file diff --git a/FtpBackupRunner.cs b/FtpBackupRunner.cs index 303598d..ea7b363 100644 --- a/FtpBackupRunner.cs +++ b/FtpBackupRunner.cs @@ -2,7 +2,9 @@ using System.Globalization; using System.IO.Compression; using System.Net; - +using System.Net.Http; +using System.Text.Json; +using System.Linq; namespace BackupService; @@ -25,7 +27,6 @@ public async Task RunJobAsync( try { - if (string.IsNullOrWhiteSpace(job.Host)) { logger.LogError("Backup '{name}' is missing Host.", job.Name); @@ -47,12 +48,12 @@ public async Task RunJobAsync( return; } - var tempDir = Path.Combine(job.LocalPath, "temp", job.Host); - Directory.CreateDirectory(tempDir); - - var archiveDir = Path.Combine(job.LocalPath, job.Host); + var archiveDir = job.LocalPath; Directory.CreateDirectory(archiveDir); - + + var remotePath = string.IsNullOrWhiteSpace(job.RemotePath) + ? "/" + : job.RemotePath; using var client = new FtpClient(job.Host, job.Username, job.Password, job.Port); @@ -81,10 +82,6 @@ public async Task RunJobAsync( }; } - var remotePath = string.IsNullOrWhiteSpace(job.RemotePath) - ? "/" - : job.RemotePath; - logger.LogInformation( "Connecting to {host}:{port} for backup '{name}'.", job.Host, @@ -93,40 +90,330 @@ public async Task RunJobAsync( client.Connect(); logger.LogInformation( - "Connected to {host}. Starting mirror of {remotePath}.", - job.Host, - remotePath); + "Connected to {host}.", + job.Host); + + if (job.Mode == BackupMode.ArchivesOnly) + { + logger.LogInformation( + "ArchivesOnly mode: downloading only .zip files from {remotePath} for '{name}'.", + remotePath, + job.Name); + + var listings = client.GetListing(remotePath); + var zipItems = listings + .Where(i => i.Type == FtpObjectType.File && + i.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (!zipItems.Any()) + { + logger.LogInformation( + "No .zip files found in {remotePath} for backup '{name}'.", + remotePath, + job.Name); + } + + int downloaded = 0; + int failed = 0; + + foreach (var item in zipItems) + { + completionToken.ThrowIfCancellationRequested(); - var results = client.DownloadDirectory( - tempDir, - remotePath, - FtpFolderSyncMode.Mirror, - FtpLocalExists.Overwrite, - FtpVerify.None); + var localFile = Path.Combine(archiveDir, item.Name); - LogResults(job.Name, results); + logger.LogInformation( + "Downloading archive {remoteName} to {localFile} for '{name}'.", + item.Name, + localFile, + job.Name); - string zipPath = Path.Combine(archiveDir, DateTime.Now.ToString("yyyy-MM-dd") + ".zip"); + var status = client.DownloadFile( + localFile, + item.FullName, + FtpLocalExists.Overwrite, + FtpVerify.None); - if (File.Exists(zipPath)) + if (status == FluentFTP.FtpStatus.Success) + { + downloaded++; + + if (item.Modified != DateTime.MinValue) + { + File.SetLastWriteTime(localFile, item.Modified); + File.SetCreationTime(localFile, item.Modified); + } + + logger.LogInformation("Successfully downloaded {name}.", item.Name); + } + else + { + failed++; + logger.LogWarning("Failed to download {name}.", item.Name); + } + } + + logger.LogInformation( + "ArchivesOnly backup completed for '{name}'. Downloaded {downloaded} archive(s), {failed} failed.", + job.Name, + downloaded, + failed); + } + else if (job.Mode == BackupMode.RemoteZip) { - File.Delete(zipPath); + logger.LogInformation( + "RemoteZip mode: attempting remote zip trigger or downloading .zip from {remotePath} for '{name}'.", + remotePath, + job.Name); + + var downloadedAny = false; + + if (!string.IsNullOrWhiteSpace(job.RemoteTriggerUrl)) + { + try + { + using var http = new HttpClient(); + http.Timeout = TimeSpan.FromSeconds(Math.Max(30, Math.Min(job.RemoteTriggerTimeoutSeconds, 3600))); + + logger.LogInformation("Triggering remote zip at {url} for '{name}'.", job.RemoteTriggerUrl, job.Name); + using var resp = await http.PostAsync(job.RemoteTriggerUrl, null, completionToken); + + string? location = resp.Headers.Location?.ToString(); + + if (string.IsNullOrWhiteSpace(location)) + { + try + { + var body = await resp.Content.ReadAsStringAsync(completionToken); + if (!string.IsNullOrWhiteSpace(body)) + { + try + { + using var doc = JsonDocument.Parse(body); + if (doc.RootElement.TryGetProperty("location", out var locEl)) + { + location = locEl.GetString(); + } + } + catch + { + var trimmed = body.Trim(); + if (Uri.IsWellFormedUriString(trimmed, UriKind.Absolute)) + { + location = trimmed; + } + } + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to read trigger response body."); + } + } + + if (!string.IsNullOrWhiteSpace(location)) + { + logger.LogInformation("Polling {location} for archive availability.", location); + var sw = System.Diagnostics.Stopwatch.StartNew(); + while (sw.Elapsed.TotalSeconds < job.RemoteTriggerTimeoutSeconds) + { + completionToken.ThrowIfCancellationRequested(); + + try + { + using var headReq = new HttpRequestMessage(HttpMethod.Head, location); + using var headResp = await http.SendAsync(headReq, completionToken); + if (headResp.IsSuccessStatusCode) + { + var uri = new Uri(location); + var fileName = Path.GetFileName(uri.LocalPath); + if (string.IsNullOrWhiteSpace(fileName)) + { + fileName = DateTime.Now.ToString("yyyy-MM-dd") + ".zip"; + } + + var localFile = Path.Combine(archiveDir, fileName); + logger.LogInformation("Downloading remote archive {loc} to {localFile} for '{name}'.", location, localFile, job.Name); + using var getResp = await http.GetAsync(location, HttpCompletionOption.ResponseHeadersRead, completionToken); + getResp.EnsureSuccessStatusCode(); + await using var stream = await getResp.Content.ReadAsStreamAsync(completionToken); + await using var fs = File.Create(localFile); + await stream.CopyToAsync(fs, cancellationToken: completionToken); + + downloadedAny = true; + logger.LogInformation("Successfully downloaded remote archive {name}.", fileName); + break; + } + } + catch (OperationCanceledException) { throw; } + catch (Exception) { /* ignore and retry */ } + + await Task.Delay(TimeSpan.FromSeconds(job.RemoteTriggerPollIntervalSeconds), completionToken); + } + } + } + catch (OperationCanceledException) { throw; } + catch (Exception ex) + { + logger.LogWarning(ex, "Remote trigger failed for '{name}'.", job.Name); + } + } + + if (!downloadedAny) + { + logger.LogInformation("RemoteZip: falling back to checking .zip files on FTP."); + + var listings = client.GetListing(remotePath); + var zipItems = listings + .Where(i => i.Type == FtpObjectType.File && + i.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (!zipItems.Any()) + { + logger.LogInformation("No .zip files found in {remotePath} for backup '{name}'. Falling back to Full.", remotePath, job.Name); + } + else + { + int downloaded = 0; + int failed = 0; + + foreach (var item in zipItems) + { + completionToken.ThrowIfCancellationRequested(); + + var localFile = Path.Combine(archiveDir, item.Name); + + logger.LogInformation( + "Downloading archive {remoteName} to {localFile} for '{name}'.", + item.Name, + localFile, + job.Name); + + var status = client.DownloadFile( + localFile, + item.FullName, + FtpLocalExists.Overwrite, + FtpVerify.None); + + if (status == FluentFTP.FtpStatus.Success) + { + downloaded++; + + if (item.Modified != DateTime.MinValue) + { + File.SetLastWriteTime(localFile, item.Modified); + File.SetCreationTime(localFile, item.Modified); + } + + logger.LogInformation("Successfully downloaded {name}.", item.Name); + downloadedAny = true; + } + else + { + failed++; + logger.LogWarning("Failed to download {name}.", item.Name); + } + } + + logger.LogInformation( + "RemoteZip (FTP) completed for '{name}'. Downloaded {downloaded} archive(s), {failed} failed.", + job.Name, + downloaded, + failed); + } + } + + if (!downloadedAny) + { + logger.LogInformation("RemoteZip: falling back to Full mode for '{name}'.", job.Name); + + var tempDir = Path.Combine(job.LocalPath, "temp", job.Host); + Directory.CreateDirectory(tempDir); + + logger.LogInformation( + "Full mode: mirroring {remotePath} to temp directory.", + remotePath); + + var results = client.DownloadDirectory( + tempDir, + remotePath, + FtpFolderSyncMode.Mirror, + FtpLocalExists.Overwrite, + FtpVerify.None); + + LogResults(job.Name, results); + + string zipPath = Path.Combine(archiveDir, DateTime.Now.ToString("yyyy-MM-dd") + ".zip"); + + if (File.Exists(zipPath)) + { + File.Delete(zipPath); + } + + ZipFile.CreateFromDirectory(tempDir, zipPath, CompressionLevel.Optimal, includeBaseDirectory: false); + + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, recursive: true); + } + } } + else + { + var tempDir = Path.Combine(job.LocalPath, "temp", job.Host); + Directory.CreateDirectory(tempDir); + + logger.LogInformation( + "Full mode: mirroring {remotePath} to temp directory.", + remotePath); + + var results = client.DownloadDirectory( + tempDir, + remotePath, + FtpFolderSyncMode.Mirror, + FtpLocalExists.Overwrite, + FtpVerify.None); + + LogResults(job.Name, results); - ZipFile.CreateFromDirectory(tempDir, zipPath, CompressionLevel.Optimal, includeBaseDirectory: false); + string zipPath = Path.Combine(archiveDir, DateTime.Now.ToString("yyyy-MM-dd") + ".zip"); + + if (File.Exists(zipPath)) + { + File.Delete(zipPath); + } + + ZipFile.CreateFromDirectory(tempDir, zipPath, CompressionLevel.Optimal, includeBaseDirectory: false); + + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, recursive: true); + } + } foreach (var file in Directory.GetFiles(archiveDir, "*.zip")) { var creationDate = File.GetCreationTime(file); if ((DateTime.Now - creationDate).TotalDays > job.RetentionDays) - File.Delete(file); - } - - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, recursive: true); + { + try + { + File.Delete(file); + logger.LogInformation( + "Deleted old archive {file} (older than {days} days) for '{name}'.", + Path.GetFileName(file), + job.RetentionDays, + job.Name); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to delete old archive {file}.", file); + } + } } - } + } catch (OperationCanceledException) { logger.LogWarning("Backup '{name}' cancelled.", job.Name); @@ -137,8 +424,7 @@ public async Task RunJobAsync( } } - -private static bool IsLocalDrivePath(string path) + private static bool IsLocalDrivePath(string path) { if (!Path.IsPathRooted(path)) { @@ -216,9 +502,7 @@ public static async Task CopyDirectory( cancellationToken.ThrowIfCancellationRequested(); var directoryName = Path.GetFileName(directory); var targetSubdir = Path.Combine(targetDir, directoryName); - //CopyDirectory(directory, targetSubdir, cancellationToken); await CopyDirectory(directory, targetSubdir, cancellationToken); - } } @@ -263,4 +547,4 @@ private void CleanupHistory( } } } -} +} \ No newline at end of file diff --git a/RemoteTriggerEndpoint/Program.cs b/RemoteTriggerEndpoint/Program.cs new file mode 100644 index 0000000..36387e0 --- /dev/null +++ b/RemoteTriggerEndpoint/Program.cs @@ -0,0 +1,48 @@ +using System.IO.Compression; +using System.Threading.Channels; +using Microsoft.Extensions.FileProviders; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSingleton(Channel.CreateUnbounded()); +builder.Services.AddHostedService(); + +builder.Services.AddLogging(); +builder.Services.AddCors(); + +var app = builder.Build(); + +var archivesDir = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "archives"); +Directory.CreateDirectory(archivesDir); + +app.UseStaticFiles(new StaticFileOptions +{ + FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")), + RequestPath = "" +}); + +app.MapPost("/trigger", async (TriggerDto dto, Channel channel) => +{ + if (dto == null || string.IsNullOrWhiteSpace(dto.SourcePath)) + return Results.BadRequest(new { error = "sourcePath is required" }); + + if (!Directory.Exists(dto.SourcePath)) + return Results.BadRequest(new { error = "sourcePath does not exist on server" }); + + var fileName = string.IsNullOrWhiteSpace(dto.ArchiveName) + ? $"{Path.GetFileName(dto.SourcePath).Replace(Path.DirectorySeparatorChar, '_')}-{DateTime.UtcNow:yyyyMMddHHmmss}.zip" + : dto.ArchiveName!; + + var outputPath = Path.Combine(archivesDir, fileName); + + var req = new ZipRequest(dto.SourcePath, outputPath, dto.Overwrite ?? false); + await channel.Writer.WriteAsync(req); + + return Results.Accepted($"/archives/{Uri.EscapeDataString(fileName)}"); +}); + +app.Run(); + +public record TriggerDto(string SourcePath, string? ArchiveName, bool? Overwrite); + +public record ZipRequest(string SourcePath, string OutputPath, bool Overwrite); diff --git a/RemoteTriggerEndpoint/RemoteTriggerEndpoint.csproj b/RemoteTriggerEndpoint/RemoteTriggerEndpoint.csproj new file mode 100644 index 0000000..54acff4 --- /dev/null +++ b/RemoteTriggerEndpoint/RemoteTriggerEndpoint.csproj @@ -0,0 +1,7 @@ + + + net8.0 + enable + enable + + diff --git a/RemoteTriggerEndpoint/ZipWorker.cs b/RemoteTriggerEndpoint/ZipWorker.cs new file mode 100644 index 0000000..a9182ed --- /dev/null +++ b/RemoteTriggerEndpoint/ZipWorker.cs @@ -0,0 +1,45 @@ +using System.IO.Compression; +using System.Threading.Channels; + +public class ZipWorker : BackgroundService +{ + private readonly Channel _channel; + private readonly ILogger _logger; + + public ZipWorker(Channel channel, ILogger logger) + { + _channel = channel; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await foreach (var req in _channel.Reader.ReadAllAsync(stoppingToken)) + { + try + { + _logger.LogInformation("ZipWorker: creating archive {file}", req.OutputPath); + + if (File.Exists(req.OutputPath) && !req.Overwrite) + { + _logger.LogInformation("ZipWorker: {file} already exists and overwrite=false, skipping.", req.OutputPath); + continue; + } + + var tmp = req.OutputPath + ".tmp"; + if (File.Exists(tmp)) File.Delete(tmp); + + ZipFile.CreateFromDirectory(req.SourcePath, tmp, CompressionLevel.Optimal, includeBaseDirectory: false); + + if (File.Exists(req.OutputPath)) File.Delete(req.OutputPath); + File.Move(tmp, req.OutputPath); + + _logger.LogInformation("ZipWorker: created archive {file}", req.OutputPath); + } + catch (Exception ex) + { + _logger.LogError(ex, "ZipWorker: failed to create archive {file}", req.OutputPath); + } + } + } +} diff --git a/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.deps.json b/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.deps.json new file mode 100644 index 0000000..e0771b1 --- /dev/null +++ b/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.deps.json @@ -0,0 +1,23 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v8.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v8.0": { + "RemoteTriggerEndpoint/1.0.0": { + "runtime": { + "RemoteTriggerEndpoint.dll": {} + } + } + } + }, + "libraries": { + "RemoteTriggerEndpoint/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.dll b/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.dll new file mode 100644 index 0000000..6de1a7b Binary files /dev/null and b/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.dll differ diff --git a/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.exe b/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.exe new file mode 100644 index 0000000..5864004 Binary files /dev/null and b/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.exe differ diff --git a/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.pdb b/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.pdb new file mode 100644 index 0000000..8165388 Binary files /dev/null and b/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.pdb differ diff --git a/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.runtimeconfig.json b/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.runtimeconfig.json new file mode 100644 index 0000000..8a91515 --- /dev/null +++ b/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.runtimeconfig.json @@ -0,0 +1,19 @@ +{ + "runtimeOptions": { + "tfm": "net8.0", + "frameworks": [ + { + "name": "Microsoft.NETCore.App", + "version": "8.0.0" + }, + { + "name": "Microsoft.AspNetCore.App", + "version": "8.0.0" + } + ], + "configProperties": { + "System.GC.Server": true, + "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false + } + } +} \ No newline at end of file diff --git a/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.staticwebassets.endpoints.json b/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.staticwebassets.endpoints.json new file mode 100644 index 0000000..5576e88 --- /dev/null +++ b/RemoteTriggerEndpoint/bin/Debug/net8.0/RemoteTriggerEndpoint.staticwebassets.endpoints.json @@ -0,0 +1 @@ +{"Version":1,"ManifestType":"Build","Endpoints":[]} \ No newline at end of file diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.AssemblyInfo.cs b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.AssemblyInfo.cs new file mode 100644 index 0000000..fb97f73 --- /dev/null +++ b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("RemoteTriggerEndpoint")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("RemoteTriggerEndpoint")] +[assembly: System.Reflection.AssemblyTitleAttribute("RemoteTriggerEndpoint")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// Wygenerowane przez klasę WriteCodeFragment programu MSBuild. + diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.AssemblyInfoInputs.cache b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.AssemblyInfoInputs.cache new file mode 100644 index 0000000..a2cb9ca --- /dev/null +++ b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +c272ed4b597ee6e8cc88eef93ac227774648223d16434dc024e68a45b8771445 diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.GeneratedMSBuildEditorConfig.editorconfig b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 0000000..c62c4bf --- /dev/null +++ b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,23 @@ +is_global = true +build_property.TargetFramework = net8.0 +build_property.TargetFrameworkIdentifier = .NETCoreApp +build_property.TargetFrameworkVersion = v8.0 +build_property.TargetPlatformMinVersion = +build_property.UsingMicrosoftNETSdkWeb = true +build_property.ProjectTypeGuids = +build_property.InvariantGlobalization = +build_property.PlatformNeutralAssembly = +build_property.EnforceExtendedAnalyzerRules = +build_property._SupportedPlatformList = Linux,macOS,Windows +build_property.RootNamespace = RemoteTriggerEndpoint +build_property.RootNamespace = RemoteTriggerEndpoint +build_property.ProjectDir = C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\ +build_property.EnableComHosting = +build_property.EnableGeneratedComInterfaceComImportInterop = +build_property.RazorLangVersion = 8.0 +build_property.SupportLocalizedComponentNames = +build_property.GenerateRazorMetadataSourceChecksumAttributes = +build_property.MSBuildProjectDirectory = C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint +build_property._RazorSourceGeneratorDebug = +build_property.EffectiveAnalysisLevelStyle = 8.0 +build_property.EnableCodeStyleSeverity = diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.GlobalUsings.g.cs b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.GlobalUsings.g.cs new file mode 100644 index 0000000..c65f0a7 --- /dev/null +++ b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.GlobalUsings.g.cs @@ -0,0 +1,17 @@ +// +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Routing; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Net.Http; +global using System.Net.Http.Json; +global using System.Threading; +global using System.Threading.Tasks; diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.MvcApplicationPartsAssemblyInfo.cache b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.MvcApplicationPartsAssemblyInfo.cache new file mode 100644 index 0000000..e69de29 diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.assets.cache b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.assets.cache new file mode 100644 index 0000000..6ed94f3 Binary files /dev/null and b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.assets.cache differ diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.csproj.CoreCompileInputs.cache b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..ade1c7c --- /dev/null +++ b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +683e58331f2c1cf8d1da9d9ff3c2fc0502cb8a2cfbe359476087724399d7f780 diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.csproj.FileListAbsolute.txt b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..aa92ecc --- /dev/null +++ b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.csproj.FileListAbsolute.txt @@ -0,0 +1,26 @@ +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\bin\Debug\net8.0\RemoteTriggerEndpoint.staticwebassets.endpoints.json +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\bin\Debug\net8.0\RemoteTriggerEndpoint.exe +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\bin\Debug\net8.0\RemoteTriggerEndpoint.deps.json +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\bin\Debug\net8.0\RemoteTriggerEndpoint.runtimeconfig.json +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\bin\Debug\net8.0\RemoteTriggerEndpoint.dll +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\bin\Debug\net8.0\RemoteTriggerEndpoint.pdb +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\rpswa.dswa.cache.json +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\RemoteTriggerEndpoint.GeneratedMSBuildEditorConfig.editorconfig +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\RemoteTriggerEndpoint.AssemblyInfoInputs.cache +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\RemoteTriggerEndpoint.AssemblyInfo.cs +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\RemoteTriggerEndpoint.csproj.CoreCompileInputs.cache +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\RemoteTriggerEndpoint.MvcApplicationPartsAssemblyInfo.cache +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\rjimswa.dswa.cache.json +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\rjsmrazor.dswa.cache.json +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\rjsmcshtml.dswa.cache.json +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\scopedcss\bundle\RemoteTriggerEndpoint.styles.css +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\staticwebassets.build.json +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\staticwebassets.build.json.cache +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\staticwebassets.development.json +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\staticwebassets.build.endpoints.json +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\swae.build.ex.cache +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\RemoteTriggerEndpoint.dll +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\refint\RemoteTriggerEndpoint.dll +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\RemoteTriggerEndpoint.pdb +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\RemoteTriggerEndpoint.genruntimeconfig.cache +C:\Users\tvxma\Desktop\projekty\RemoteBackup-reposytoryArch\RemoteTriggerEndpoint\obj\Debug\net8.0\ref\RemoteTriggerEndpoint.dll diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.dll b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.dll new file mode 100644 index 0000000..6de1a7b Binary files /dev/null and b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.dll differ diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.genruntimeconfig.cache b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.genruntimeconfig.cache new file mode 100644 index 0000000..77a4073 --- /dev/null +++ b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.genruntimeconfig.cache @@ -0,0 +1 @@ +1651134bb6d3c843ccd52f285d20ffeee9357de5a9d99a2920dc2622d692307d diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.pdb b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.pdb new file mode 100644 index 0000000..8165388 Binary files /dev/null and b/RemoteTriggerEndpoint/obj/Debug/net8.0/RemoteTriggerEndpoint.pdb differ diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/apphost.exe b/RemoteTriggerEndpoint/obj/Debug/net8.0/apphost.exe new file mode 100644 index 0000000..5864004 Binary files /dev/null and b/RemoteTriggerEndpoint/obj/Debug/net8.0/apphost.exe differ diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/ref/RemoteTriggerEndpoint.dll b/RemoteTriggerEndpoint/obj/Debug/net8.0/ref/RemoteTriggerEndpoint.dll new file mode 100644 index 0000000..f35510b Binary files /dev/null and b/RemoteTriggerEndpoint/obj/Debug/net8.0/ref/RemoteTriggerEndpoint.dll differ diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/refint/RemoteTriggerEndpoint.dll b/RemoteTriggerEndpoint/obj/Debug/net8.0/refint/RemoteTriggerEndpoint.dll new file mode 100644 index 0000000..f35510b Binary files /dev/null and b/RemoteTriggerEndpoint/obj/Debug/net8.0/refint/RemoteTriggerEndpoint.dll differ diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/rjsmcshtml.dswa.cache.json b/RemoteTriggerEndpoint/obj/Debug/net8.0/rjsmcshtml.dswa.cache.json new file mode 100644 index 0000000..479a4e0 --- /dev/null +++ b/RemoteTriggerEndpoint/obj/Debug/net8.0/rjsmcshtml.dswa.cache.json @@ -0,0 +1 @@ +{"GlobalPropertiesHash":"6iAtOFnl21laiyUNpyBTAO6IYN1zDBOoyxacXNSEYt0=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["dodu8R7\u002BM/eyNAahsuLVEsNIgQz7HHZPSC2Zq2DTv/M="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/rjsmrazor.dswa.cache.json b/RemoteTriggerEndpoint/obj/Debug/net8.0/rjsmrazor.dswa.cache.json new file mode 100644 index 0000000..53a434b --- /dev/null +++ b/RemoteTriggerEndpoint/obj/Debug/net8.0/rjsmrazor.dswa.cache.json @@ -0,0 +1 @@ +{"GlobalPropertiesHash":"tz7A8kmttSN9+SL3/NuLsoDvPr4WMTbab6KAU6dljeo=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["dodu8R7\u002BM/eyNAahsuLVEsNIgQz7HHZPSC2Zq2DTv/M="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/staticwebassets.build.endpoints.json b/RemoteTriggerEndpoint/obj/Debug/net8.0/staticwebassets.build.endpoints.json new file mode 100644 index 0000000..5576e88 --- /dev/null +++ b/RemoteTriggerEndpoint/obj/Debug/net8.0/staticwebassets.build.endpoints.json @@ -0,0 +1 @@ +{"Version":1,"ManifestType":"Build","Endpoints":[]} \ No newline at end of file diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/staticwebassets.build.json b/RemoteTriggerEndpoint/obj/Debug/net8.0/staticwebassets.build.json new file mode 100644 index 0000000..efc39d3 --- /dev/null +++ b/RemoteTriggerEndpoint/obj/Debug/net8.0/staticwebassets.build.json @@ -0,0 +1 @@ +{"Version":1,"Hash":"IHnFtYJ/J9qzhSAqAkp0ew1KxHR3UkhEc5mdDZjqe2A=","Source":"RemoteTriggerEndpoint","BasePath":"/","Mode":"Root","ManifestType":"Build","ReferencedProjectsConfiguration":[],"DiscoveryPatterns":[],"Assets":[],"Endpoints":[]} \ No newline at end of file diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/staticwebassets.build.json.cache b/RemoteTriggerEndpoint/obj/Debug/net8.0/staticwebassets.build.json.cache new file mode 100644 index 0000000..8e6063a --- /dev/null +++ b/RemoteTriggerEndpoint/obj/Debug/net8.0/staticwebassets.build.json.cache @@ -0,0 +1 @@ +IHnFtYJ/J9qzhSAqAkp0ew1KxHR3UkhEc5mdDZjqe2A= \ No newline at end of file diff --git a/RemoteTriggerEndpoint/obj/Debug/net8.0/swae.build.ex.cache b/RemoteTriggerEndpoint/obj/Debug/net8.0/swae.build.ex.cache new file mode 100644 index 0000000..e69de29 diff --git a/RemoteTriggerEndpoint/obj/RemoteTriggerEndpoint.csproj.nuget.dgspec.json b/RemoteTriggerEndpoint/obj/RemoteTriggerEndpoint.csproj.nuget.dgspec.json new file mode 100644 index 0000000..ff19669 --- /dev/null +++ b/RemoteTriggerEndpoint/obj/RemoteTriggerEndpoint.csproj.nuget.dgspec.json @@ -0,0 +1,77 @@ +{ + "format": 1, + "restore": { + "C:\\Users\\tvxma\\Desktop\\projekty\\RemoteBackup-reposytoryArch\\RemoteTriggerEndpoint\\RemoteTriggerEndpoint.csproj": {} + }, + "projects": { + "C:\\Users\\tvxma\\Desktop\\projekty\\RemoteBackup-reposytoryArch\\RemoteTriggerEndpoint\\RemoteTriggerEndpoint.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "C:\\Users\\tvxma\\Desktop\\projekty\\RemoteBackup-reposytoryArch\\RemoteTriggerEndpoint\\RemoteTriggerEndpoint.csproj", + "projectName": "RemoteTriggerEndpoint", + "projectPath": "C:\\Users\\tvxma\\Desktop\\projekty\\RemoteBackup-reposytoryArch\\RemoteTriggerEndpoint\\RemoteTriggerEndpoint.csproj", + "packagesPath": "C:\\Users\\tvxma\\.nuget\\packages\\", + "outputPath": "C:\\Users\\tvxma\\Desktop\\projekty\\RemoteBackup-reposytoryArch\\RemoteTriggerEndpoint\\obj\\", + "projectStyle": "PackageReference", + "fallbackFolders": [ + "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages" + ], + "configFilePaths": [ + "C:\\Users\\tvxma\\AppData\\Roaming\\NuGet\\NuGet.Config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" + ], + "originalTargetFrameworks": [ + "net8.0" + ], + "sources": { + "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, + "C:\\Program Files\\dotnet\\library-packs": {}, + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + }, + "SdkAnalysisLevel": "10.0.100" + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.AspNetCore.App": { + "privateAssets": "none" + }, + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.103/PortableRuntimeIdentifierGraph.json" + } + } + } + } +} \ No newline at end of file diff --git a/RemoteTriggerEndpoint/obj/RemoteTriggerEndpoint.csproj.nuget.g.props b/RemoteTriggerEndpoint/obj/RemoteTriggerEndpoint.csproj.nuget.g.props new file mode 100644 index 0000000..b183162 --- /dev/null +++ b/RemoteTriggerEndpoint/obj/RemoteTriggerEndpoint.csproj.nuget.g.props @@ -0,0 +1,16 @@ + + + + True + NuGet + $(MSBuildThisFileDirectory)project.assets.json + $(UserProfile)\.nuget\packages\ + C:\Users\tvxma\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages + PackageReference + 7.0.0 + + + + + + \ No newline at end of file diff --git a/RemoteTriggerEndpoint/obj/RemoteTriggerEndpoint.csproj.nuget.g.targets b/RemoteTriggerEndpoint/obj/RemoteTriggerEndpoint.csproj.nuget.g.targets new file mode 100644 index 0000000..35a7576 --- /dev/null +++ b/RemoteTriggerEndpoint/obj/RemoteTriggerEndpoint.csproj.nuget.g.targets @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/RemoteTriggerEndpoint/obj/project.assets.json b/RemoteTriggerEndpoint/obj/project.assets.json new file mode 100644 index 0000000..1497f29 --- /dev/null +++ b/RemoteTriggerEndpoint/obj/project.assets.json @@ -0,0 +1,83 @@ +{ + "version": 3, + "targets": { + "net8.0": {} + }, + "libraries": {}, + "projectFileDependencyGroups": { + "net8.0": [] + }, + "packageFolders": { + "C:\\Users\\tvxma\\.nuget\\packages\\": {}, + "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages": {} + }, + "project": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "C:\\Users\\tvxma\\Desktop\\projekty\\RemoteBackup-reposytoryArch\\RemoteTriggerEndpoint\\RemoteTriggerEndpoint.csproj", + "projectName": "RemoteTriggerEndpoint", + "projectPath": "C:\\Users\\tvxma\\Desktop\\projekty\\RemoteBackup-reposytoryArch\\RemoteTriggerEndpoint\\RemoteTriggerEndpoint.csproj", + "packagesPath": "C:\\Users\\tvxma\\.nuget\\packages\\", + "outputPath": "C:\\Users\\tvxma\\Desktop\\projekty\\RemoteBackup-reposytoryArch\\RemoteTriggerEndpoint\\obj\\", + "projectStyle": "PackageReference", + "fallbackFolders": [ + "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages" + ], + "configFilePaths": [ + "C:\\Users\\tvxma\\AppData\\Roaming\\NuGet\\NuGet.Config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" + ], + "originalTargetFrameworks": [ + "net8.0" + ], + "sources": { + "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, + "C:\\Program Files\\dotnet\\library-packs": {}, + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + }, + "SdkAnalysisLevel": "10.0.100" + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.AspNetCore.App": { + "privateAssets": "none" + }, + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.103/PortableRuntimeIdentifierGraph.json" + } + } + } +} \ No newline at end of file diff --git a/RemoteTriggerEndpoint/obj/project.nuget.cache b/RemoteTriggerEndpoint/obj/project.nuget.cache new file mode 100644 index 0000000..6577819 --- /dev/null +++ b/RemoteTriggerEndpoint/obj/project.nuget.cache @@ -0,0 +1,8 @@ +{ + "version": 2, + "dgSpecHash": "S1VR74TAdOg=", + "success": true, + "projectFilePath": "C:\\Users\\tvxma\\Desktop\\projekty\\RemoteBackup-reposytoryArch\\RemoteTriggerEndpoint\\RemoteTriggerEndpoint.csproj", + "expectedPackageFiles": [], + "logs": [] +} \ No newline at end of file diff --git a/appsettings.Development.json b/appsettings.Development.json index 884c3c8..cfa2ae4 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -23,12 +23,31 @@ "Username": "user", "Password": "password", "RemotePath": "/", - "LocalPath": "D:\\Backups\\Sample", + "LocalPath": "D:\\Backups", "Encryption": "Explicit", "Passive": true, "AllowInvalidCertificate": false, "TimeoutMinutes": 60, "HistoryCopies": 5 + }, + { + "Name": "RemoteZipExample", + "Mode": "RemoteZip", + "Host": "ftp.example.com", + "Port": 21, + "Username": "user", + "Password": "password", + "RemotePath": "/backups", + "LocalPath": "D:\\Backups\\RemoteZipExample", + "RemoteTriggerUrl": "https://example.com/api/trigger-backup", + "RemoteTriggerPollIntervalSeconds": 5, + "RemoteTriggerTimeoutSeconds": 600, + "Encryption": "Explicit", + "Passive": true, + "AllowInvalidCertificate": false, + "TimeoutMinutes": 60, + "HistoryCopies": 5, + "RetentionDays": 14 } ] } diff --git a/appsettings.json b/appsettings.json index da7d60b..a4d81f9 100644 --- a/appsettings.json +++ b/appsettings.json @@ -20,7 +20,8 @@ "RetentionDays": 7, "Backups": [ { - "Name": "EXAMPLE_NAME", + "Name": "FullMirrorExample", + "Mode": "ArchivesOnly", "Host": "FTP_HOST", "Port": 21, "Username": "FTP_USERNAME", @@ -32,7 +33,23 @@ "AllowInvalidCertificate": true, "OperationTimeoutMinutes": 10, "CompletionTimeoutMinutes": 180, - "HistoryCopies": 5 + "HistoryCopies": 5, + "RetentionDays": 7 + }, + { + "Name": "ArchivesOnlyExample", + "Mode": "ArchivesOnly", + "Host": "FTP_HOST_ARCHIVES", + "Port": 21, + "Username": "FTP_USERNAME", + "Password": "FTP_PASSWORD", + "RemotePath": "/archives", + "LocalPath": "/", + "Encryption": "Explicit", + "Passive": true, + "AllowInvalidCertificate": true, + "CompletionTimeoutMinutes": 180, + "RetentionDays": 14 } ] }, @@ -40,4 +57,4 @@ "StartHour": 2, "StartMinute": 0 } -} +} \ No newline at end of file diff --git a/readme.md b/readme.md index 57069d2..9fb8bf8 100644 --- a/readme.md +++ b/readme.md @@ -68,6 +68,15 @@ Each backup job supports: download (default `10`). - `CompletionTimeoutMinutes`: overall per-job timeout for copy/archive work (default `180`). +- `Mode`: backup mode: `Full`, `ArchivesOnly`, or `RemoteZip` (default `Full`). +- `RemoteTriggerUrl`: optional HTTP endpoint to request server-side zipping + (used with `RemoteZip` mode). The endpoint should return a `Location` + URL (or JSON with `{"location":"..."}`) where the resulting archive + will be available for download. +- `RemoteTriggerPollIntervalSeconds`: polling interval when waiting for remote + archive (default `5` seconds). +- `RemoteTriggerTimeoutSeconds`: max seconds to wait for remote archive + after triggering (default `600` seconds). Example: @@ -128,11 +137,16 @@ $env:BackupOptions__Backups__0__Password = "password" - The service runs once per day at the configured time. - Backups execute sequentially; a failure does not block the next job. - Each job is cancelled if it exceeds its timeout. -- The remote path is mirrored into `LocalPath\current`. -- Snapshot copies are stored in `LocalPath\_history\yyyyMMdd_HHmmss`. -- A daily zip archive is created at - `LocalPath\archives\\yyyy-MM-dd.zip`, and archives older than - `RetentionDays` are deleted. +- **ArchivesOnly mode**: downloads only `.zip` files from the remote path + directly to `LocalPath`, preserving their original names. +- **Full mode**: mirrors the entire remote directory to a temporary folder, + then creates a dated zip archive at `LocalPath\yyyy-MM-dd.zip`. + - **RemoteZip mode**: optionally POSTs to `RemoteTriggerUrl` to request a + server-side archive. If the trigger returns a download location the + service will poll and download that archive; otherwise it falls back to + checking for `.zip` files on the FTP server and then to `Full`. +- Daily zip archives are stored directly in `LocalPath` with filenames + `yyyy-MM-dd.zip`, and archives older than `RetentionDays` are deleted. - Logs are written to the Windows Event Log and to the file path in `FileLogging:Path`. @@ -164,3 +178,39 @@ sc.exe delete BackupService the specific backup or install a trusted certificate on the machine. - If Event Log entries do not appear, run the service once with elevated permissions or pre-create the event source. + +## Remote trigger endpoint (new) + +I added a minimal example endpoint that can be deployed on target servers to +support the `RemoteZip` mode. The endpoint accepts a `POST /trigger` JSON +body, starts a background job that creates a zip archive from a local folder, +stores the archive under `wwwroot/archives/` and returns `202 Accepted` with +the `Location` header set to `/archives/{filename}`. `FtpBackupRunner` can +POST to this URL and poll for the archive using `HEAD` as implemented. + +Files added: +- `RemoteTriggerEndpoint/RemoteTriggerEndpoint.csproj` +- `RemoteTriggerEndpoint/Program.cs` (minimal API exposing `POST /trigger`) +- `RemoteTriggerEndpoint/ZipWorker.cs` (background worker that creates ZIPs) + +Quick start (from repository root): + +```powershell +dotnet run --project .\RemoteTriggerEndpoint -p:ASPNETCORE_URLS="http://0.0.0.0:5000" +``` + +Example `curl` to trigger zipping (run on the server or point `RemoteTriggerUrl` from the backup service to this server): + +```bash +curl -X POST http://localhost:5000/trigger -H "Content-Type: application/json" \ + -d '{"sourcePath":"C:\\path\\to\\data","archiveName":"siteA-backup.zip","overwrite":true}' -i +``` + +Response: +- `202 Accepted` with `Location: /archives/siteA-backup.zip` — the background job is creating the archive. +- `HEAD /archives/siteA-backup.zip` will return `200` when the file is ready; otherwise `404`. + +If you'd like, I can: +- add authentication to the endpoint (API key / basic) for security, +- add retention/cleanup for server-side archives, or +- create a lightweight PowerShell script alternative instead of ASP.NET Core.