tesses-backup/TessesDedupClient/Program.cs

617 lines
20 KiB
C#

using System.Diagnostics;
using System.Net;
using Newtonsoft.Json;
using Tesses.VirtualFilesystem;
using Tesses.VirtualFilesystem.Filesystems;
using TessesDedup;
string verb = "";
List<KeyValuePair<string,string>> newArgs=new List<KeyValuePair<string, string>>();
List<string> positionalArguments =new List<string>();
string endpoint = Environment.GetEnvironmentVariable("TBKP_ENDPOINT") ?? "";
string accessKey = Environment.GetEnvironmentVariable("TBKP_ACCESS_KEY") ?? "";
bool env_set = !string.IsNullOrWhiteSpace(endpoint) && !string.IsNullOrWhiteSpace(accessKey);
string GetLoginPath()
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData,Environment.SpecialFolderOption.Create),"tbkp_login.json");
}
if(!env_set)
{
string conf = GetLoginPath();
if(File.Exists(conf))
{
var res=JsonConvert.DeserializeObject<LoginConf>(File.ReadAllText(conf));
if(res != null)
{
endpoint = res.Endpoint;
accessKey = res.AccessKey;
}
}
}
IEnumerable<string> GetArgsViaKey(string key)
{
foreach(var item in newArgs)
{
if(item.Key == key)
yield return item.Value;
}
}
bool TryGetFirst(string key,out string val)
{
foreach(var item in GetArgsViaKey(key))
{
val = item;
return true;
}
val="";
return false;
}
string GetOrAsk(string prompt,bool password,params string[] keys)
{
foreach(var item in keys)
{
if(TryGetFirst(item,out var str))
{
return str;
}
}
while(true)
{
if(password)
{
string res = ReadLine.ReadPassword($"{prompt}: ");
if(!string.IsNullOrWhiteSpace(res))
return res;
}
else{
string res = ReadLine.Read($"{prompt}: ");
if(!string.IsNullOrWhiteSpace(res))
return res;
}
}
}
string GetOrAskWithDefault(string prompt,string defaultValue,params string[] keys)
{
foreach(var item in keys)
{
if(TryGetFirst(item,out var str))
{
return str;
}
}
while(true)
{
ReadLine.AddHistory(new string[]{defaultValue});
string res = ReadLine.Read($"{prompt}: ");
if(!string.IsNullOrWhiteSpace(res))
return res;
}
}
if(args.Length > 0)
{
if(args[0] == "--help")
{
PrintHelp();
return;
}
else
{
verb = args[0];
bool mustBePositionalArgs=false;
for(int i = 1;i<args.Length;i++)
{
if(!mustBePositionalArgs && args[i] == "--")
{
mustBePositionalArgs = true;
}else
if(!mustBePositionalArgs && args[i].StartsWith('-'))
{
string key= args[i].TrimStart('-');
if(key == "help")
{
PrintHelp(verb);
return;
}
else
if(i+1<args.Length)
{
string value = args[i+1];
i++;
newArgs.Add(new KeyValuePair<string, string>(key,value));
}
}
else
{
positionalArguments.Add(args[i]);
}
}
}
}
else
{
PrintHelp();
return;
}
void PrintHelp(string? page=null)
{
if(string.IsNullOrWhiteSpace(page))
{
Console.WriteLine("tbkp");
Console.WriteLine("VERBS:");
Console.WriteLine("\tlogin\tLogin to backup server");
Console.WriteLine("\tlogout\tLogout from backup server");
Console.WriteLine("\tmount\tMount backups to filesystem");
Console.WriteLine("\tunmount\tUnmount backups to filesystem");
Console.WriteLine("\tbackup\tCreate backup");
Console.WriteLine("\tbackupex\tCreate backup with multiple mounts uses Zio C# Library: https://github.com/xoofx/zio");
Console.WriteLine("\trestore\tRestore backup with optional subentry");
Console.WriteLine("\tlist\tList backups");
Console.WriteLine("TIPS:");
Console.WriteLine("\tFlags always require a value (except for --help) so if you want to set a boolean to true type --flagName true or --flagName false for false");
Console.WriteLine("\tSet environment variables TBKP_ENDPOINT and TBKP_ACCESS_KEY whereever configuration is unavailable");
}
else
{
switch(page)
{
case "login":
Console.WriteLine("tbkp login [options]");
Console.WriteLine("FLAGS:");
Console.WriteLine("-u,--user\tUsername");
Console.WriteLine("-p,--pass\tPassword");
Console.WriteLine("-e,--url\tThe Url to the server");
Console.WriteLine("-d,--deviceName\tThe Device Name");
Console.WriteLine("-a,--justAccessKey true\tusing --justAccessKey true will print created access key to console and wont save to file");
Console.WriteLine();
Console.WriteLine("NOTE: If the flags are not set (other than --justAccessKey true), we will ask for credentials");
break;
case "logout":
Console.WriteLine("tbkp logout");
Console.WriteLine("This is a verb without flags or arguments");
break;
case "mount":
Console.WriteLine("tbkp mount [path]");
Console.WriteLine("ARGS:");
Console.WriteLine("path (optional):\tMount to this path on your computer");
Console.WriteLine();
Console.WriteLine($"NOTE: If path is not specified, \"{DefaultTBKP()}\" will be assumed");
Console.WriteLine("NOTE: httpdirfs is required, debian: sudo apt install httpdirfs, others: https://github.com/fangfufu/httpdirfs");
break;
case "unmount":
Console.WriteLine("tbkp unmount [path]");
Console.WriteLine("ARGS:");
Console.WriteLine("path (optional):\tunmount from this path on your computer");
Console.WriteLine();
Console.WriteLine($"NOTE: If path is not specified, \"{DefaultTBKP()}\" will be assumed");
break;
case "backup":
Console.WriteLine("tbkp backup [options] path");
Console.WriteLine("ARGS:");
Console.WriteLine("path (optional):\tfolder to backup (defaults to current directory)");
Console.WriteLine();
Console.WriteLine("FLAGS:");
Console.WriteLine("-t, --tag:\tTag of backup (defaults to \"default\")");
Console.WriteLine("-s,--silent true\tusing --silent true will disable progress");
break;
case "backupex":
Console.WriteLine("tbkp backupex [options] root");
Console.WriteLine("ARGS:");
Console.WriteLine("root (optional):\troot folder to backup");
Console.WriteLine();
Console.WriteLine("FLAGS:");
Console.WriteLine("-t, --tag:\tTag of backup (defaults to \"default\")");
Console.WriteLine("-v, --volume:\tMount folders in backup like bind mounts in docker, -v /Path/On/Host:/Path/In/Backup");
Console.WriteLine("-s,--silent true\tusing --silent true will disable progress");
Console.WriteLine();
Console.WriteLine("NOTE: On windows using -v command you must replace C:\\YourPath\\ with \\con\\C\\YourPath\\ due to the colon");
Console.WriteLine("NOTE: On windows using -v command you can use / rather than \\ except for the \\con\\ part");
break;
case "list":
Console.WriteLine("tbkp list [options] /path/to/dir");
Console.WriteLine("ARGS:");
Console.WriteLine("/path/to/dir (optional):\tfolder to enumerate in backup");
Console.WriteLine();
Console.WriteLine("FLAGS:");
Console.WriteLine("-b, --backupId\t: The backup number (without this, it lists the backups)");
break;
case "restore":
Console.WriteLine("tbkp list [options] /path/to/output");
Console.WriteLine("ARGS:");
Console.WriteLine("/path/to/output:\tthe path to restore to, for files this will be the filename, for directory this will be the folder that contains the folder in backup's files");
Console.WriteLine();
Console.WriteLine("FLAGS:");
Console.WriteLine("-b, --backupId\t: The backup number");
Console.WriteLine("-p, --path\t: The path inside backup (defaults to / and can be file, but if file the /path/to/output must not be a directory)");
Console.WriteLine("-s,--silent true\tusing --silent true will disable progress");
break;
}
}
}
void Mount(string path)
{
Directory.CreateDirectory(path);
MountIntern(path);
}
void Mount2()
{
string p = DefaultTBKP();
string file = Path.Combine(p,"readme.txt");
if(File.Exists(file))
{
MountIntern(p);
}
else
{
Console.WriteLine("Already mounted");
}
}
void MountIntern(string path)
{
using(Process p = new Process())
{
p.StartInfo.FileName = Find("httpdirfs");
p.StartInfo.ArgumentList.Add("-o");
p.StartInfo.ArgumentList.Add("nonempty");
p.StartInfo.ArgumentList.Add("-u");
p.StartInfo.ArgumentList.Add("$access_key");
p.StartInfo.ArgumentList.Add("-p");
p.StartInfo.ArgumentList.Add(accessKey);
p.StartInfo.ArgumentList.Add($"{endpoint.TrimEnd('/')}/data/");
p.StartInfo.ArgumentList.Add(path);
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput=true;
p.StartInfo.RedirectStandardError=true;
p.Start();
}
}
void Unmount2()
{
string p = DefaultTBKP();
string file = Path.Combine(p,"readme.txt");
if(File.Exists(file))
{
Console.WriteLine("Filesystem not mounted");
}
else
{
Umount(p);
}
}
void Umount(string path)
{
using(Process p = new Process())
{
p.StartInfo.FileName = Find(Environment.OSVersion.Platform == PlatformID.Win32NT ? "mountvol" : "umount");
if(Environment.OSVersion.Platform == PlatformID.Win32NT)
{
p.StartInfo.ArgumentList.Add(path);
p.StartInfo.ArgumentList.Add("/d");
}
else
{
p.StartInfo.ArgumentList.Add(path);
}
if(p.Start()) {
p.WaitForExit();
}
}
}
string Find(string v)
{
string[] path = (Environment.GetEnvironmentVariable("PATH") ?? "").Split(Path.PathSeparator);
string[] pathext = (Environment.GetEnvironmentVariable("PATHEXT") ?? "").Split(Path.PathSeparator);
foreach(var dir in path)
{
string _vp = Path.Combine(dir,v);
if(File.Exists(_vp)) return _vp;
foreach(var ext in pathext)
{
string p=$"{_vp}{ext}";
if(File.Exists(p)) return p;
}
}
return v;
}
string DefaultTBKP()
{
var dir= Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments,Environment.SpecialFolderOption.Create),"Tesses Backups");
if(!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
File.WriteAllText(Path.Combine(dir,"readme.txt"),"Don't Delete this file, unless you delete the folder it is in.\nAlso don't put any files in this directory as the tool mounts here\n");
}
return dir;
}
switch (verb)
{
case "login":
{
if(env_set) {
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("ERROR: TBKP_ENDPOINT and TBKP_ACCESS_KEY are set");
Console.ResetColor();
return;
}
string url=GetOrAsk("Endpoint (Server Url)",false,"url","e");
string username=GetOrAsk("Username",false,"user","u");
string password=GetOrAsk("Password",true,"pass","p");
string deviceName=GetOrAskWithDefault("Device Name (press up for hostname)",Dns.GetHostName(),"deviceName","d");
using DedupClient client=new DedupClient(url);
var res=await client.LoginAsync(username,password,deviceName);
if(res.Success)
{
if((TryGetFirst("justAccessKey",out var akV) || TryGetFirst("a",out akV)) && akV == "true")
{
Console.WriteLine($"Your access key is: {res.Key}");
}
else
{
LoginConf conf = new LoginConf();
conf.AccessKey = res.Key;
conf.Endpoint = url;
File.WriteAllText(GetLoginPath(),JsonConvert.SerializeObject(conf));
Console.WriteLine("Logged in successfully");
}
}
else
{
Console.WriteLine("Can't login, invalid password or something like that");
}
}
break;
case "logout":
{
using DedupClient client=new DedupClient(endpoint);
await client.LogoutAsync(endpoint);
if(!env_set)
{
File.Delete(GetLoginPath());
}
}
break;
case "mount":
{
if(positionalArguments.Count > 0)
{
Mount(positionalArguments[0]);
}
else
{
Mount2();
}
}
break;
case "unmount":
{
if(positionalArguments.Count > 0)
{
Umount(positionalArguments[0]);
}
else
{
Unmount2();
}
}
break;
case "restore":
{
bool silent = false;
if(TryGetFirst("s",out var sltStr) || TryGetFirst("silent",out sltStr))
{
silent = sltStr=="true";
}
if(positionalArguments.Count > 0)
{
if((TryGetFirst("backupId",out var backupId) || TryGetFirst("b",out backupId)) && long.TryParse(backupId,out var id))
{
string path = "/";
if(TryGetFirst("p",out var p) || TryGetFirst("path",out p))
path = p;
//we need to restore backup
// public async Task RestoreAsync(string key,long id,UnixPath srcPath,IVirtualFilesystem fs,UnixPath destPath,CancellationToken token=default)
using var ddc = new DedupClient(endpoint,!silent);
LocalFileSystem fs = new LocalFileSystem();
await ddc.RestoreAsync(accessKey,id,new UnixPath(path),fs,UnixPath.FromLocal(Path.GetFullPath(positionalArguments[0])));
}
}
}
break;
case "list":
{
if(positionalArguments.Count > 0)
{
if((TryGetFirst("backupId",out var backupId) || TryGetFirst("b",out backupId)) && long.TryParse(backupId,out var id))
{
using DedupClient client = new DedupClient(endpoint);
var bkp=await client.GetBackup(accessKey,id);
foreach(var item in bkp.GetEntryFromPath(new UnixPath(positionalArguments[0])).Entries)
{
Console.WriteLine($"[{item.Type.ToString().ToUpper()}] {item.Name}");
}
}
else
{
Console.WriteLine("Use \"--backupId n\" from this list in the square brackets");
using DedupClient client = new DedupClient(endpoint);
await foreach(var item in client.ListBackupsAsync(accessKey))
{
Console.WriteLine($"[{item.Id}] {item.DeviceName} - {item.Tag} ({item.CreationDate.ToString("G")})");
}
}
}
else
{
if((TryGetFirst("backupId",out var backupId) || TryGetFirst("b",out backupId)) && long.TryParse(backupId,out var id))
{
using DedupClient client = new DedupClient(endpoint);
var bkp=await client.GetBackup(accessKey,id);
foreach(var item in bkp.Root.Entries)
{
Console.WriteLine($"[{item.Type.ToString().ToUpper()}] {item.Name}");
}
}
else
{
using DedupClient client = new DedupClient(endpoint);
await foreach(var item in client.ListBackupsAsync(accessKey))
{
Console.WriteLine($"[{item.Id}] {item.DeviceName} - {item.Tag} ({item.CreationDate.ToString("G")})");
}
}
}
}
break;
case "backup":
{
bool silent = false;
if(TryGetFirst("s",out var sltStr) || TryGetFirst("silent",out sltStr))
{
silent = sltStr=="true";
}
string tag="default";
if(TryGetFirst("t",out var t) || TryGetFirst("tag",out t))
{
tag = t;
}
UnixPath path = Special.CurDir;
if(positionalArguments.Count > 0)
{
path = new UnixPath(Path.GetFullPath(positionalArguments[0]));
}
LocalFileSystem fs=new LocalFileSystem();
DedupClient client=new DedupClient(endpoint,!silent);
await client.BackupAsync(accessKey,fs.GetSubdirFilesystem(path),tag);
}
break;
case "backupex":
{
bool silent = false;
if(TryGetFirst("s",out var sltStr) || TryGetFirst("silent",out sltStr))
{
silent = sltStr=="true";
}
string tag="default";
if(TryGetFirst("t",out var t) || TryGetFirst("tag",out t))
{
tag = t;
}
LocalFileSystem fs=new LocalFileSystem();
ZioMountableWrapper wrapper;
if(positionalArguments.Count > 0)
{
var p = new UnixPath(Path.GetFullPath(positionalArguments[0]));
wrapper=ZioMountableWrapper.Create(fs.GetSubdirFilesystem(p),true);
}
else
{
wrapper=ZioMountableWrapper.Create(true);
}
foreach(var bind in GetArgsViaKey("v"))
{
var _bind=bind.Split(new char[]{':'});
if(_bind.Length == 2)
{
if(_bind[0].StartsWith("\\con\\"))
{
string[] path=_bind[0].Replace("\\con\\","").Split(new char[]{'\\'},2,StringSplitOptions.RemoveEmptyEntries);
_bind[0] = $"{path[0]}:\\";
if(path.Length > 1)
{
_bind[0] += path[1];
}
}
wrapper.Mount(UnixPath.FromLocal(_bind[1]),fs.GetSubdirFilesystem(new UnixPath(UnixPath.FromLocal(_bind[0]))));
}
}
foreach(var bind in GetArgsViaKey("volume"))
{
var _bind=bind.Split(new char[]{':'});
if(_bind.Length == 2)
{
if(_bind[0].StartsWith("\\con\\"))
{
string[] path=_bind[0].Replace("\\con\\","").Split(new char[]{'\\'},2,StringSplitOptions.RemoveEmptyEntries);
_bind[0] = $"{path[0]}:\\";
if(path.Length > 1)
{
_bind[0] += path[1];
}
}
wrapper.Mount(UnixPath.FromLocal(_bind[1]),fs.GetSubdirFilesystem(new UnixPath(UnixPath.FromLocal(_bind[0]))));
}
// \con\C\Users\
}
using DedupClient client=new DedupClient(endpoint,!silent);
await client.BackupAsync(accessKey,wrapper,tag);
}
break;
}
internal class LoginConf
{
[JsonProperty("endpoint")]
public string Endpoint {get;set;}="";
[JsonProperty("access_key")]
public string AccessKey {get;set;}="";
}