617 lines
20 KiB
C#
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;}="";
|
|
} |