// SparkleShare, a collaboration and sharing tool. // Copyright (C) 2010 Hylke Bons // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . using System; using System.IO; using System.Threading; namespace Sparkles.Git { public class GitFetcher : SSHFetcher { GitCommand git_clone; SSHAuthenticationInfo auth_info; string password_salt = Path.GetRandomFileName ().SHA256 ().Substring (0, 16); protected override bool IsFetchedRepoEmpty { get { var git_rev_parse = new GitCommand (TargetFolder, "rev-parse HEAD"); git_rev_parse.StartAndWaitForExit (); return (git_rev_parse.ExitCode != 0); } } public GitFetcher (SparkleFetcherInfo fetcher_info, SSHAuthenticationInfo auth_info) : base (fetcher_info) { this.auth_info = auth_info; var uri_builder = new UriBuilder (RemoteUrl); if (!RemoteUrl.Scheme.Equals ("ssh") && !RemoteUrl.Scheme.Equals ("git")) uri_builder.Scheme = "ssh"; if (RemoteUrl.Host.Equals ("github.com") || RemoteUrl.Host.Equals ("gitlab.com")) { AvailableStorageTypes.Add ( new StorageTypeInfo (StorageType.LargeFiles, "Large File Storage", "Trade off versioning to save space;\nkeeps file history on the host only")); uri_builder.Scheme = "ssh"; uri_builder.UserName = "git"; if (!RemoteUrl.AbsolutePath.EndsWith (".git")) uri_builder.Path += ".git"; } else if (string.IsNullOrEmpty (RemoteUrl.UserInfo)) { uri_builder.UserName = "storage"; } RemoteUrl = uri_builder.Uri; AvailableStorageTypes.Add ( new StorageTypeInfo (StorageType.Encrypted, "Encrypted Storage", "Trade off efficiency for privacy;\nencrypts before storing files on the host")); } public override bool Fetch () { if (!base.Fetch ()) return false; StorageType? storage_type = DetermineStorageType (); if (storage_type == null) return false; FetchedRepoStorageType = (StorageType) storage_type; string git_clone_command = "clone --progress --no-checkout"; if (!FetchPriorHistory) git_clone_command += " --depth=1"; if (storage_type == StorageType.LargeFiles) git_clone_command = "lfs clone --progress --no-checkout"; git_clone = new GitCommand (Configuration.DefaultConfiguration.TmpPath, string.Format ("{0} \"{1}\" \"{2}\"", git_clone_command, RemoteUrl, TargetFolder), auth_info); git_clone.StartInfo.RedirectStandardError = true; git_clone.Start (); StreamReader output_stream = git_clone.StandardError; if (FetchedRepoStorageType == StorageType.LargeFiles) output_stream = git_clone.StandardOutput; double percentage = 0; double speed = 0; string information = ""; while (!output_stream.EndOfStream) { string line = output_stream.ReadLine (); ErrorStatus error = GitCommand.ParseProgress (line, out percentage, out speed, out information); if (error != ErrorStatus.None) { IsActive = false; git_clone.Kill (); git_clone.Dispose (); return false; } OnProgressChanged (percentage, speed, information); } git_clone.WaitForExit (); if (git_clone.ExitCode != 0) return false; Thread.Sleep (500); OnProgressChanged (100, 0, ""); Thread.Sleep (500); return true; } public override void Stop () { try { if (git_clone != null && !git_clone.HasExited) { git_clone.Kill (); git_clone.Dispose (); } } catch (Exception e) { Logger.LogInfo ("Fetcher", "Failed to dispose properly", e); } if (Directory.Exists (TargetFolder)) { try { Directory.Delete (TargetFolder, recursive: true); Logger.LogInfo ("Fetcher", "Deleted '" + TargetFolder + "'"); } catch (Exception e) { Logger.LogInfo ("Fetcher", "Failed to delete '" + TargetFolder + "'", e); } } } public override string Complete (StorageType selected_storage_type) { string identifier = base.Complete (selected_storage_type); string identifier_path = Path.Combine (TargetFolder, ".sparkleshare"); InstallConfiguration (); InstallGitLFS (); InstallAttributeRules (); InstallExcludeRules (); if (IsFetchedRepoEmpty) { File.WriteAllText (identifier_path, identifier); File.SetAttributes (identifier_path, FileAttributes.Hidden); // We can't do the "commit --all" shortcut because it doesn't add untracked files var git_add = new GitCommand (TargetFolder, "add .sparkleshare"); var git_commit = new GitCommand (TargetFolder, string.Format ("commit --message=\"{0}\" --author=\"{1}\"", "Set up SparkleShare project", "SparkleShare ")); git_add.StartAndWaitForExit (); git_commit.StartAndWaitForExit (); // These branches will be pushed later by "git push --all" if (selected_storage_type == StorageType.LargeFiles) { var git_branch = new GitCommand (TargetFolder, "branch x-sparkleshare-lfs", auth_info); git_branch.StartAndWaitForExit (); } if (selected_storage_type == StorageType.Encrypted) { var git_branch = new GitCommand (TargetFolder, string.Format ("branch x-sparkleshare-encrypted-{0}", password_salt), auth_info); git_branch.StartAndWaitForExit (); } } else { string branch = "HEAD"; string prefered_branch = "SparkleShare"; // Prefer the "SparkleShare" branch if it exists var git_show_ref = new GitCommand (TargetFolder, "show-ref --verify --quiet refs/heads/" + prefered_branch); git_show_ref.StartAndWaitForExit (); if (git_show_ref.ExitCode == 0) branch = prefered_branch; var git_checkout = new GitCommand (TargetFolder, string.Format ("checkout --quiet --force {0}", branch)); git_checkout.StartAndWaitForExit (); if (File.Exists (identifier_path)) { File.SetAttributes (identifier_path, FileAttributes.Hidden); identifier = File.ReadAllText (identifier_path).Trim (); } } // git-lfs may leave junk behind string git_lfs_tmp_path = Path.Combine (Configuration.DefaultConfiguration.TmpPath, "lfs"); if (Directory.Exists (git_lfs_tmp_path)) Directory.Delete (git_lfs_tmp_path, recursive: true); return identifier; } public override void EnableFetchedRepoCrypto (string password) { string password_file = ".git/info/encryption_password"; var git_config_required = new GitCommand (TargetFolder, "config filter.encryption.required true"); var git_config_smudge = new GitCommand (TargetFolder, "config filter.encryption.smudge " + string.Format ("\"openssl enc -d -aes-256-cbc -base64 -S {0} -pass file:{1} -md sha256\"", password_salt, password_file)); var git_config_clean = new GitCommand (TargetFolder, "config filter.encryption.clean " + string.Format ("\"openssl enc -e -aes-256-cbc -base64 -S {0} -pass file:{1} -md sha256\"", password_salt, password_file)); git_config_required.StartAndWaitForExit (); git_config_smudge.StartAndWaitForExit (); git_config_clean.StartAndWaitForExit (); string git_info_path = Path.Combine (TargetFolder, ".git", "info"); Directory.CreateDirectory (git_info_path); // Store the password, TODO: 600 permissions string password_file_path = Path.Combine (git_info_path, "encryption_password"); File.WriteAllText (password_file_path, password.SHA256 (password_salt)); } public override bool IsFetchedRepoPasswordCorrect (string password) { string password_check_file_path = Path.Combine (TargetFolder, ".sparkleshare"); if (!File.Exists (password_check_file_path)) { var git_show = new GitCommand (TargetFolder, "show HEAD:.sparkleshare"); string output = git_show.StartAndReadStandardOutput (); if (git_show.ExitCode == 0) File.WriteAllText (password_check_file_path, output); else return false; } string args = string.Format ("enc -d -aes-256-cbc -base64 -S {0} -pass pass:{1} -in \"{2}\" -md sha256", password_salt, password.SHA256 (password_salt), password_check_file_path); var process = new Command ("openssl", args); process.StartInfo.WorkingDirectory = TargetFolder; process.StartAndWaitForExit (); if (process.ExitCode == 0) { File.Delete (password_check_file_path); return true; } return false; } public override string FormatName () { string name = Path.GetFileName (RemoteUrl.AbsolutePath); name = name.ReplaceUnderscoreWithSpace (); if (name.EndsWith (".git")) name = name.Replace (".git", ""); return name; } StorageType? DetermineStorageType () { var git_ls_remote = new GitCommand (Configuration.DefaultConfiguration.TmpPath, string.Format ("ls-remote --heads \"{0}\"", RemoteUrl), auth_info); string output = git_ls_remote.StartAndReadStandardOutput (); if (git_ls_remote.ExitCode != 0) return null; if (string.IsNullOrWhiteSpace (output)) return StorageType.Unknown; string encrypted_storage_prefix = "x-sparkleshare-encrypted-"; string large_file_storage_prefix = "x-sparkleshare-lfs"; foreach (string line in output.Split ("\n".ToCharArray ())) { // Remote branches are outputed as "remote/branch", we need the second part string [] line_parts = line.Split ('/'); string branch = line_parts [line_parts.Length - 1]; if (branch == large_file_storage_prefix) return StorageType.LargeFiles; if (branch.StartsWith (encrypted_storage_prefix)) { password_salt = branch.Replace (encrypted_storage_prefix, ""); return StorageType.Encrypted; } } return StorageType.Plain; } void InstallConfiguration () { string [] settings = { "core.autocrlf input", "core.quotepath false", // For commands to output Unicode characters "as is". e.g. '"h\303\251"' becomes 'hé'. "core.precomposeunicode true", // Use the same Unicode form on all filesystems "core.ignorecase false", // Be case sensitive explicitly to work on Mac "core.filemode false", // Ignore permission changes "core.safecrlf false", "core.excludesfile \"\"", "core.packedGitLimit 128m", // Some memory limiting options "core.packedGitWindowSize 128m", "pack.deltaCacheSize 128m", "pack.packSizeLimit 128m", "pack.windowMemory 128m", "push.default matching" }; if (InstallationInfo.OperatingSystem == OS.Windows) settings [0] = "core.autocrlf true"; foreach (string setting in settings) { var git_config = new GitCommand (TargetFolder, "config " + setting); git_config.StartAndWaitForExit (); } } void InstallExcludeRules () { string git_info_path = Path.Combine (TargetFolder, ".git", "info"); Directory.CreateDirectory (git_info_path); string exclude_rules = string.Join (Environment.NewLine, ExcludeRules); string exclude_rules_file_path = Path.Combine (git_info_path, "exclude"); File.WriteAllText (exclude_rules_file_path, exclude_rules); } void InstallAttributeRules () { string git_info_path = Path.Combine (TargetFolder, ".git", "info"); Directory.CreateDirectory (git_info_path); string git_attributes_file_path = Path.Combine (git_info_path, "attributes"); if (FetchedRepoStorageType == StorageType.LargeFiles) { File.WriteAllText (git_attributes_file_path, "* filter=lfs diff=lfs merge=lfs -text"); return; } if (FetchedRepoStorageType == StorageType.Encrypted) { File.WriteAllText (git_attributes_file_path, "* filter=encryption -diff -delta merge=binary"); return; } TextWriter writer = new StreamWriter (git_attributes_file_path); // Treat all files as binary as we always want to keep both file versions on a conflict writer.WriteLine ("* merge=binary"); // Compile a list of files we don't want Git to compress. Not compressing // already compressed files decreases memory usage and increases speed string [] extensions = { "jpg", "jpeg", "png", "tiff", "gif", // Images "flac", "mp3", "ogg", "oga", // Audio "avi", "mov", "mpg", "mpeg", "mkv", "ogv", "ogx", "webm", // Video "zip", "gz", "bz", "bz2", "rpm", "deb", "tgz", "rar", "ace", "7z", "pak", "tc", "iso", ".dmg" // Archives }; foreach (string extension in extensions) { writer.WriteLine ("*." + extension + " -delta merge=binary"); writer.WriteLine ("*." + extension.ToUpper () + " -delta merge=binary"); } writer.Close (); } void InstallGitLFS () { var git_config_required = new GitCommand (TargetFolder, "config filter.lfs.required true"); string GIT_SSH_COMMAND = GitCommand.FormatGitSSHCommand (auth_info); string smudge_command; string clean_command; if (InstallationInfo.OperatingSystem == OS.macOS || InstallationInfo.OperatingSystem == OS.Windows) { smudge_command = "env GIT_SSH_COMMAND='" + GIT_SSH_COMMAND + "' " + Path.Combine (Configuration.DefaultConfiguration.BinPath, "git-lfs").Replace ("\\", "/") + " smudge %f"; clean_command = Path.Combine (Configuration.DefaultConfiguration.BinPath, "git-lfs").Replace ("\\", "/") + " clean %f"; } else { smudge_command = "env GIT_SSH_COMMAND='" + GIT_SSH_COMMAND + "' git-lfs smudge %f"; clean_command = "git-lfs clean %f"; } var git_config_smudge = new GitCommand (TargetFolder, string.Format ("config filter.lfs.smudge \"{0}\"", smudge_command)); var git_config_clean = new GitCommand (TargetFolder, string.Format ("config filter.lfs.clean '{0}'", clean_command)); git_config_required.StartAndWaitForExit (); git_config_clean.StartAndWaitForExit (); git_config_smudge.StartAndWaitForExit (); } } }