using System.Diagnostics; using System.Net; using Newtonsoft.Json; using Tesses.VirtualFilesystem; using Tesses.VirtualFilesystem.Filesystems; using TessesDedup; string verb = ""; List> newArgs=new List>(); List positionalArguments =new List(); 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(File.ReadAllText(conf)); if(res != null) { endpoint = res.Endpoint; accessKey = res.AccessKey; } } } IEnumerable 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(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;}=""; }