├── .editorconfig ├── .gitattributes ├── .gitignore ├── .gitlab-ci.yml ├── App.cs ├── Bootstrapper.cs ├── Dockerfile ├── LICENSE ├── Models ├── AppSettings.cs ├── BitbucketRepository.cs ├── GitProvider.cs ├── GithubRepository.cs ├── GitlabRepository.cs └── Repository.cs ├── Program.cs ├── Properties └── AssemblyInfo.cs ├── README.md ├── Services ├── Api │ ├── IBitbucketApi.cs │ ├── IGithubApi.cs │ └── IGitlabApi.cs ├── FullBackupStrategy.cs ├── Git │ ├── BitbucketService.cs │ ├── GithubService.cs │ ├── GitlabService.cs │ └── IGitService.cs ├── Heartbeat │ ├── DeadmansSnitchBeat.cs │ ├── IHeartbeat.cs │ └── NullBeat.cs ├── IBackupStrategy.cs ├── IMailClient.cs ├── IncrementalBackupStrategy.cs ├── MailClient.cs └── TokenGenerator.cs ├── app-settings.json ├── fusonic-git-backup.csproj ├── fusonic-git-backup.sln ├── git-askpass.cmd └── git-askpass.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | # Rules in this file were initially inferred by Visual Studio IntelliCode from the C:\Projects\github\fusonic-git-backup codebase based on best match to current usage at 29/11/2021 2 | # You can modify the rules from these initially generated values to suit your own policies 3 | # You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 4 | [*.cs] 5 | 6 | 7 | #Core editorconfig formatting - indentation 8 | 9 | #use soft tabs (spaces) for indentation 10 | indent_style = space 11 | 12 | #Formatting - new line options 13 | 14 | #place catch statements on a new line 15 | csharp_new_line_before_catch = true 16 | #place else statements on a new line 17 | csharp_new_line_before_else = true 18 | #require members of object intializers to be on separate lines 19 | csharp_new_line_before_members_in_object_initializers = true 20 | #require braces to be on a new line for control_blocks, types, lambdas, object_collection_array_initializers, and methods (also known as "Allman" style) 21 | csharp_new_line_before_open_brace = control_blocks, types, lambdas, object_collection_array_initializers, methods 22 | #require elements of query expression clauses to be on separate lines 23 | csharp_new_line_between_query_expression_clauses = true 24 | 25 | #Formatting - organize using options 26 | 27 | #sort System.* using directives alphabetically, and place them before other usings 28 | dotnet_sort_system_directives_first = true 29 | 30 | #Formatting - spacing options 31 | 32 | #require NO space between a cast and the value 33 | csharp_space_after_cast = false 34 | #require a space before the colon for bases or interfaces in a type declaration 35 | csharp_space_after_colon_in_inheritance_clause = true 36 | #require a space after a keyword in a control flow statement such as a for loop 37 | csharp_space_after_keywords_in_control_flow_statements = true 38 | #require a space before the colon for bases or interfaces in a type declaration 39 | csharp_space_before_colon_in_inheritance_clause = true 40 | #remove space within empty argument list parentheses 41 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 42 | #remove space between method call name and opening parenthesis 43 | csharp_space_between_method_call_name_and_opening_parenthesis = false 44 | #do not place space characters after the opening parenthesis and before the closing parenthesis of a method call 45 | csharp_space_between_method_call_parameter_list_parentheses = false 46 | #remove space within empty parameter list parentheses for a method declaration 47 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 48 | #place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. 49 | csharp_space_between_method_declaration_parameter_list_parentheses = false 50 | 51 | #Formatting - wrapping options 52 | 53 | #leave code block on single line 54 | csharp_preserve_single_line_blocks = true 55 | 56 | #Style - Code block preferences 57 | 58 | #prefer no curly braces if allowed 59 | csharp_prefer_braces = false:suggestion 60 | 61 | #Style - expression bodied member options 62 | 63 | #prefer expression-bodied members for constructors 64 | csharp_style_expression_bodied_constructors = true:suggestion 65 | #prefer expression-bodied members for methods 66 | csharp_style_expression_bodied_methods = true:suggestion 67 | #prefer expression-bodied members for properties 68 | csharp_style_expression_bodied_properties = true:suggestion 69 | 70 | #Style - expression level options 71 | 72 | #prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them 73 | dotnet_style_predefined_type_for_member_access = true:suggestion 74 | 75 | #Style - Expression-level preferences 76 | 77 | #prefer objects to be initialized using object initializers when possible 78 | dotnet_style_object_initializer = true:suggestion 79 | 80 | #Style - implicit and explicit types 81 | 82 | #prefer var over explicit type in all cases, unless overridden by another code style rule 83 | csharp_style_var_elsewhere = true:suggestion 84 | #prefer var is used to declare variables with built-in system types such as int 85 | csharp_style_var_for_built_in_types = true:suggestion 86 | #prefer var when the type is already mentioned on the right-hand side of a declaration expression 87 | csharp_style_var_when_type_is_apparent = true:suggestion 88 | 89 | #Style - language keyword and framework type options 90 | 91 | #prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them 92 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 93 | 94 | #Style - Miscellaneous preferences 95 | 96 | #prefer anonymous functions over local functions 97 | csharp_style_pattern_local_over_anonymous_function = false:suggestion 98 | 99 | #Style - modifier options 100 | 101 | #prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. 102 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion 103 | 104 | #Style - Modifier preferences 105 | 106 | #when this rule is set to a list of modifiers, prefer the specified ordering. 107 | csharp_preferred_modifier_order = public,private,internal,readonly,static,async,override:suggestion 108 | 109 | #Style - qualification options 110 | 111 | #prefer fields not to be prefaced with this. or Me. in Visual Basic 112 | dotnet_style_qualification_for_field = false:suggestion 113 | #prefer properties not to be prefaced with this. or Me. in Visual Basic 114 | dotnet_style_qualification_for_property = false:suggestion 115 | csharp_style_namespace_declarations=file_scoped:warning 116 | 117 | # RCS1090: Add call to 'ConfigureAwait' (or vice versa). 118 | dotnet_diagnostic.RCS1090.severity = none 119 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | #IDE 5 | .idea/ 6 | *.sln 7 | app-settings.overwrite.json 8 | out 9 | GitBackup 10 | 11 | # Visual Studio Code files 12 | .vscode/ 13 | 14 | # User-specific files 15 | *.suo 16 | *.user 17 | *.userosscache 18 | *.sln.docstates 19 | 20 | # User-specific files (MonoDevelop/Xamarin Studio) 21 | *.userprefs 22 | 23 | # Build results 24 | [Dd]ebug/ 25 | [Dd]ebugPublic/ 26 | [Rr]elease/ 27 | [Rr]eleases/ 28 | [Xx]64/ 29 | [Xx]86/ 30 | [Bb]uild/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | 35 | # Visual Studio 2015 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # MSTest test Results 41 | [Tt]est[Rr]esult*/ 42 | [Bb]uild[Ll]og.* 43 | 44 | # NUNIT 45 | *.VisualState.xml 46 | TestResult.xml 47 | 48 | # Build Results of an ATL Project 49 | [Dd]ebugPS/ 50 | [Rr]eleasePS/ 51 | dlldata.c 52 | 53 | # DNX 54 | project.lock.json 55 | artifacts/ 56 | 57 | *_i.c 58 | *_p.c 59 | *_i.h 60 | *.ilk 61 | *.meta 62 | *.obj 63 | *.pch 64 | *.pdb 65 | *.pgc 66 | *.pgd 67 | *.rsp 68 | *.sbr 69 | *.tlb 70 | *.tli 71 | *.tlh 72 | *.tmp 73 | *.tmp_proj 74 | *.log 75 | *.vspscc 76 | *.vssscc 77 | .builds 78 | *.pidb 79 | *.svclog 80 | *.scc 81 | 82 | # Chutzpah Test files 83 | _Chutzpah* 84 | 85 | # Visual C++ cache files 86 | ipch/ 87 | *.aps 88 | *.ncb 89 | *.opendb 90 | *.opensdf 91 | *.sdf 92 | *.cachefile 93 | *.VC.db 94 | 95 | # Visual Studio profiler 96 | *.psess 97 | *.vsp 98 | *.vspx 99 | *.sap 100 | 101 | # TFS 2012 Local Workspace 102 | $tf/ 103 | 104 | # Guidance Automation Toolkit 105 | *.gpState 106 | 107 | # ReSharper is a .NET coding add-in 108 | _ReSharper*/ 109 | *.[Rr]e[Ss]harper 110 | *.DotSettings.user 111 | 112 | # JustCode is a .NET coding add-in 113 | .JustCode 114 | 115 | # TeamCity is a build add-in 116 | _TeamCity* 117 | 118 | # DotCover is a Code Coverage Tool 119 | *.dotCover 120 | 121 | # NCrunch 122 | _NCrunch_* 123 | .*crunch*.local.xml 124 | nCrunchTemp_* 125 | 126 | # MightyMoose 127 | *.mm.* 128 | AutoTest.Net/ 129 | 130 | # Web workbench (sass) 131 | .sass-cache/ 132 | 133 | # Installshield output folder 134 | [Ee]xpress/ 135 | 136 | # DocProject is a documentation generator add-in 137 | DocProject/buildhelp/ 138 | DocProject/Help/*.HxT 139 | DocProject/Help/*.HxC 140 | DocProject/Help/*.hhc 141 | DocProject/Help/*.hhk 142 | DocProject/Help/*.hhp 143 | DocProject/Help/Html2 144 | DocProject/Help/html 145 | 146 | # Click-Once directory 147 | publish/ 148 | 149 | # Publish Web Output 150 | *.[Pp]ublish.xml 151 | *.azurePubxml 152 | 153 | # TODO: Un-comment the next line if you do not want to checkin 154 | # your web deploy settings because they may include unencrypted 155 | # passwords 156 | #*.pubxml 157 | *.publishproj 158 | 159 | # NuGet Packages 160 | *.nupkg 161 | # The packages folder can be ignored because of Package Restore 162 | **/packages/* 163 | # except build/, which is used as an MSBuild target. 164 | !**/packages/build/ 165 | # Uncomment if necessary however generally it will be regenerated when needed 166 | #!**/packages/repositories.config 167 | # NuGet v3's project.json files produces more ignoreable files 168 | *.nuget.props 169 | *.nuget.targets 170 | 171 | # Microsoft Azure Build Output 172 | csx/ 173 | *.build.csdef 174 | 175 | # Microsoft Azure Emulator 176 | ecf/ 177 | rcf/ 178 | 179 | # Windows Store app package directory 180 | AppPackages/ 181 | BundleArtifacts/ 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | [Ss]tyle[Cc]op.* 192 | ~$* 193 | *~ 194 | *.dbmdl 195 | *.dbproj.schemaview 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # RIA/Silverlight projects 202 | Generated_Code/ 203 | 204 | # Backup & report files from converting an old project file 205 | # to a newer Visual Studio version. Backup files are not needed, 206 | # because we have git ;-) 207 | _UpgradeReport_Files/ 208 | 209 | UpgradeLog*.XML 210 | UpgradeLog*.htm 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # LightSwitch generated files 244 | GeneratedArtifacts/ 245 | ModelManifest.xml 246 | 247 | # Paket dependency manager 248 | .paket/paket.exe 249 | 250 | # FAKE - F# Make 251 | .fake/ 252 | /Pioneer.Console.Boilerplate/nuget.exe 253 | /src/package-lock.json 254 | /src/Pioneer.Console.Boilerplate.Template/nuget.exe 255 | /src/nuget.exe -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - build_image 4 | 5 | variables: 6 | DOCKER_HOST: tcp://docker:2376 7 | DOCKER_TLS_CERTDIR: "/certs/${CI_JOB_ID}" 8 | DOCKER_TLS_VERIFY: 1 9 | DOCKER_CERT_PATH: "/certs/${CI_JOB_ID}/client" 10 | DOCKER_VERSION: "20.03" 11 | 12 | build: 13 | image: mcr.microsoft.com/dotnet/sdk:6.0 14 | stage: build 15 | before_script: 16 | - dotnet restore 17 | script: 18 | - dotnet publish -c Release -o out 19 | artifacts: 20 | paths: 21 | - out 22 | expire_in: 1 hour 23 | 24 | build_image: 25 | image: docker:git 26 | stage: build_image 27 | services: 28 | - docker:dind 29 | script: 30 | - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY 31 | - docker build -t $CI_REGISTRY_IMAGE . 32 | - docker push $CI_REGISTRY_IMAGE:latest 33 | - docker login -u $DOCKER_HUB_USER -p $DOCKER_HUB_PW 34 | - docker tag $CI_REGISTRY_IMAGE:latest fusonic/git-backup:latest 35 | - docker push fusonic/git-backup:latest 36 | only: 37 | - main -------------------------------------------------------------------------------- /App.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks.Dataflow; 2 | using Fusonic.GitBackup.Models; 3 | using Fusonic.GitBackup.Services; 4 | using Fusonic.GitBackup.Services.Git; 5 | using Fusonic.GitBackup.Services.Heartbeat; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Fusonic.GitBackup; 9 | 10 | internal class App 11 | { 12 | private readonly IEnumerable gitServices; 13 | private readonly IBackupStrategy backupRunner; 14 | private readonly ILogger logger; 15 | private readonly IHeartbeat heartbeat; 16 | private readonly AppSettings settings; 17 | 18 | public App(IEnumerable gitServices, IBackupStrategy backupRunner, ILogger logger, IHeartbeat heartbeat, AppSettings settings) 19 | { 20 | this.gitServices = gitServices; 21 | this.backupRunner = backupRunner; 22 | this.logger = logger; 23 | this.heartbeat = heartbeat; 24 | this.settings = settings; 25 | } 26 | 27 | public async Task Run() 28 | { 29 | var current = 0; 30 | var pending = 0; 31 | var repositoryCount = 0; 32 | 33 | var fetchBlock = new TransformManyBlock( 34 | async service => 35 | { 36 | var friendlyServiceName = service.Provider.ToString(); 37 | logger.LogInformation($"Fetching repositories from {friendlyServiceName} ..."); 38 | 39 | var providerSettings = settings.Git.Where(x => x.Type == service.Provider); 40 | var repositories = await service.GetRepositoryUrisAsync(providerSettings); 41 | if (providerSettings.Any() && repositories.Count == 0) 42 | throw new InvalidOperationException($"{friendlyServiceName} API returned 0 repositories. Please check your credentials."); 43 | 44 | Interlocked.Add(ref repositoryCount, repositories.Count); 45 | logger.LogInformation($"Fetched {repositories.Count} repositories from {friendlyServiceName}. ({repositoryCount} total repositories to backup) "); 46 | 47 | return repositories; 48 | }, new ExecutionDataflowBlockOptions() { BoundedCapacity = 1000, MaxDegreeOfParallelism = settings.Backup.MaxDegreeOfParallelism }); 49 | 50 | var mirrorBlock = new ActionBlock( 51 | async repository => 52 | { 53 | Interlocked.Increment(ref current); 54 | Interlocked.Increment(ref pending); 55 | 56 | logger.LogInformation($"Cloning {current} of {repositoryCount} ({pending} running) ({repository.HttpsUrl})"); 57 | await backupRunner.Backup(repository); 58 | logger.LogInformation($"Finished {repository.HttpsUrl} ({pending} pending)"); 59 | 60 | Interlocked.Decrement(ref pending); 61 | }, new ExecutionDataflowBlockOptions() { BoundedCapacity = 1000, MaxDegreeOfParallelism = settings.Backup.MaxDegreeOfParallelism, }); 62 | 63 | fetchBlock.LinkTo(mirrorBlock, new DataflowLinkOptions() { PropagateCompletion = true }); 64 | 65 | foreach (var service in gitServices) 66 | { 67 | await fetchBlock.SendAsync(service); 68 | } 69 | 70 | fetchBlock.Complete(); 71 | await mirrorBlock.Completion; 72 | 73 | await backupRunner.Cleanup(); 74 | await heartbeat.Notify(); 75 | 76 | logger.LogInformation("Done."); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Bootstrapper.cs: -------------------------------------------------------------------------------- 1 | using Fusonic.GitBackup.Models; 2 | using Fusonic.GitBackup.Services; 3 | using Fusonic.GitBackup.Services.Api; 4 | using Fusonic.GitBackup.Services.Git; 5 | using Fusonic.GitBackup.Services.Heartbeat; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | using RestEase; 10 | using SimpleInjector; 11 | using SimpleInjector.Lifestyles; 12 | 13 | namespace Fusonic.GitBackup; 14 | 15 | internal static class Bootstrapper 16 | { 17 | internal static Container CreateContainer() 18 | { 19 | var container = new Container() 20 | { 21 | Options = 22 | { 23 | DefaultScopedLifestyle = new AsyncScopedLifestyle(), 24 | DefaultLifestyle = Lifestyle.Scoped, 25 | } 26 | }; 27 | container.Collection.Register(new[] 28 | { 29 | typeof(BitbucketService), 30 | typeof(GitlabService), 31 | typeof(GithubService) 32 | }); 33 | 34 | var serviceCollection = new ServiceCollection(); 35 | serviceCollection.AddLogging(builder => builder.AddConsole()); 36 | 37 | serviceCollection.AddSimpleInjector(container, action => action.AddLogging()); 38 | 39 | var serviceProvider = serviceCollection.BuildServiceProvider(); 40 | serviceProvider.UseSimpleInjector(container); 41 | 42 | var builder = new ConfigurationBuilder() 43 | .SetBasePath(AppContext.BaseDirectory) 44 | .AddJsonFile("app-settings.json", false) 45 | .AddJsonFile("app-settings.overwrite.json", true) 46 | .AddUserSecrets(); 47 | 48 | var configuration = builder.Build(); 49 | 50 | var settings = new AppSettings(); 51 | configuration.Bind(settings); 52 | container.RegisterInstance(settings); 53 | 54 | container.Register(() => () => RestClient.For("https://gitlab.com/api/v4")); 55 | container.Register(() => () => RestClient.For("https://api.github.com")); 56 | container.Register(() => () => RestClient.For("https://api.bitbucket.org/2.0")); 57 | 58 | container.Register(typeof(IHeartbeat), () => 59 | !string.IsNullOrEmpty(settings.DeadmanssnitchUrl) 60 | ? new DeadmansSnitchBeat(settings.DeadmanssnitchUrl, container.GetInstance()) 61 | : (IHeartbeat)new NullBeat()); 62 | 63 | container.Register(typeof(IBackupStrategy), 64 | settings.Backup.Local.Strategy == AppSettings.LocalSettings.BackupStrategy.Full 65 | ? typeof(FullBackupStrategy) 66 | : typeof(IncrementalBackupStrategy)); 67 | 68 | container.Register(); 69 | container.Verify(); 70 | return container; 71 | } 72 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env 2 | WORKDIR /src 3 | COPY . ./ 4 | RUN dotnet publish -c Release -o /app 5 | 6 | FROM mcr.microsoft.com/dotnet/runtime:6.0 7 | WORKDIR /app 8 | 9 | RUN apt-get update \ 10 | && apt-get install -y --no-install-recommends git \ 11 | && rm -rf /tmp/* /var/tmp/* /var/lib/apt/lists/* /usr/share/man/ || true 12 | 13 | COPY --from=build-env /app . 14 | ENTRYPOINT ["dotnet", "fusonic-git-backup.dll"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Fusonic GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Models/AppSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Fusonic.GitBackup.Models; 2 | 3 | public class AppSettings 4 | { 5 | public GitSettings[] Git { get; set; } = new GitSettings[0]; 6 | public BackupSettings Backup { get; set; } 7 | public MailSettings Mail { get; set; } 8 | public string DeadmanssnitchUrl { get; set; } 9 | 10 | public class GitSettings 11 | { 12 | public GitProvider Type { get; set; } 13 | public string Username { get; set; } 14 | public string PersonalAccessToken { get; set; } 15 | 16 | public override string ToString() => Type.ToString(); 17 | } 18 | 19 | public class BackupSettings 20 | { 21 | public int MaxDegreeOfParallelism { get; set; } 22 | public LocalSettings Local { get; set; } 23 | } 24 | 25 | public class LocalSettings 26 | { 27 | public int DeleteAfterDays { get; set; } 28 | public string Destination { get; set; } 29 | public BackupStrategy Strategy { get; set; } 30 | 31 | public enum BackupStrategy 32 | { 33 | Incremental, 34 | Full 35 | } 36 | } 37 | 38 | public class MailSettings 39 | { 40 | public string Host { get; set; } 41 | public int Port { get; set; } 42 | public bool UseSsl { get; set; } 43 | public string Username { get; set; } 44 | public string Password { get; set; } 45 | public MailUserSettings Sender { get; set; } 46 | public MailUserSettings Receiver { get; set; } 47 | } 48 | 49 | public class MailUserSettings 50 | { 51 | public string Name { get; set; } 52 | public string Address { get; set; } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Models/BitbucketRepository.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Fusonic.GitBackup.Models; 4 | 5 | public class BitbucketRepository 6 | { 7 | [JsonProperty("values")] 8 | public List Values { get; set; } 9 | 10 | [JsonProperty("next")] 11 | public string Next { get; set; } 12 | 13 | public class ValuesProperty 14 | { 15 | [JsonProperty("links")] 16 | public LinksProperty Links { get; set; } 17 | 18 | [JsonProperty("full_name")] 19 | public string Name { get; set; } 20 | } 21 | 22 | public class LinksProperty 23 | { 24 | [JsonProperty("clone")] 25 | public List Clone { get; set; } 26 | } 27 | 28 | public class CloneProperty 29 | { 30 | [JsonProperty("href")] 31 | public string Href { get; set; } 32 | 33 | [JsonProperty("name")] 34 | public string Name { get; set; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Models/GitProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Fusonic.GitBackup; 2 | 3 | public enum GitProvider 4 | { 5 | Bitbucket, 6 | Github, 7 | Gitlab 8 | } 9 | -------------------------------------------------------------------------------- /Models/GithubRepository.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Fusonic.GitBackup.Models; 4 | 5 | public class GithubRepository 6 | { 7 | [JsonProperty("clone_url")] 8 | public string HttpsUrl { get; set; } 9 | 10 | [JsonProperty("full_name")] 11 | public string Name { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /Models/GitlabRepository.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Fusonic.GitBackup.Models; 4 | 5 | public class GitlabRepository 6 | { 7 | [JsonProperty("http_url_to_repo")] 8 | public string HttpsUrl { get; set; } 9 | 10 | [JsonProperty("path_with_namespace")] 11 | public string Name { get; set; } 12 | 13 | [JsonProperty("default_branch")] 14 | public string DefaultBranch { get; set; } 15 | 16 | public override string ToString() 17 | => $"{Name}"; 18 | } 19 | -------------------------------------------------------------------------------- /Models/Repository.cs: -------------------------------------------------------------------------------- 1 | namespace Fusonic.GitBackup.Models; 2 | 3 | public class Repository 4 | { 5 | public string HttpsUrl { get; set; } 6 | public GitProvider Provider { get; set; } 7 | public string Name { get; set; } 8 | public string Username { get; set; } 9 | public string PersonalAccessToken { get; set; } 10 | 11 | public override string ToString() 12 | => $"{Provider.ToString()}: {Name}"; 13 | } 14 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using Fusonic.GitBackup; 2 | using Fusonic.GitBackup.Models; 3 | using Fusonic.GitBackup.Services; 4 | using MailKit.Net.Smtp; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Logging; 7 | using SimpleInjector; 8 | using SimpleInjector.Lifestyles; 9 | 10 | Container container = null; 11 | 12 | try 13 | { 14 | container = Bootstrapper.CreateContainer(); 15 | using (AsyncScopedLifestyle.BeginScope(container)) 16 | { 17 | await container.GetService().Run(); 18 | } 19 | } 20 | catch (Exception ex) when (container != null) 21 | { 22 | var sentMail = SendErrorMail(ex); 23 | using (AsyncScopedLifestyle.BeginScope(container)) 24 | { 25 | var logger = container.GetService(); 26 | if (sentMail) 27 | { 28 | logger.LogError("An error occured. An email with detailed error message has been sent to the address defined in the app-settings.json."); 29 | } 30 | logger.LogError(ex.ToString()); 31 | } 32 | } 33 | 34 | bool SendErrorMail(Exception ex) 35 | { 36 | using (AsyncScopedLifestyle.BeginScope(container)) 37 | { 38 | var settings = container.GetService(); 39 | if (string.IsNullOrEmpty(settings.Mail.Host)) 40 | return false; 41 | 42 | using var client = new SmtpClient(); 43 | client.Connect(settings.Mail.Host, settings.Mail.Port, settings.Mail.UseSsl); 44 | client.Authenticate(settings.Mail.Username, settings.Mail.Password); 45 | 46 | var mailClient = new MailClient(settings, client); 47 | mailClient.SendMail("Git-Backup Fatal Error!", ex.ToString()); 48 | return true; 49 | } 50 | } -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("Pioneer Code.")] 9 | [assembly: AssemblyProduct("fusonic-git-backup")] 10 | [assembly: AssemblyTrademark("")] 11 | 12 | // Setting ComVisible to false makes the types in this assembly not visible 13 | // to COM components. If you need to access a type in this assembly from 14 | // COM, set the ComVisible attribute to true on that type. 15 | [assembly: ComVisible(false)] 16 | 17 | // The following GUID is for the ID of the typelib if this project is exposed to COM 18 | [assembly: Guid("d1c4ab83-c553-4e3b-8e75-c9e76498206b")] 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fusonic-Git-Backup 2 | 3 | ## What is Fusonic-Git-Backup? 4 | Fusonic-Git-Backup is a dotnet core based tool to backup your github, gitlab and bitbucket 5 | repositories in one run. 6 | 7 | ## Features 8 | * Full Docker support 9 | * Email notification in error case 10 | * Configure the tool individually in the app-settings.json 11 | * Backup your repos with "git clone --mirror" to ensure the full backup of the repos with all branches 12 | * Rebuild a fully functional repository from your backup with a simple "git clone" 13 | * Automaticaly builds a clear backup folder structure for you. (date/org/repopath/files) 14 | 15 | ## Setup 16 | ### Config File 17 | If you want to run the tool inside the docker container, set the "Destination" to "/app/GitBackup". 18 | In the projects root folder you can find the app-settings.json to setup the tool for your needs. 19 | Enter the following user specific information and replace the placeholders to execute the tool properly: 20 | 21 | { 22 | "Git": [ 23 | { 24 | "Type": "Bitbucket", 25 | "Username": "Bituser123", 26 | "PersonalAccessToken": "YourPrivateAccessToken" 27 | }, 28 | { 29 | "Type": "Github", 30 | "Username": "Bituser123", 31 | "PersonalAccessToken": "YourPrivateAccessToken" 32 | }, 33 | { 34 | "Type": "Gitlab", 35 | "Username": "YourUser1", 36 | "PersonalAccessToken": "YourPrivateAccessToken" 37 | }, 38 | { 39 | "Type": "Gitlab", 40 | "Username": "YourUser2", 41 | "PersonalAccessToken": "YourPrivateAccessToken" 42 | } 43 | ], 44 | "Backup": { 45 | "Local": { 46 | "DeleteAfterDays": "1", 47 | "Destination": "/my/absoulte/path/to/backup/directory" 48 | } 49 | }, 50 | "Mail": { 51 | "Host": "mailhoster.net", 52 | "Port": "25", 53 | "Username": "your.email@mail.com", 54 | "Password": "yourpassword", 55 | "UseSsl": "false", 56 | "Sender": { 57 | "Address": "your.email@mail.com", 58 | "Name": "Max Musterman" 59 | }, 60 | "Receiver": { 61 | "Address": "Receiver.email@mail.com", 62 | "Name": "Manuel Musterman" 63 | } 64 | }, 65 | "DeadmanssnitchUrl": "You can delete this if you dont use a dead man snitch" 66 | } 67 | 68 | * Git: In this section you can configure your Gitlab, Github and Bitbucket account. If you dont 69 | need one or two of the connections, simply remove them from the config. You can configure as many 70 | users as you want. 71 | * Type: Enter the type of your Account here. It should be either "Bitbucket", "Github" or 72 | "Gitlab". 73 | * Username: Enter the username of your account here. You can find your username in your user 74 | settings on each platform. 75 | * PersonalAccessToken: On each platform the "PersonalAccessToken" is named differently. 76 | You need the generate a "PersonalAccessToken" for each platform to establish a connection 77 | with the Git-Api and to grant the right permissions to your backup account. Generate your 78 | passwords on their websites. Read permissions for repos should be enough. 79 | * Bitbucket: Go to "user settings -> App password" to generate your password. 80 | * Gitlab: Go to "user settings -> Access Tokens" to generate a token. Watch out for 81 | the expire date. Gitlab is the only platform that lets the token expire after a given 82 | time. Renew your token when your token expires. You can select the date by your own. 83 | * Github: Go to "user settings -> Personal access tokens" to generate a token. 84 | * Backup: Here you can configure you backup settings. 85 | * Local: Configure to store your backups directly on your machine. 86 | * DeleteAfterDays: Tells after how many days an old backup should be deleted 87 | automaticaly. 88 | * Destination: The path to the folder where the backups should be stored. If you use Docker, 89 | this sould be set to /app/GitBackup 90 | * Mail: Setup the Mailserver to send messages in error case. Get all the informations about 91 | your mail settings from your mail server provider. For example see the gmail settings: 92 | https://support.google.com/a/answer/176600?hl=en 93 | * Host: The address, IP or Url, to you mail server host. 94 | * Port: The prefered port. 95 | * Username: On gmail for example, its your email address 96 | * Password: The password of your email account 97 | * UseSsl: Tells if you want to use a ssh connection (needed if you use port 465) 98 | * Sender: Information about the sender of the emails: 99 | * Address: Address of the sender 100 | * Name: Name of the sender 101 | * Receiver: Information about the Receiver of the emails: 102 | * Address: Address of the Receiver 103 | * Name: Name of the Receiver 104 | * DeadmanssnitchUrl: Paste your uri to your deadmansnitch here to call it whenever the backup succeeds. 105 | 106 | ## Run GitBackup locally 107 | To run your tool localy you need to follow three steps: 108 | 109 | ### Install dotnet and git 110 | Install dotnet and git on your machine to run your tool. 111 | 112 | ### Execute the tool 113 | Now after configuring ssh, the app-settings.json and a dotnet environment you are all done and 114 | can try out the tool by running the following command in the commandline in your tools directory 115 | 116 | > cd /path/to/tool 117 | > dotnet run 118 | 119 | You can either use the run command to run it or create a release folder to directly execute your 120 | compiled dll file. This will store your dll's in the out folder. 121 | 122 | > cd /path/to/tool 123 | > dotnet publish git-backup.sln -c Release -o out 124 | 125 | ### Local Cron-Job 126 | To run your backup tool as often as you want, install a cronjob, that executes dotnet run or your 127 | compiled dll file. With your dll file it will look something like this: 128 | 129 | @midnight dotnet /path/to/tool/out/fusonic-git-backup.dll 130 | 131 | ## Run GitBackup with Docker 132 | You can build the docker image on you own but the fastest way to run your tool with docker is to 133 | download the already built image from the Git repository and execute the downloaded docker image 134 | as follows: 135 | 136 | docker run --rm -v /ABSOLUTE/PATH/TO/BACKUP/DEST:/backup -v /ABSOLUTE/PATH/TO/YOUR/CUSTOMCONFIG/config.json:/app/app-settings.json fusonic/git-backup:latest 137 | 138 | * /ABSOLUTEPATH/TO/BACKUP/DEST => Set your absolute path to the destination path on your real machine here. This path 139 | will be mounted to the /app/GitBackup witch is the working path of the tool inside the docker container. 140 | * /ABSOLUTE/PATH/TO/YOUR/CUSTOMCONFIG => You need to mount your app-settings.json to tell the file what it has 141 | to do. 142 | 143 | ### Docker cron job 144 | To execute the tool and create backups after regular time spans, implement a cron job with the docker run comand from above. 145 | 146 | ## Restore a Backup 147 | To restore a backup in a new repo simply clone the git repo from the backup location. 148 | 149 | ## Deadman Snitch 150 | Configure a Deaman Snitch to ensure your backup program is running as expected. The Deadman Snitch will wait for a life signal in a configurable period of time. 151 | The tool will send a request to satisfy the Snitch whenever it gets executed successfully. If the tool fails it can not send a request to satisfy the Snitch in the configured 152 | time period and the snitch will send you an email with an error alert. For example this can be useful if the full server shuts down and you wont get any other error messages, 153 | since the tool will not even execute. 154 | 155 | ## FAQ 156 | ### How often does the projects create backups? 157 | Run the tool every day to get a daily backup, or as often as you want. For example with a cron-job. 158 | The program will create a full backup, each time it gets executed. 159 | 160 | ### How do I know if a buckup failed? 161 | If a backup fails, an email will be sent to the address configured in the app-settings.json file. 162 | The email includes the full exception message of the error to serve you all the information you need 163 | to debug the tool. 164 | 165 | --------------- Happy backuping! --------------- -------------------------------------------------------------------------------- /Services/Api/IBitbucketApi.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using Fusonic.GitBackup.Models; 3 | using RestEase; 4 | 5 | namespace Fusonic.GitBackup.Services.Api; 6 | 7 | [Header("User-Agent", "FusonicGitBackup")] 8 | public interface IBitbucketApi 9 | { 10 | [Header("Authorization")] 11 | AuthenticationHeaderValue Authorization { get; set; } 12 | 13 | [Get("repositories")] 14 | Task GetRepositoriesAsync([RawQueryString] string after, [Query("role")] string role); 15 | 16 | [Get("repositories")] 17 | Task GetRepositoriesAsync([Query("role")] string role); 18 | } 19 | -------------------------------------------------------------------------------- /Services/Api/IGithubApi.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using Fusonic.GitBackup.Models; 3 | using RestEase; 4 | 5 | namespace Fusonic.GitBackup.Services.Api; 6 | 7 | [Header("User-Agent", "FusonicGitBackup")] 8 | public interface IGithubApi 9 | { 10 | [Header("Authorization")] 11 | AuthenticationHeaderValue Authorization { get; set; } 12 | 13 | [Get("user/repos")] 14 | Task>> GetRepositoriesAsync([RawQueryString] string perPage); 15 | } 16 | -------------------------------------------------------------------------------- /Services/Api/IGitlabApi.cs: -------------------------------------------------------------------------------- 1 | using Fusonic.GitBackup.Models; 2 | using RestEase; 3 | 4 | namespace Fusonic.GitBackup.Services.Api; 5 | 6 | [Header("User-Agent", "FusonicGitBackup")] 7 | public interface IGitlabApi 8 | { 9 | [Header("PRIVATE-TOKEN")] 10 | string PrivateToken { get; set; } 11 | 12 | [Get("projects")] 13 | Task>> GetRepositoriesAsync([RawQueryString] string perPage); 14 | } 15 | -------------------------------------------------------------------------------- /Services/FullBackupStrategy.cs: -------------------------------------------------------------------------------- 1 | using CliWrap; 2 | using CliWrap.Buffered; 3 | using Fusonic.GitBackup.Models; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Fusonic.GitBackup.Services; 7 | 8 | internal class FullBackupStrategy : IBackupStrategy 9 | { 10 | private readonly AppSettings settings; 11 | private readonly ILogger logger; 12 | private readonly string timestampFolderName; 13 | 14 | public FullBackupStrategy(AppSettings settings, ILogger logger) 15 | { 16 | this.settings = settings; 17 | this.logger = logger; 18 | timestampFolderName = DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss"); 19 | } 20 | 21 | public async Task Backup(Repository repository) 22 | { 23 | await Cli.Wrap("git") 24 | .WithArguments($"clone --mirror {repository.HttpsUrl} {settings.Backup.Local.Destination}/{timestampFolderName}/{repository.Provider}/{repository.Name}") 25 | .WithValidation(CommandResultValidation.ZeroExitCode) 26 | .ExecuteBufferedAsync(); 27 | } 28 | 29 | public Task Cleanup() 30 | { 31 | var deleteAfterDays = settings.Backup.Local.DeleteAfterDays; 32 | logger.LogInformation($"Deleting backups older than {deleteAfterDays}"); 33 | 34 | return Task.Run(() => 35 | { 36 | foreach (var dir in Directory.EnumerateDirectories(settings.Backup.Local.Destination) 37 | .Where(x => DateTime.Now - Directory.GetLastWriteTime(x) 38 | > TimeSpan.FromDays(deleteAfterDays))) 39 | { 40 | Directory.Delete(dir, true); 41 | } 42 | 43 | logger.LogInformation("Old backups deleted"); 44 | }); 45 | } 46 | } -------------------------------------------------------------------------------- /Services/Git/BitbucketService.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using Fusonic.GitBackup.Models; 3 | using Fusonic.GitBackup.Services.Api; 4 | using static Fusonic.GitBackup.Models.AppSettings; 5 | 6 | namespace Fusonic.GitBackup.Services.Git; 7 | 8 | internal class BitbucketService : IGitService 9 | { 10 | private readonly Func apiFactory; 11 | 12 | public BitbucketService(Func apiFactory) => this.apiFactory = apiFactory; 13 | 14 | public GitProvider Provider => GitProvider.Bitbucket; 15 | 16 | public async Task> GetRepositoryUrisAsync(IEnumerable settings) 17 | { 18 | var allRepositories = new List(); 19 | foreach (var gitSetting in settings) 20 | { 21 | var api = apiFactory(); 22 | api.Authorization = new AuthenticationHeaderValue("Basic", TokenGenerator.GenerateBase64Token(gitSetting.Username, gitSetting.PersonalAccessToken)); 23 | 24 | var after = ""; 25 | while (after != null) 26 | { 27 | BitbucketRepository resp; 28 | if (after != "") 29 | resp = await api.GetRepositoriesAsync(after, "member"); 30 | else 31 | resp = await api.GetRepositoriesAsync("member"); 32 | 33 | allRepositories.AddRange(from v in resp.Values 34 | from c in v.Links.Clone 35 | where c.Name == "https" 36 | select new Repository() 37 | { 38 | HttpsUrl = c.Href, 39 | Provider = GitProvider.Bitbucket, 40 | Name = v.Name, 41 | Username = gitSetting.Username, 42 | PersonalAccessToken = gitSetting.PersonalAccessToken 43 | }); 44 | if (resp.Next != null) 45 | after = "?" + resp.Next.Split(new string[] { "?", "&" }, StringSplitOptions.None)[1]; 46 | else 47 | after = null; 48 | } 49 | } 50 | return allRepositories; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Services/Git/GithubService.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using System.Text.RegularExpressions; 3 | using Fusonic.GitBackup.Models; 4 | using Fusonic.GitBackup.Services.Api; 5 | using static Fusonic.GitBackup.Models.AppSettings; 6 | 7 | namespace Fusonic.GitBackup.Services.Git; 8 | 9 | internal class GithubService : IGitService 10 | { 11 | private readonly Func apiFactory; 12 | private readonly Regex regex = new Regex(@"<(.*)\?(.*)>; rel=""next""", RegexOptions.CultureInvariant); 13 | 14 | public GithubService(Func apiFactory) => this.apiFactory = apiFactory; 15 | 16 | public GitProvider Provider => GitProvider.Github; 17 | 18 | public async Task> GetRepositoryUrisAsync(IEnumerable settings) 19 | { 20 | var repositories = new List(); 21 | foreach (var gitSetting in settings) 22 | { 23 | var api = apiFactory(); 24 | api.Authorization = new AuthenticationHeaderValue("Basic", TokenGenerator.GenerateBase64Token(gitSetting.Username, gitSetting.PersonalAccessToken)); 25 | 26 | var nextPage = "?per_page=1000"; 27 | while (!string.IsNullOrEmpty(nextPage)) 28 | { 29 | var response = await api.GetRepositoriesAsync(nextPage); 30 | 31 | repositories.AddRange(response.GetContent().Select(x => new Repository() 32 | { 33 | HttpsUrl = x.HttpsUrl, 34 | Provider = GitProvider.Github, 35 | Name = x.Name, 36 | Username = gitSetting.Username, 37 | PersonalAccessToken = gitSetting.PersonalAccessToken 38 | })); 39 | 40 | if (response.ResponseMessage.Headers.Contains("Link")) 41 | { 42 | nextPage = response.ResponseMessage.Headers.GetValues("Link").FirstOrDefault(); 43 | var match = regex.Match(nextPage); 44 | if (match.Success) 45 | nextPage = "?" + match.Groups[2].Value; 46 | else 47 | nextPage = null; 48 | } 49 | else 50 | nextPage = null; 51 | } 52 | } 53 | return repositories; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Services/Git/GitlabService.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Fusonic.GitBackup.Models; 3 | using Fusonic.GitBackup.Services.Api; 4 | using static Fusonic.GitBackup.Models.AppSettings; 5 | 6 | namespace Fusonic.GitBackup.Services.Git; 7 | 8 | internal class GitlabService : IGitService 9 | { 10 | private readonly Func apiFactory; 11 | private readonly Regex regex = new Regex(@"<(.*)\?(.*)>; rel=""next""", RegexOptions.CultureInvariant); 12 | 13 | public GitlabService(Func apiFactory) 14 | { 15 | this.apiFactory = apiFactory; 16 | } 17 | 18 | public GitProvider Provider => GitProvider.Gitlab; 19 | 20 | public async Task> GetRepositoryUrisAsync(IEnumerable settings) 21 | { 22 | var repositories = new List(); 23 | foreach (var gitSetting in settings) 24 | { 25 | var api = apiFactory(); 26 | api.PrivateToken = gitSetting.PersonalAccessToken; 27 | 28 | var nextPage = "?per_page=1000&membership=true"; 29 | while (!string.IsNullOrEmpty(nextPage)) 30 | { 31 | var response = await api.GetRepositoriesAsync(nextPage); 32 | var responseContent = response.GetContent(); 33 | repositories.AddRange(responseContent 34 | .Where(x => x.DefaultBranch != null) 35 | .Select(x => new Repository() 36 | { 37 | HttpsUrl = x.HttpsUrl.Replace("//", "//gitlab-ci-token@"), 38 | Provider = GitProvider.Gitlab, 39 | Name = x.Name, 40 | Username = gitSetting.Username, 41 | PersonalAccessToken = gitSetting.PersonalAccessToken 42 | })); 43 | 44 | nextPage = response.ResponseMessage.Headers.GetValues("Link").FirstOrDefault(); 45 | var match = regex.Match(nextPage); 46 | if (match.Success) 47 | nextPage = "?" + match.Groups[2].Value; 48 | else 49 | nextPage = null; 50 | } 51 | } 52 | return repositories; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Services/Git/IGitService.cs: -------------------------------------------------------------------------------- 1 | using Fusonic.GitBackup.Models; 2 | using static Fusonic.GitBackup.Models.AppSettings; 3 | 4 | namespace Fusonic.GitBackup.Services.Git; 5 | 6 | public interface IGitService 7 | { 8 | GitProvider Provider { get; } 9 | Task> GetRepositoryUrisAsync(IEnumerable settings); 10 | } 11 | -------------------------------------------------------------------------------- /Services/Heartbeat/DeadmansSnitchBeat.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace Fusonic.GitBackup.Services.Heartbeat; 4 | 5 | internal class DeadmansSnitchBeat : IHeartbeat 6 | { 7 | private readonly string url; 8 | private readonly ILogger logger; 9 | 10 | public DeadmansSnitchBeat(string url, ILogger logger) 11 | { 12 | this.url = url; 13 | this.logger = logger; 14 | } 15 | 16 | public async Task Notify() 17 | { 18 | logger.LogInformation($"Sending Heartbeat to {url} ..."); 19 | using (var client = new HttpClient()) 20 | { 21 | await client.GetStringAsync(url); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Services/Heartbeat/IHeartbeat.cs: -------------------------------------------------------------------------------- 1 | namespace Fusonic.GitBackup.Services.Heartbeat; 2 | 3 | public interface IHeartbeat 4 | { 5 | Task Notify(); 6 | } 7 | -------------------------------------------------------------------------------- /Services/Heartbeat/NullBeat.cs: -------------------------------------------------------------------------------- 1 | namespace Fusonic.GitBackup.Services.Heartbeat; 2 | 3 | internal class NullBeat : IHeartbeat 4 | { 5 | public Task Notify() => Task.CompletedTask; 6 | } 7 | -------------------------------------------------------------------------------- /Services/IBackupStrategy.cs: -------------------------------------------------------------------------------- 1 | using Fusonic.GitBackup.Models; 2 | 3 | namespace Fusonic.GitBackup.Services; 4 | 5 | interface IBackupStrategy 6 | { 7 | Task Backup(Repository repository); 8 | Task Cleanup(); 9 | } 10 | -------------------------------------------------------------------------------- /Services/IMailClient.cs: -------------------------------------------------------------------------------- 1 | namespace Fusonic.GitBackup.Services; 2 | 3 | public interface IMailClient 4 | { 5 | void SendMail(string subject, string body); 6 | } 7 | -------------------------------------------------------------------------------- /Services/IncrementalBackupStrategy.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using CliWrap; 3 | using CliWrap.Buffered; 4 | using Fusonic.GitBackup.Models; 5 | 6 | namespace Fusonic.GitBackup.Services; 7 | 8 | internal class IncrementalBackupStrategy : IBackupStrategy 9 | { 10 | private readonly AppSettings settings; 11 | 12 | public IncrementalBackupStrategy(AppSettings settings) 13 | => this.settings = settings; 14 | 15 | public async Task Backup(Repository repository) 16 | { 17 | var path = $"{settings.Backup.Local.Destination}/{repository.Provider}/{repository.Name}"; 18 | var cmd = Directory.Exists(path) 19 | ? $"--git-dir={path} remote update" 20 | : $"clone --mirror {repository.HttpsUrl} {path}"; 21 | 22 | await Cli.Wrap("git") 23 | .WithArguments(cmd) 24 | .WithEnvironmentVariables(env => env 25 | .Set("GIT_BACKUP_ACCESS_TOKEN", repository.PersonalAccessToken) 26 | .Set("GIT_ASKPASS", Path.GetFullPath("git-askpass" + (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".cmd" : ".sh")))) 27 | .WithValidation(CommandResultValidation.ZeroExitCode) 28 | .ExecuteBufferedAsync(); 29 | } 30 | 31 | public Task Cleanup() => Task.CompletedTask; // No cleanup when incrementally backuping 32 | } -------------------------------------------------------------------------------- /Services/MailClient.cs: -------------------------------------------------------------------------------- 1 | using Fusonic.GitBackup.Models; 2 | using MailKit.Net.Smtp; 3 | using MimeKit; 4 | 5 | namespace Fusonic.GitBackup.Services; 6 | 7 | internal class MailClient : IMailClient 8 | { 9 | private readonly AppSettings settings; 10 | private readonly SmtpClient client; 11 | 12 | public MailClient(AppSettings settings, SmtpClient client) 13 | { 14 | this.settings = settings; 15 | this.client = client; 16 | } 17 | 18 | public void SendMail(string subject, string body) 19 | { 20 | var message = new MimeMessage(); 21 | message.From.Add(new MailboxAddress(settings.Mail.Sender.Name, settings.Mail.Sender.Address)); 22 | message.To.Add(new MailboxAddress(settings.Mail.Receiver.Name, settings.Mail.Receiver.Address)); 23 | message.Subject = subject; 24 | message.Body = new TextPart("plain") { Text = body }; 25 | 26 | client.Send(message); 27 | client.Disconnect(true); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Services/TokenGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Fusonic.GitBackup.Services; 4 | 5 | internal static class TokenGenerator 6 | { 7 | public static string GenerateBase64Token(string username, string password) 8 | { 9 | return Convert.ToBase64String(Encoding.ASCII.GetBytes(username + ":" + password)); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Git": [ 3 | // Uncomment services to backup: 4 | //{ 5 | // "Type": "Bitbucket", 6 | // "Username": "", 7 | // "PersonalAccessToken": "" 8 | //}, 9 | //{ 10 | // "Type": "Github", 11 | // "Username": "", 12 | // "PersonalAccessToken": "" 13 | //}, 14 | //{ 15 | // "Type": "Gitlab", 16 | // "PersonalAccessToken": "" 17 | //} 18 | ], 19 | "Backup": { 20 | "MaxDegreeOfParallelism": 8, 21 | "Local": { 22 | "DeleteAfterDays": 1, 23 | "Destination": "/app/GitBackup", 24 | "BackupStrategy": "Incremental" 25 | } 26 | }, 27 | "Mail": { 28 | "Host": "", 29 | "Port": 25, 30 | "Username": "", 31 | "Password": "", 32 | "UseSsl": "false", 33 | "Sender": { 34 | "Address": "", 35 | "Name": "" 36 | }, 37 | "Receiver": { 38 | "Address": "", 39 | "Name": "" 40 | } 41 | }, 42 | "DeadmanssnitchUrl": "" 43 | } -------------------------------------------------------------------------------- /fusonic-git-backup.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | fusonic-git-backup 5 | Exe 6 | fusonic-git-backup 7 | false 8 | false 9 | false 10 | true 11 | 7c038160-caba-4295-bf29-f8ca2cbf333c 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | PreserveNewest 39 | 40 | 41 | PreserveNewest 42 | 43 | 44 | -------------------------------------------------------------------------------- /fusonic-git-backup.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31912.275 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fusonic-git-backup", "fusonic-git-backup.csproj", "{F6E977BF-7B3A-4EF7-8E1B-979918106096}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C8023BB9-1C29-4D1D-ABD8-30CA449F8E0B}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {F6E977BF-7B3A-4EF7-8E1B-979918106096}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {F6E977BF-7B3A-4EF7-8E1B-979918106096}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {F6E977BF-7B3A-4EF7-8E1B-979918106096}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {F6E977BF-7B3A-4EF7-8E1B-979918106096}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {BDDD73DC-88CF-460A-86F5-C5BC2B4E6FA6} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /git-askpass.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo %GIT_BACKUP_ACCESS_TOKEN% -------------------------------------------------------------------------------- /git-askpass.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec echo $GIT_BACKUP_ACCESS_TOKEN --------------------------------------------------------------------------------