Compare commits

..

12 Commits

Author SHA1 Message Date
523441dd2f extra logging 2025-12-17 00:15:43 -05:00
6e91d188fe ensure proper disposal of objects 2025-12-17 00:15:23 -05:00
ea7ff0b051 changed logging to allow debugging 2025-12-16 23:46:58 -05:00
63fa071180 Merge remote-tracking branch 'main/master'
# Conflicts:
#	Program.cs
2025-12-16 23:28:22 -05:00
3a1ddd7341 download in parallel 2025-12-16 23:27:50 -05:00
38322aa1c2 download in parallel 2025-12-16 23:26:58 -05:00
db55acd188 adding buffer size change 2025-12-16 23:12:24 -05:00
15ffda9be9 include tzdata to avoid time zone issues 2025-12-16 17:44:46 -05:00
5a509226bc fix all config.json paths 2025-12-16 17:26:46 -05:00
a24f7fd453 fix config path 2025-12-16 17:22:45 -05:00
a8e619de01 fix log location 2025-12-16 17:16:28 -05:00
f05c386168 specify USER 2025-12-16 17:16:20 -05:00
2 changed files with 62 additions and 20 deletions

View File

@@ -1,5 +1,11 @@
FROM mcr.microsoft.com/dotnet/runtime:10.0 AS base FROM mcr.microsoft.com/dotnet/runtime:10.0 AS base
USER $APP_UID
# Install timezone data so .NET can resolve local time zones
RUN apt-get update \
&& apt-get install -y --no-install-recommends tzdata \
&& rm -rf /var/lib/apt/lists/*
USER 1000
WORKDIR /app WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
@@ -8,7 +14,6 @@ WORKDIR /src
COPY ["FtpThing.csproj", "./"] COPY ["FtpThing.csproj", "./"]
RUN dotnet restore "FtpThing.csproj" RUN dotnet restore "FtpThing.csproj"
COPY . . COPY . .
WORKDIR "/src/"
RUN dotnet build "./FtpThing.csproj" -c $BUILD_CONFIGURATION -o /app/build RUN dotnet build "./FtpThing.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish FROM build AS publish

View File

@@ -4,40 +4,65 @@ using Cronos;
using Renci.SshNet; using Renci.SshNet;
using Renci.SshNet.Sftp; using Renci.SshNet.Sftp;
using Serilog; using Serilog;
using Serilog.Core;
using Serilog.Events;
var resetEvent = new ManualResetEvent(false); var resetEvent = new ManualResetEvent(false);
using var tokenSource = new CancellationTokenSource(); using var tokenSource = new CancellationTokenSource();
Console.CancelKeyPress += (sender, eventArgs) => Console.CancelKeyPress += (_, eventArgs) =>
{ {
resetEvent?.Set(); Log.Debug("Canceling with CancelKeyPress");
tokenSource?.Cancel(); Dispose();
eventArgs.Cancel = true; eventArgs.Cancel = true;
Log.CloseAndFlush();
}; };
AppDomain.CurrentDomain.ProcessExit += (_, _) => Dispose();
SetupLogging(); SetupLogging();
//TODO: check environment variables to see if config file is enabled, otherwise use env vars
var config = LoadConfig(); var config = LoadConfig();
Log.Information("Configuration file loaded. Beginning initial grab."); Log.Information("Configuration file loaded. Beginning initial grab.");
await RunJob(config, tokenSource.Token); await RunJob(config, tokenSource.Token);
Log.Information("Initial grab complete. Initializing schedule."); Log.Information("Initial grab complete. Initializing schedule.");
Task.Run(() => ScheduleJobs(config, tokenSource.Token)); Task.Run(() => ScheduleJobs(config, tokenSource.Token));
Log.Verbose("Jobs scheduled. Main Thread waiting.");
resetEvent.WaitOne(); resetEvent.WaitOne();
return; return;
void Dispose()
{
resetEvent?.Set();
tokenSource?.Cancel();
Log.CloseAndFlush();
}
static void SetupLogging() static void SetupLogging()
{ {
var minLevel = (Environment.GetEnvironmentVariable("LOG_LEVEL") ?? "info").ToUpperInvariant() switch
{
"VERBOSE" => LogEventLevel.Verbose,
"DEBUG" => LogEventLevel.Debug,
"WARNING" => LogEventLevel.Warning,
"ERROR" => LogEventLevel.Error,
"FATAL" => LogEventLevel.Fatal,
_ => LogEventLevel.Information
};
var levelSwitch = new LoggingLevelSwitch(minLevel);
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information() .MinimumLevel.ControlledBy(levelSwitch)
.WriteTo.Console() .WriteTo.Console()
.WriteTo.File("log.txt", .WriteTo.File("log.txt",
rollingInterval: RollingInterval.Day, rollingInterval: RollingInterval.Day,
rollOnFileSizeLimit: true, rollOnFileSizeLimit: true,
retainedFileCountLimit: 10) retainedFileCountLimit: 10)
.CreateLogger(); .CreateLogger();
Log.Debug($"Logging initialized at {minLevel}");
} }
static Config LoadConfig() static Config LoadConfig()
@@ -48,14 +73,15 @@ static Config LoadConfig()
WriteIndented = true WriteIndented = true
}; };
if (!File.Exists("config.json")) //TODO: stop creating dummy config file
if (!File.Exists("/config.json"))
{ {
Log.Fatal("No config file found, creating dummy config."); Log.Fatal("No config file found, creating dummy config.");
CreateConfig(serializationOptions); CreateConfig(serializationOptions);
System.Environment.Exit(1); Environment.Exit(1);
} }
Log.Verbose("Loading config from file.");
var configFile = File.ReadAllText("config.json"); var configFile = File.ReadAllText("/config.json");
return JsonSerializer.Deserialize<Config>(configFile, serializationOptions); return JsonSerializer.Deserialize<Config>(configFile, serializationOptions);
} }
@@ -64,11 +90,12 @@ static void CreateConfig(JsonSerializerOptions options)
var dummyGrab = new GrabTarget(@"/path/to/source", @"/path/to/destination"); var dummyGrab = new GrabTarget(@"/path/to/source", @"/path/to/destination");
var dummySFtp = new SFtpTarget("ftp.domain.example", 22, "username", false, "password"); var dummySFtp = new SFtpTarget("ftp.domain.example", 22, "username", false, "password");
var config = new Config(dummySFtp, [dummyGrab, dummyGrab], "* * * * *"); var config = new Config(dummySFtp, [dummyGrab, dummyGrab], "* * * * *");
File.WriteAllText("config.json", JsonSerializer.Serialize(config, options)); File.WriteAllText("/config.json", JsonSerializer.Serialize(config, options));
} }
static async Task ScheduleJobs(Config config, CancellationToken token) static async Task ScheduleJobs(Config config, CancellationToken token)
{ {
Log.Debug($"Scheduling job with cron expression '{config.Schedule}'");
var schedule = CronExpression.Parse(config.Schedule); var schedule = CronExpression.Parse(config.Schedule);
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)
@@ -80,11 +107,12 @@ static async Task ScheduleJobs(Config config, CancellationToken token)
Log.Information($"Next scheduled scan at {nextJob.Value}"); Log.Information($"Next scheduled scan at {nextJob.Value}");
var delay = nextJob.Value - now; var delay = nextJob.Value - now;
Log.Debug($"{delay} until next scan at {nextJob.Value}");
if (delay > TimeSpan.Zero) if (delay > TimeSpan.Zero)
{ {
await Task.Delay(delay, token); await Task.Delay(delay, token);
} }
Log.Debug($"Delay {delay} wait is complete, beginning scan now.");
await RunJob(config, token); await RunJob(config, token);
} }
} }
@@ -92,8 +120,13 @@ static async Task ScheduleJobs(Config config, CancellationToken token)
static async Task RunJob(Config config, CancellationToken token) static async Task RunJob(Config config, CancellationToken token)
{ {
using var client = GetClient(config.FTP); using var client = GetClient(config.FTP);
Log.Information($"Connecting to {config.FTP.Host}:{config.FTP.Port} as {config.FTP.UserName}");
var buffer = Math.Clamp(config.BufferSizeMB ?? 1, 0.5f, 4);
client.BufferSize = (uint)(1024 * 1024 * buffer);
Log.Debug($"Connection buffer size: {client.BufferSize}");
await client.ConnectAsync(token); await client.ConnectAsync(token);
Log.Information($"Connected to {config.FTP.Host}:{config.FTP.Port} as {config.FTP.UserName}");
foreach (var target in config.Targets) foreach (var target in config.Targets)
{ {
Log.Information($"Beginning grab operation for '{target.Source}' to '{target.Destination}'"); Log.Information($"Beginning grab operation for '{target.Source}' to '{target.Destination}'");
@@ -105,21 +138,23 @@ static async Task RunJob(Config config, CancellationToken token)
static async Task RecurseDirectory(SftpClient client, string source, string destination, bool deleteDirectory, CancellationToken token) static async Task RecurseDirectory(SftpClient client, string source, string destination, bool deleteDirectory, CancellationToken token)
{ {
Log.Information($"Scanning directory '{source}'"); Log.Information($"Scanning directory '{source}'");
await foreach (var item in client.ListDirectoryAsync(source, token)) await Parallel.ForEachAsync(client.ListDirectoryAsync(source, token), token, async (item,token) =>
{ {
if (item.Name is ".." or ".") continue; if (item.Name is ".." or ".") return;
if (item.IsDirectory) if (item.IsDirectory)
{ {
Log.Verbose($"{item.Name} is a directory");
var newPath = Path.Combine(destination, item.Name); var newPath = Path.Combine(destination, item.Name);
await RecurseDirectory(client, item.FullName, newPath, true, token); await RecurseDirectory(client, item.FullName, newPath, true, token);
continue; return;
} }
Log.Verbose($"{item.Name} is a file");
await DownloadFile(client, item, destination, token); await DownloadFile(client, item, destination, token);
Log.Information($"Deleting '{item.Name}'"); Log.Information($"Deleting '{item.Name}'");
await client.DeleteAsync(item.FullName, token); await client.DeleteAsync(item.FullName, token);
} });
if (deleteDirectory) if (deleteDirectory)
{ {
@@ -131,9 +166,11 @@ static async Task RecurseDirectory(SftpClient client, string source, string dest
static async Task DownloadFile(SftpClient client, ISftpFile item, string destination, CancellationToken token) static async Task DownloadFile(SftpClient client, ISftpFile item, string destination, CancellationToken token)
{ {
Directory.CreateDirectory(destination); Directory.CreateDirectory(destination);
Log.Verbose($"Ensuring '{destination}' path exists.");
await using var stream = File.Open(Path.Combine(destination, item.Name), FileMode.Create, FileAccess.Write); await using var stream = File.Open(Path.Combine(destination, item.Name), FileMode.Create, FileAccess.Write);
Log.Information($"Downloading '{item.FullName}' to '{stream.Name}'"); Log.Information($"Downloading '{item.FullName}' to '{stream.Name}'");
await client.DownloadFileAsync(item.FullName, stream, token); await client.DownloadFileAsync(item.FullName, stream, token);
Log.Verbose("Download completed.");
} }
static SftpClient GetClient(SFtpTarget target) static SftpClient GetClient(SFtpTarget target)
@@ -144,6 +181,6 @@ static SftpClient GetClient(SFtpTarget target)
} }
public record Config(SFtpTarget FTP, GrabTarget[] Targets, string Schedule); public record Config(SFtpTarget FTP, GrabTarget[] Targets, string Schedule, float? BufferSizeMB = null);
public record SFtpTarget(string Host, int Port, string UserName, bool usePrivateKey, string? Password = null, string? KeyFile = null); public record SFtpTarget(string Host, int Port, string UserName, bool usePrivateKey, string? Password = null, string? KeyFile = null);
public record GrabTarget(string Source, string Destination); public record GrabTarget(string Source, string Destination);