Skip to content

Commit f3aa3c1

Browse files
Merge pull request #12 from andreaskueffel/development
add scp provider
2 parents d7b99e4 + b62cef0 commit f3aa3c1

6 files changed

Lines changed: 196 additions & 29 deletions

File tree

FileSyncApp/Program.cs

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,12 @@ public class Program
2929
public static volatile bool keepRunning = true;
3030
public static LoggingLevelSwitch LoggingLevel { get; private set; }
3131
public static ILoggerFactory LoggerFactory { get; private set; }
32+
private static Microsoft.Extensions.Logging.ILogger log;
3233
public static Dictionary<string, IFileJob> Jobs = new Dictionary<string, IFileJob>();
3334
public static void Main(string[] args)
3435
{
35-
36-
37-
38-
39-
40-
LoggingLevel= new LoggingLevelSwitch(Serilog.Events.LogEventLevel.Information);
41-
#if DEBUG
42-
LoggingLevel= new LoggingLevelSwitch(Serilog.Events.LogEventLevel.Verbose);
43-
#endif
4436
ConfigureLogger();
45-
LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => { builder.AddSerilog(Serilog.Log.Logger); });
37+
log = LoggerFactory.CreateLogger("FileSyncAppMain");
4638
if (null!=args && args.Length > 0)
4739
{
4840
if (args.Contains("debug"))
@@ -58,7 +50,7 @@ public static void Main(string[] args)
5850

5951
static void RunProgram()
6052
{
61-
Console.WriteLine("FileSyncApp - synchronizing folders and clean them up");
53+
log.LogInformation("FileSyncApp - synchronizing folders and clean them up");
6254
Dictionary<string, IFileJobOptions> jobOptions = new Dictionary<string, IFileJobOptions>();
6355
var jsonSettings = new JsonSerializerSettings
6456
{
@@ -68,6 +60,7 @@ static void RunProgram()
6860
};
6961
if (!File.Exists("config.json"))
7062
{
63+
log.LogInformation("Config file {A} not found, creating a new one", "config.json");
7164
var cleanJob = FileCleanJobOptionsBuilder.CreateBuilder()
7265
.WithDestinationPath("temp")
7366
.WithInterval(TimeSpan.FromMinutes(21))
@@ -103,6 +96,7 @@ static void RunProgram()
10396
var json = JsonConvert.SerializeObject(jobOptions, Formatting.Indented, jsonSettings);
10497
File.WriteAllText("config.json", json);
10598
}
99+
log.LogInformation("reading config file {A}", "config.json");
106100
var readJobOptions = JsonConvert.DeserializeObject<Dictionary<string, IFileJobOptions>>(File.ReadAllText("config.json"), jsonSettings);
107101
//List<IFileJob> Jobs = new List<IFileJob>();
108102

@@ -117,16 +111,22 @@ static void RunProgram()
117111
{
118112
job.Value.StartJob();
119113
}
120-
Console.WriteLine("Press Ctrl+C to exit");
114+
log.LogInformation("Press Ctrl+C to exit");
121115
while (keepRunning)
122116
{
123-
Thread.Sleep(200);
117+
Thread.Sleep(500);
124118
}
125119
}
126120

127121

128-
private static void ConfigureLogger()
122+
public static void ConfigureLogger()
129123
{
124+
if (LoggingLevel != null)
125+
return;
126+
LoggingLevel = new LoggingLevelSwitch(Serilog.Events.LogEventLevel.Information);
127+
#if DEBUG
128+
LoggingLevel = new LoggingLevelSwitch(Serilog.Events.LogEventLevel.Verbose);
129+
#endif
130130
string serilogFileTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext:l} {Message:lj}{NewLine}{Exception}";
131131
string serilogConsoleTemplate = "{Timestamp:HH:mm:ss.fff}[{Level:u3}]{SourceContext:l} {Message:lj}{NewLine}{Exception}";
132132
Serilog.Log.Logger = new LoggerConfiguration()
@@ -137,12 +137,13 @@ private static void ConfigureLogger()
137137
retainedFileCountLimit: 10,
138138
fileSizeLimitBytes: 1024 * 1024 * 100,
139139
rollingInterval: RollingInterval.Day,
140+
rollOnFileSizeLimit: true,
140141
outputTemplate: serilogFileTemplate),
141142
blockWhenFull: true)
142143
.MinimumLevel.ControlledBy(LoggingLevel)
143144
.CreateLogger();
144145

145-
146+
LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => { builder.AddSerilog(Serilog.Log.Logger); });
146147
}
147148

148149

FileSyncAppWin/MainForm.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using FileSyncLibNet.Commons;
2-
31
namespace FileSyncAppWin
42
{
53
public partial class MainForm : Form
@@ -12,11 +10,11 @@ public MainForm()
1210
this.FormClosing += (s, e) => { FileSyncApp.Program.keepRunning = false; consoleThread?.Join(10_000); };
1311
consoleThread = new Thread(() =>
1412
{
15-
1613
FileSyncApp.Program.Main(null);
1714
});
18-
FileSyncApp.Program.JobsReady += (s,e)=> {
19-
foreach(var job in FileSyncApp.Program.Jobs)
15+
FileSyncApp.Program.JobsReady += (s, e) =>
16+
{
17+
foreach (var job in FileSyncApp.Program.Jobs)
2018
{
2119
job.Value.JobStarted += (j, text) =>
2220
{
@@ -36,6 +34,7 @@ public MainForm()
3634

3735
};
3836
consoleThread.Start();
37+
3938
this.Resize += ((s, e) =>
4039
{
4140
this.SuspendLayout();
@@ -56,20 +55,20 @@ public MainForm()
5655
{
5756
this.BeginInvoke(() => { WindowState = FormWindowState.Normal; ShowInTaskbar = true; });
5857
};
59-
notifyIcon1.BalloonTipClicked += (s, e) => { this.BeginInvoke(() => { WindowState = FormWindowState.Normal; ShowInTaskbar = true; }); };
58+
notifyIcon1.BalloonTipClicked += (s, e) => { this.BeginInvoke(() => { WindowState = FormWindowState.Normal; ShowInTaskbar = true; }); };
6059
//notifyIcon1.BalloonTipShown += (s, e) => { ShowInTaskbar = false; };
6160

6261
//this.WindowState = FormWindowState.Minimized;
6362
}
6463

65-
6664

67-
65+
66+
6867
private void NewLogOutput(string e)
6968
{
7069

71-
this.BeginInvoke(() => { textBox1.Text = string.Join(Environment.NewLine, (new string[] { $"{DateTime.Now.ToString("HH:mm:ss.fff")} {e}" }).Concat(textBox1.Text.Split(Environment.NewLine).Take(1000))); });
72-
70+
this.BeginInvoke(() => { textBox1.Text = string.Join(Environment.NewLine, (new string[] { $"{DateTime.Now.ToString("HH:mm:ss.fff")} {e}" }).Concat(textBox1.Text.Split(Environment.NewLine).Take(1000))); });
71+
7372
}
7473
}
7574
}

FileSyncLibNet/FileSyncJob/FileSyncJob.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ private FileSyncJob(IFileJobOptions fileSyncJobOptions)
3535
case SyncProvider.Robocopy:
3636
syncProvider = new RoboCopyProvider(fileSyncJobOptions);
3737
break;
38+
case SyncProvider.SCP:
39+
syncProvider = new ScpProvider(fileSyncJobOptions);
40+
break;
3841
}
3942
}
4043

@@ -70,7 +73,14 @@ public void StopJob()
7073
}
7174
private void TimerElapsed(object state)
7275
{
73-
RunJobInterlocked();
76+
try
77+
{
78+
RunJobInterlocked();
79+
}
80+
catch (Exception exc)
81+
{
82+
JobError?.Invoke(this, new FileSyncJobEventArgs(JobName, FileSyncJobStatus.Error, exc));
83+
}
7484
}
7585

7686
private void RunJobInterlocked()

FileSyncLibNet/FileSyncLibNet.csproj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
<Description>A library to easily backup or sync 2 folders either once or in a given interval.</Description>
1313
</PropertyGroup>
1414
<ItemGroup>
15-
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
16-
<PackageReference Include="RoboSharp" Version="1.3.5" />
17-
<PackageReference Include="SMBLibrary" Version="1.5.0.1" />
15+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
16+
<PackageReference Include="RoboSharp" Version="1.5.3" />
17+
<PackageReference Include="SMBLibrary" Version="1.5.3.5" />
18+
<PackageReference Include="SSH.NET" Version="2024.1.0" />
1819
</ItemGroup>
1920
<ItemGroup>
2021
<Content Include="../README.md">
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
using FileSyncLibNet.Commons;
2+
using FileSyncLibNet.FileCleanJob;
3+
using FileSyncLibNet.FileSyncJob;
4+
using Microsoft.Extensions.Logging;
5+
using Renci.SshNet;
6+
using Renci.SshNet.Sftp;
7+
using System;
8+
using System.Diagnostics;
9+
using System.IO;
10+
using System.Linq;
11+
using System.Text.RegularExpressions;
12+
13+
namespace FileSyncLibNet.SyncProviders
14+
{
15+
internal class ScpProvider : ProviderBase
16+
{
17+
18+
19+
public ScpProvider(IFileJobOptions options) : base(options)
20+
{
21+
22+
}
23+
24+
private void CreateDirectoryRecursive(SftpClient client, string path)
25+
{
26+
var dirs = path.Split('/');
27+
string currentPath =path.StartsWith("/")?"/":"";
28+
for (int i = 0; i < dirs.Length; i++)
29+
{
30+
currentPath = (currentPath.EndsWith("/")?currentPath:(currentPath+"/")) + dirs[i];
31+
if (!string.IsNullOrEmpty(currentPath) && currentPath != "/")
32+
if(!client.Exists(currentPath))
33+
client.CreateDirectory(currentPath);
34+
}
35+
}
36+
37+
public override void SyncSourceToDest()
38+
{
39+
if (!(JobOptions is IFileSyncJobOptions jobOptions))
40+
throw new ArgumentException("this instance has no information about syncing files, it has type " + JobOptions.GetType().ToString());
41+
var sw = Stopwatch.StartNew();
42+
//Format
43+
44+
var pattern = @"scp://(?:(?<user>[^@]+)@)?(?<host>[^:/]+)(?::(?<port>\d+))?(?<path>/.*)?";
45+
var match = Regex.Match(JobOptions.DestinationPath, pattern);
46+
string path;
47+
SftpClient ftpClient;
48+
if (!match.Success)
49+
{
50+
throw new UriFormatException($"Unable to match scp pattern with given URL {JobOptions.DestinationPath}, use format scp://host:port/path");
51+
}
52+
else
53+
{
54+
var user = match.Groups["user"].Value;
55+
var host = match.Groups["host"].Value;
56+
var port = int.Parse(match.Groups["port"].Success ? match.Groups["port"].Value : "22"); // Default SCP port
57+
path = match.Groups["path"].Value;
58+
59+
60+
ftpClient = new SftpClient(host, port, JobOptions.Credentials.UserName, JobOptions.Credentials.Password);
61+
}
62+
ftpClient.Connect();
63+
CreateDirectoryRecursive(ftpClient, path);
64+
65+
var remoteFiles = ftpClient.ListDirectory(path);
66+
67+
Directory.CreateDirectory(jobOptions.SourcePath);
68+
69+
int copied = 0;
70+
int skipped = 0;
71+
DirectoryInfo _di = new DirectoryInfo(jobOptions.SourcePath);
72+
//Dateien ins Backup kopieren
73+
if (JobOptions.Credentials != null)
74+
{
75+
76+
}
77+
78+
79+
foreach (var dir in JobOptions.Subfolders.Count > 0 ? _di.GetDirectories() : new[] { _di })
80+
{
81+
if (JobOptions.Subfolders.Count > 0 && !JobOptions.Subfolders.Select(x => x.ToLower()).Contains(dir.Name.ToLower()))
82+
continue;
83+
var _fi = dir.EnumerateFiles(
84+
searchPattern: JobOptions.SearchPattern,
85+
searchOption: JobOptions.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
86+
87+
if (jobOptions.RememberLastSync)
88+
{
89+
var old = _fi.Count();
90+
_fi = _fi.Where(x => x.LastWriteTime > (LastRun - jobOptions.Interval)).ToList();
91+
skipped += old - _fi.Count();
92+
LastRun = DateTimeOffset.Now;
93+
}
94+
foreach (FileInfo f in _fi)
95+
{
96+
97+
var relativeFilename = f.FullName.Substring(Path.GetFullPath(jobOptions.SourcePath).Length).TrimStart('\\');
98+
var remoteFilename = Path.Combine(path, relativeFilename).Replace('\\', '/');
99+
ISftpFile remotefile = null;
100+
if (ftpClient.Exists(remoteFilename))
101+
{
102+
remotefile = ftpClient.Get(remoteFilename);
103+
}
104+
bool exists = remotefile != null;
105+
bool lengthMatch = exists && remotefile.Length == f.Length;
106+
bool timeMatch = exists && Math.Abs((remotefile.LastWriteTimeUtc -f.LastWriteTimeUtc).TotalSeconds)<1;
107+
108+
bool copy = !exists || !lengthMatch || !timeMatch;
109+
if (copy)
110+
{
111+
try
112+
{
113+
logger.LogDebug("Copy {A}", relativeFilename);
114+
CreateDirectoryRecursive(ftpClient, Path.GetDirectoryName(remoteFilename).Replace('\\', '/'));
115+
116+
using (var fileStream = System.IO.File.OpenRead(f.FullName))
117+
{
118+
ftpClient.UploadFile(fileStream, remoteFilename);
119+
}
120+
ftpClient.SetLastWriteTimeUtc(remoteFilename, f.LastAccessTimeUtc);
121+
copied++;
122+
if (jobOptions.DeleteSourceAfterBackup)
123+
{
124+
File.Delete(f.FullName);
125+
}
126+
}
127+
catch (Exception exc)
128+
{
129+
logger.LogError(exc, "Exception copying {A}", relativeFilename);
130+
}
131+
}
132+
else
133+
{
134+
skipped++;
135+
logger.LogTrace("Skip {A}", relativeFilename);
136+
}
137+
}
138+
}
139+
sw.Stop();
140+
logger.LogInformation("{A} files copied, {B} files skipped in {C}s", copied, skipped, sw.ElapsedMilliseconds / 1000.0);
141+
}
142+
143+
144+
public override void DeleteFiles()
145+
{
146+
if (!(JobOptions is IFileCleanJobOptions jobOptions))
147+
throw new ArgumentException("this instance has no information about deleting files, it has type " + JobOptions.GetType().ToString());
148+
149+
throw new NotImplementedException("deleting files via scp currently is not supported");
150+
}
151+
152+
153+
154+
}
155+
}

FileSyncLibNet/SyncProviders/SyncProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ public enum SyncProvider
99
FileIO,
1010
SMBLib,
1111
Robocopy,
12+
SCP,
1213
}
1314
}

0 commit comments

Comments
 (0)