From 33e253c8444b67fd5edf333447a29090678b2fb4 Mon Sep 17 00:00:00 2001 From: Julian van de Groep Date: Sun, 14 Oct 2018 01:03:50 +0200 Subject: [PATCH] Initial import (thanks VS) --- BackupManager/BackupManager.csproj | 12 + BackupManager/Config.cs | 25 ++ BackupManager/GoogleDatastore.cs | 78 ++++ BackupManager/Program.cs | 367 ++++++++++++++++++ .../PublishProfiles/FolderProfile.pubxml | 16 + 5 files changed, 498 insertions(+) create mode 100644 BackupManager/BackupManager.csproj create mode 100644 BackupManager/Config.cs create mode 100644 BackupManager/GoogleDatastore.cs create mode 100644 BackupManager/Program.cs create mode 100644 BackupManager/Properties/PublishProfiles/FolderProfile.pubxml diff --git a/BackupManager/BackupManager.csproj b/BackupManager/BackupManager.csproj new file mode 100644 index 0000000..a16cc9d --- /dev/null +++ b/BackupManager/BackupManager.csproj @@ -0,0 +1,12 @@ + + + + Exe + netcoreapp2.1 + + + + + + + diff --git a/BackupManager/Config.cs b/BackupManager/Config.cs new file mode 100644 index 0000000..8b68343 --- /dev/null +++ b/BackupManager/Config.cs @@ -0,0 +1,25 @@ +namespace BackupManager +{ + public class Config + { + public string GoogleClientId { get; set; } + public string GoogleClientSecret { get; set; } + public string GoogleBackupDirectory { get; set; } = @"Backups"; + + // these should not be edited in the xml file + public string GoogleAccessToken { get; set; } + public string GoogleTokenType { get; set; } + public long? GoogleTokenExpires { get; set; } + public string GoogleRefreshToken { get; set; } + public string GoogleTokenIssued { get; set; } + + public string MySqlDumpPathWindows { get; set; } = @"C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqldump.exe"; + public string MySqlDumpPath { get; set; } = @"mysqldump"; + public string MySqlHost { get; set; } = @"localhost"; + public string MySqlUser { get; set; } + public string MySqlPass { get; set; } + public string MySqlDatabases { get; set; } = @"misuzu"; + + public string MisuzuPath { get; set; } + } +} diff --git a/BackupManager/GoogleDatastore.cs b/BackupManager/GoogleDatastore.cs new file mode 100644 index 0000000..57e7711 --- /dev/null +++ b/BackupManager/GoogleDatastore.cs @@ -0,0 +1,78 @@ +using Google.Apis.Auth.OAuth2.Responses; +using Google.Apis.Json; +using Google.Apis.Util.Store; +using Newtonsoft.Json.Linq; +using System; +using System.Text; +using System.Threading.Tasks; + +namespace BackupManager +{ + public sealed class GoogleDatastore : IDataStore + { + public Config Config { get; set; } + + public GoogleDatastore(Config config) + { + Config = config; + } + + public Task GetAsync(string key) + { + TaskCompletionSource tcs = new TaskCompletionSource(); + + TokenResponse tr = new TokenResponse { + AccessToken = Config.GoogleAccessToken, + TokenType = Config.GoogleTokenType, + ExpiresInSeconds = Config.GoogleTokenExpires, + RefreshToken = Config.GoogleRefreshToken, + IssuedUtc = Config.GoogleTokenIssued == null + ? new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) + : DateTime.Parse(Config.GoogleTokenIssued) + }; + + tcs.SetResult((T)(object)tr); // fuck it + return tcs.Task; + } + + public Task DeleteAsync(string key) + => ClearAsync(); + + public Task ClearAsync() + { + Config.GoogleAccessToken = null; + Config.GoogleTokenType = null; + Config.GoogleTokenExpires = null; + Config.GoogleTokenIssued = null; + Config.GoogleTokenIssued = null; + return Task.Delay(0); + } + + public Task StoreAsync(string key, T value) + { + JObject json = JObject.Parse(NewtonsoftJsonSerializer.Instance.Serialize(value)); + + JToken accessToken = json.SelectToken(@"access_token"); + if (accessToken != null) + Config.GoogleAccessToken = (string)accessToken; + + JToken tokenType = json.SelectToken(@"token_type"); + if (tokenType != null) + Config.GoogleTokenType = (string)tokenType; + + JToken expiresIn = json.SelectToken(@"expires_in"); + if (expiresIn != null) + Config.GoogleTokenExpires = (long?)expiresIn; + + JToken refreshToken = json.SelectToken(@"refresh_token"); + if (refreshToken != null) + Config.GoogleRefreshToken = (string)refreshToken; + + JToken tokenIssued = json.SelectToken(@"Issued"); + if (refreshToken != null) + Config.GoogleTokenIssued = (string)tokenIssued; + + return Task.Delay(0); + } + } +} diff --git a/BackupManager/Program.cs b/BackupManager/Program.cs new file mode 100644 index 0000000..32a402b --- /dev/null +++ b/BackupManager/Program.cs @@ -0,0 +1,367 @@ +using Google.Apis.Auth.OAuth2; +using Google.Apis.Drive.v3; +using Google.Apis.Services; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Xml; +using System.Xml.Serialization; +using GFile = Google.Apis.Drive.v3.Data.File; + +namespace BackupManager +{ + public static class Program + { + public readonly static Stopwatch sw = new Stopwatch(); + + private const string FOLDER_MIME = @"application/vnd.google-apps.folder"; + + private const string CONFIG_NAME = @"FlashiiBackupManager.v1.xml"; + + public static bool IsWindows + => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + public readonly static DateTimeOffset Startup = DateTimeOffset.UtcNow; + + public static string Basename + => $@"{Environment.MachineName} {Startup.Year:0000}-{Startup.Month:00}-{Startup.Day:00} {Startup.Hour:00}{Startup.Minute:00}{Startup.Second:00}"; + public static string DatabaseDumpName + => $@"{Basename}.sql.gz"; + public static string UserDataName + => $@"{Basename}.zip"; + + private static Config Config; + private readonly static string ConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.Personal), + CONFIG_NAME + ); + + private static DriveService DriveService; + private static GFile BackupStorage; + + public static bool Headless; + + public static string WindowsToUnixPath(this string path) + { + return IsWindows ? path.Replace('\\', '/') : path; + } + + public static Stream ToXml(this object obj, bool pretty = false) + { + MemoryStream ms = new MemoryStream(); + XmlSerializer xs = new XmlSerializer(obj.GetType()); + + using (XmlWriter xw = XmlWriter.Create(ms, new XmlWriterSettings { Indent = pretty })) + xs.Serialize(xw, obj); + + ms.Seek(0, SeekOrigin.Begin); + return ms; + } + + public static T FromXml(Stream xml) + { + if (xml.CanSeek) + xml.Seek(0, SeekOrigin.Begin); + + XmlSerializer xs = new XmlSerializer(typeof(T)); + return (T)xs.Deserialize(xml); + } + + public static void SaveConfig() + { + Log(@"Saving configuration..."); + using (FileStream fs = new FileStream(ConfigPath, FileMode.Create, FileAccess.Write)) + using (Stream cs = Config.ToXml(true)) + cs.CopyTo(fs); + } + + public static void LoadConfig() + { + Log(@"Loading configuration..."); + using (FileStream fs = File.OpenRead(ConfigPath)) + Config = FromXml(fs); + } + + public static void Main(string[] args) + { + Headless = args.Contains(@"-cron") || args.Contains(@"-headless"); + + Log(@"Flashii Backup Manager"); + sw.Start(); + + if (!File.Exists(ConfigPath)) + { + Config = new Config(); + SaveConfig(); + Error(@"No configuration file exists, created a blank one. Be sure to fill it out properly."); + } + + LoadConfig(); + + UserCredential uc = GoogleAuthenticate( + new ClientSecrets + { + ClientId = Config.GoogleClientId, + ClientSecret = Config.GoogleClientSecret, + }, + new[] { + DriveService.Scope.Drive, + DriveService.Scope.DriveFile, + } + ); + + CreateDriveService(uc); + GetBackupStorage(); + + Log(@"Database backup..."); + + using (Stream s = CreateMySqlDump()) + using (Stream g = GZipEncodeStream(s)) + { + GFile f = Upload(DatabaseDumpName, @"application/sql+gzip", g); + Log($@"MySQL dump uploaded: {f.Name} ({f.Id})"); + } + + if (Directory.Exists(Config.MisuzuPath)) + { + Log(@"Filesystem backup..."); + string mszConfig = GetMisuzuConfig(); + + if (!File.Exists(mszConfig)) + Error(@"Could not find Misuzu config."); + + string mszStore = FindMisuzuStorageDir(mszConfig); + + if (!Directory.Exists(mszStore)) + Error(@"Could not find Misuzu storage directory."); + + string archivePath = CreateMisuzuDataBackup(mszConfig, mszStore); + + using (FileStream fs = File.OpenRead(archivePath)) + { + GFile f = Upload(UserDataName, @"application/zip", fs); + Log($@"Misuzu data uploaded: {f.Name} ({f.Id})"); + } + + File.Delete(archivePath); + } + + SaveConfig(); + sw.Stop(); + Log($@"Done! Took {sw.Elapsed}."); + +#if DEBUG + Console.ReadLine(); +#endif + } + + public static void Log(object line) + { + if (Headless) + return; + + if (sw?.IsRunning == true) + { + ConsoleColor fg = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write(sw.ElapsedMilliseconds.ToString().PadRight(10)); + Console.ForegroundColor = fg; + } + + Console.WriteLine(line); + } + + public static void Error(object line, int exit = 0x00DEAD00) + { + if (!Headless) + { + Console.ForegroundColor = ConsoleColor.Red; + Log(line); + Console.ResetColor(); + } + + Environment.Exit(exit); + } + + public static GFile Upload(string name, string type, Stream stream) + { + Log($@"Uploading '{name}'..."); + FilesResource.CreateMediaUpload request = DriveService.Files.Create(new GFile + { + Name = name, + Parents = new List { + BackupStorage.Id, + }, + }, stream, type); + request.Fields = @"id, name"; + request.Upload(); + return request.ResponseBody; + } + + public static string GetMisuzuConfig() + { + return Path.Combine(Config.MisuzuPath, @"config/config.ini"); + } + + public static string FindMisuzuStorageDir(string config) + { + Log(@"Finding storage directory..."); + + string[] configLines = File.ReadAllLines(config); + bool storageSectionFound = false; + string path = string.Empty; + + foreach (string line in configLines) + { + if (!string.IsNullOrEmpty(path)) + break; + if (line.StartsWith('[')) + storageSectionFound = line == @"[Storage]"; + if (!storageSectionFound) + continue; + + string[] split = line.Split('=', StringSplitOptions.RemoveEmptyEntries); + + if (split.Length < 2 || split[0] != @"path") + continue; + + path = string.Join('=', split.Skip(1)); + break; + } + + if (string.IsNullOrEmpty(path)) + path = Path.Combine(Config.MisuzuPath, @"store"); + + return path; + } + + public static string CreateMisuzuDataBackup(string configPath, string storePath) + { + Log(@"Creating Zip archive containing non-volatile Misuzu data..."); + + string tmpName = Path.GetTempFileName(); + + using (FileStream fs = File.OpenWrite(tmpName)) + using (ZipArchive za = new ZipArchive(fs, ZipArchiveMode.Create)) + { + za.CreateEntryFromFile(configPath, @"config/config.ini", CompressionLevel.Optimal); + + string[] storeFiles = Directory.GetFiles(storePath, @"*", SearchOption.AllDirectories); + + foreach (string file in storeFiles) + za.CreateEntryFromFile( + file, + @"store/" + file.Replace(storePath, string.Empty).WindowsToUnixPath().Trim('/'), + CompressionLevel.Optimal + ); + } + + return tmpName; + } + + public static Stream CreateMySqlDump() + { + Log(@"Dumping MySQL Databases..."); + string tmpFile = Path.GetTempFileName(); + + using (FileStream fs = File.Open(tmpFile, FileMode.Open, FileAccess.ReadWrite)) + using (StreamWriter sw = new StreamWriter(fs)) + { + sw.WriteLine(@"[client]"); + sw.WriteLine($@"user={Config.MySqlUser}"); + sw.WriteLine($@"password={Config.MySqlPass}"); + sw.WriteLine(@"default-character-set=utf8"); + } + + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = IsWindows ? Config.MySqlDumpPathWindows : Config.MySqlDumpPath, + RedirectStandardError = false, + RedirectStandardInput = false, + RedirectStandardOutput = true, + Arguments = $@"--defaults-file={tmpFile} --add-locks -l --order-by-primary -B {Config.MySqlDatabases}", + UseShellExecute = false, + CreateNoWindow = true, + }; + Process p = Process.Start(psi); + + int read; + byte[] buffer = new byte[1024]; + MemoryStream ms = new MemoryStream(); + + while ((read = p.StandardOutput.BaseStream.Read(buffer, 0, buffer.Length)) > 0) + ms.Write(buffer, 0, read); + + p.WaitForExit(); + File.Delete(tmpFile); + ms.Seek(0, SeekOrigin.Begin); + + return ms; + } + + public static Stream GZipEncodeStream(Stream input) + { + Log(@"Compressing stream..."); + MemoryStream output = new MemoryStream(); + + using (GZipStream gz = new GZipStream(output, CompressionLevel.Optimal, true)) + input.CopyTo(gz); + + output.Seek(0, SeekOrigin.Begin); + return output; + } + + public static UserCredential GoogleAuthenticate(ClientSecrets cs, string[] scopes) + { + Log(@"Authenticating with Google..."); + return GoogleWebAuthorizationBroker.AuthorizeAsync( + cs, + scopes, + @"user", + CancellationToken.None, + new GoogleDatastore(Config), + new PromptCodeReceiver() + ).Result; + } + + public static void CreateDriveService(UserCredential uc) + { + Log(@"Creating Google Drive service..."); + DriveService = new DriveService(new BaseClientService.Initializer() + { + HttpClientInitializer = uc, + ApplicationName = @"Flashii Backup Manager", + }); + } + + public static void GetBackupStorage(string name = null) + { + name = name ?? Config.GoogleBackupDirectory; + Log(@"Getting backup folder..."); + FilesResource.ListRequest lr = DriveService.Files.List(); + lr.Q = $@"name = '{name}' and mimeType = '{FOLDER_MIME}'"; + lr.PageSize = 1; + lr.Fields = @"files(id)"; + GFile backupFolder = lr.Execute().Files.FirstOrDefault(); + + if (backupFolder == null) + { + Log(@"Backup folder doesn't exist yet, creating it..."); + FilesResource.CreateRequest dcr = DriveService.Files.Create(new GFile + { + Name = name, + MimeType = FOLDER_MIME, + }); + dcr.Fields = @"id"; + backupFolder = dcr.Execute(); + } + + BackupStorage = backupFolder; + } + } +} diff --git a/BackupManager/Properties/PublishProfiles/FolderProfile.pubxml b/BackupManager/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..c9eff65 --- /dev/null +++ b/BackupManager/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,16 @@ + + + + + FileSystem + Release + Any CPU + netcoreapp2.1 + bin\Release\netcoreapp2.1\publish\ + linux-x64 + true + <_IsPortable>false + + \ No newline at end of file