33using System . Linq ;
44using System . Threading . Tasks ;
55using Octokit ;
6+ using UniGetUI . Core . Data ;
67using UniGetUI . Core . Logging ;
78using UniGetUI . Core . SettingsEngine ;
89
910namespace UniGetUI . Services
1011{
1112 public class GitHubBackupService
1213 {
14+ private const string GistDescriptionEndingKey = "#[UNIGETUI_BUNDLE_BACKUP_V1]" ;
1315 private readonly GitHubAuthService _authService ;
14- private const string GistDescription = "UniGetUI Backup" ;
16+ private const string GistDescription = $ "UniGetUI package backups - DO NOT RENAME OR MODIFY { GistDescriptionEndingKey } ";
17+
18+ private const string ReadMeContents = "" +
19+ "This special Gist is used by UniGetUI to store your package backups. \n " +
20+ "Please DO NOT EDIT the contents or the description of this gist, or unexpected behaviours may occur.\n " +
21+ "Learn more about UniGetUI at https://github.com/marticliment/UniGetUI\n " ;
22+
23+
24+ private readonly string DeviceUserUniqueIdentifier ;
25+ private readonly string GistFileKey ;
1526
1627 public GitHubBackupService ( GitHubAuthService authService )
1728 {
1829 _authService = authService ;
30+ DeviceUserUniqueIdentifier = $ "{ Environment . MachineName } \\ { Environment . UserName } ";
31+ GistFileKey = $ "{ DeviceUserUniqueIdentifier } ";
1932 }
2033
21- private async Task < GitHubClient ? > GetAuthenticatedClientAsync ( )
34+ private async Task < GitHubClient ? > CreateClientAsync ( )
2235 {
2336 var token = await _authService . GetAccessTokenAsync ( ) ;
2437
@@ -27,85 +40,92 @@ public GitHubBackupService(GitHubAuthService authService)
2740 Logger . Error ( "GitHub access token is not available. Cannot perform Gist operation." ) ;
2841 return null ;
2942 }
30- return new GitHubClient ( new ProductHeaderValue ( "UniGetUI" ) )
43+
44+ return new GitHubClient ( new ProductHeaderValue ( "UniGetUI" , CoreData . VersionName ) )
3145 {
3246 Credentials = new Credentials ( token )
3347 } ;
3448 }
3549
36- public async Task < bool > BackupAsync ( Dictionary < string , string > filesToBackup )
50+ /// <summary>
51+ /// Assuming authentication is set up, upload the given bundleContents to GitHub
52+ /// </summary>
53+ /// <param name="bundleContents"></param>
54+ /// <returns>A boolean representing the success of the operation</returns>
55+ public async Task < bool > UploadPackageBundle ( string bundleContents )
3756 {
38- var client = await GetAuthenticatedClientAsync ( ) ;
39- if ( client == null ) return false ;
57+ var GHClient = await CreateClientAsync ( ) ;
58+ if ( GHClient == null )
59+ {
60+ Logger . Error ( "Upload of backup has been aborted since the user is not authenticated" ) ;
61+ return false ;
62+ }
63+ User user = await GHClient . User . Current ( ) ;
4064
4165 try
4266 {
43- var gistId = Settings . GetValue ( Settings . K . GitHubGistId ) ;
44- Gist gistToUpdate = null ;
45-
46- if ( ! string . IsNullOrEmpty ( gistId ) )
47- {
48- try
49- {
50- gistToUpdate = await client . Gist . Get ( gistId ) ;
51- Logger . Info ( $ "Found existing Gist with ID: { gistId } for update.") ;
52- }
53- catch ( NotFoundException )
54- {
55- Logger . Warn ( $ "Previously stored Gist ID { gistId } not found. Will create a new Gist.") ;
56- Settings . SetValue ( Settings . K . GitHubGistId , "" ) ;
57- gistId = null ;
58- }
59- catch ( Exception ex )
60- {
61- Logger . Error ( $ "Error fetching Gist ID { gistId } : { ex . Message } ") ;
62- Settings . SetValue ( Settings . K . GitHubGistId , "" ) ;
63- gistId = null ;
64- }
65- }
67+ var candidates = await GHClient . Gist . GetAllForUser ( user . Login ) ;
68+ Gist ? existingBackup = candidates . FirstOrDefault ( g => g . Description . EndsWith ( GistDescriptionEndingKey ) ) ;
6669
67- if ( gistToUpdate != null )
70+ if ( existingBackup is null )
6871 {
69- var update = new GistUpdate
70- {
71- Description = GistDescription
72- } ;
73- foreach ( var file in filesToBackup )
74- {
75- update . Files [ file . Key ] = new GistFileUpdate { Content = file . Value } ;
76- }
77- await client . Gist . Edit ( gistId , update ) ;
78- Logger . Info ( $ "Successfully updated Gist ID: { gistId } ") ;
72+ Logger . Warn ( $ "No matching gist was found as a valid backup, a new gist will be created...") ;
73+ existingBackup = await _createBackupGistAsync ( GHClient ) ;
7974 }
80- else
81- {
82- var newGist = new NewGist
83- {
84- Description = GistDescription ,
85- Public = false
86- } ;
87- foreach ( var file in filesToBackup )
88- {
89- newGist . Files . Add ( file . Key , file . Value ) ;
90- }
9175
92- var createdGist = await client . Gist . Create ( newGist ) ;
93- Settings . SetValue ( Settings . K . GitHubGistId , createdGist . Id ) ;
94- Logger . Info ( $ "Successfully created new Gist ID: { createdGist . Id } ") ;
95- }
76+ await _updateBackupGistAsync ( GHClient , existingBackup , bundleContents ) ;
77+ Logger . Info ( $ "Cloud backup completed successfully to gist { user . Login } /{ existingBackup . Id } ") ;
9678 return true ;
9779 }
9880 catch ( Exception ex )
9981 {
100- Logger . Error ( "Failed to backup to GitHub Gist :" ) ;
82+ Logger . Error ( "An error occurred while attempting to upload backup to GitHub:" ) ;
10183 Logger . Error ( ex ) ;
10284 return false ;
10385 }
10486 }
10587
88+ /// <summary>
89+ /// Upload the given payload to the given gist.
90+ /// Updates the existing file if GistFileKey exists, creates a new one otherwhise
91+ /// </summary>
92+ /// <param name="client"></param>
93+ /// <param name="gist"></param>
94+ /// <param name="payload"></param>
95+ private async Task _updateBackupGistAsync ( GitHubClient client , Gist gist , string payload )
96+ {
97+ var update = new GistUpdate { Description = GistDescription } ;
98+ if ( update . Files . ContainsKey ( GistFileKey ) )
99+ {
100+ update . Files [ GistFileKey ] = new GistFileUpdate { Content = payload } ;
101+ }
102+ else
103+ {
104+ update . Files . Add ( GistFileKey , new GistFileUpdate { Content = payload } ) ;
105+ }
106+ await client . Gist . Edit ( gist . Id , update ) ;
107+ Logger . Info ( $ "Successfully updated Gist ID: { gist . Id } ") ;
108+ }
109+
110+ /// <summary>
111+ /// Creates a new Gist, prepared to be detectable by UniGetUI
112+ /// </summary>
113+ /// <param name="client"></param>
114+ /// <returns></returns>
115+ private static Task < Gist > _createBackupGistAsync ( GitHubClient client )
116+ {
117+ var newGist = new NewGist
118+ {
119+ Description = GistDescription ,
120+ Public = false ,
121+ } ;
122+ newGist . Files . Add ( "- UniGetUI Package Backups" , ReadMeContents ) ;
123+ return client . Gist . Create ( newGist ) ;
124+ }
125+
106126 public async Task < string ? > RetrieveFileAsync ( string fileName )
107127 {
108- var client = await GetAuthenticatedClientAsync ( ) ;
128+ var client = await CreateClientAsync ( ) ;
109129 if ( client == null ) return null ;
110130
111131 string fileContent = null ;
@@ -131,21 +151,21 @@ public async Task<bool> BackupAsync(Dictionary<string, string> filesToBackup)
131151 catch ( NotFoundException )
132152 {
133153 Logger . Warn ( $ "Stored Gist ID { gistId } not found. Will try to find by description.") ;
134- Settings . SetValue ( Settings . K . GitHubGistId , "" ) ;
135- gistId = null ;
154+ Settings . SetValue ( Settings . K . GitHubGistId , "" ) ;
155+ gistId = null ;
136156 }
137157 catch ( Exception ex )
138158 {
139159 Logger . Error ( $ "Error fetching Gist ID { gistId } : { ex . Message } . Will try to find by description.") ;
140160 Settings . SetValue ( Settings . K . GitHubGistId , "" ) ;
141- gistId = null ;
161+ gistId = null ;
142162 }
143163 }
144164
145- if ( fileContent == null )
165+ if ( fileContent == null )
146166 {
147167 Logger . Info ( "Attempting to find settings Gist by description..." ) ;
148- var gists = await client . Gist . GetAll ( ) ;
168+ var gists = await client . Gist . GetAll ( ) ;
149169 var settingsGist = gists . FirstOrDefault ( g => g . Description == GistDescription && g . Files . ContainsKey ( fileName ) ) ;
150170
151171 if ( settingsGist != null )
@@ -154,7 +174,7 @@ public async Task<bool> BackupAsync(Dictionary<string, string> filesToBackup)
154174 if ( fullGist . Files . TryGetValue ( fileName , out var file ) && file != null )
155175 {
156176 fileContent = file . Content ;
157- Settings . SetValue ( Settings . K . GitHubGistId , fullGist . Id ) ;
177+ Settings . SetValue ( Settings . K . GitHubGistId , fullGist . Id ) ;
158178 Logger . Info ( $ "Found settings Gist by description. ID: { fullGist . Id } ") ;
159179 }
160180 else
@@ -165,7 +185,7 @@ public async Task<bool> BackupAsync(Dictionary<string, string> filesToBackup)
165185 else
166186 {
167187 Logger . Warn ( $ "No UniGetUI settings Gist found for the user with file { fileName } .") ;
168- return null ;
188+ return null ;
169189 }
170190 }
171191
@@ -185,4 +205,4 @@ public async Task<bool> BackupAsync(Dictionary<string, string> filesToBackup)
185205 }
186206 }
187207 }
188- }
208+ }
0 commit comments