22// Licensed under the MIT License. See the LICENSE.
33
44using Microsoft . Data . Sqlite ;
5+ using Microsoft . Extensions . Logging ;
6+ using Microsoft . Win32 ;
57using System . IO ;
68using Windows . Storage ;
9+ using Vanara . Windows . Shell ;
710
811namespace Files . App . Utils . Cloud
912{
1013 /// <summary>
11- /// Provides an utility for Google Drive Cloud detection.
14+ /// Provides a utility for Google Drive Cloud detection.
1215 /// </summary>
1316 public sealed class GoogleDriveCloudDetector : AbstractCloudDetector
1417 {
18+ private static readonly ILogger _logger = Ioc . Default . GetRequiredService < ILogger < App > > ( ) ;
19+
20+ private const string _googleDriveRegKeyName = @"Software\Google\DriveFS" ;
21+ private const string _googleDriveRegValName = "PerAccountPreferences" ;
22+ private const string _googleDriveRegValPropName = "value" ;
23+ private const string _googleDriveRegValPropPropName = "mount_point_path" ;
24+
1525 protected override async IAsyncEnumerable < ICloudProvider > GetProviders ( )
1626 {
1727 // Google Drive's sync database can be in a couple different locations. Go find it.
@@ -56,7 +66,8 @@ await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(Path.Combine(a
5666 var folder = await StorageFolder . GetFolderFromPathAsync ( path ) ;
5767 string title = reader [ "title" ] ? . ToString ( ) ?? folder . Name ;
5868
59- App . AppModel . GoogleDrivePath = path ;
69+ Debug . WriteLine ( "YIELD RETURNING from `GoogleDriveCloudDetector.GetProviders()` (roots): " ) ;
70+ Debug . WriteLine ( $ "Name: Google Drive ({ title } ); SyncFolder: { path } ") ;
6071
6172 yield return new CloudProvider ( CloudProviders . GoogleDrive )
6273 {
@@ -65,6 +76,7 @@ await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(Path.Combine(a
6576 } ;
6677 }
6778
79+ var iconFile = await GetGoogleDriveIconFileAsync ( ) ;
6880 // Google virtual drive
6981 reader = cmdMedia . ExecuteReader ( ) ;
7082
@@ -74,13 +86,14 @@ await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(Path.Combine(a
7486 if ( string . IsNullOrWhiteSpace ( path ) )
7587 continue ;
7688
89+ if ( ! AddMyDriveToPathAndValidate ( ref path ) )
90+ continue ;
91+
7792 var folder = await StorageFolder . GetFolderFromPathAsync ( path ) ;
7893 string title = reader [ "name" ] ? . ToString ( ) ?? folder . Name ;
79- string iconPath = Path . Combine ( Environment . GetEnvironmentVariable ( "ProgramFiles" ) , "Google" , "Drive File Stream" , "drive_fs.ico" ) ;
80-
81- App . AppModel . GoogleDrivePath = path ;
8294
83- StorageFile iconFile = await FilesystemTasks . Wrap ( ( ) => StorageFile . GetFileFromPathAsync ( iconPath ) . AsTask ( ) ) ;
95+ Debug . WriteLine ( "YIELD RETURNING from `GoogleDriveCloudDetector.GetProviders` (media): " ) ;
96+ Debug . WriteLine ( $ "Name: { title } ; SyncFolder: { path } ") ;
8497
8598 yield return new CloudProvider ( CloudProviders . GoogleDrive )
8699 {
@@ -89,6 +102,190 @@ await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(Path.Combine(a
89102 IconData = iconFile is not null ? await iconFile . ToByteArrayAsync ( ) : null ,
90103 } ;
91104 }
105+
106+ await Inspect ( database , "SELECT * FROM roots" , "root_preferences db, roots table" ) ;
107+ await Inspect ( database , "SELECT * FROM media" , "root_preferences db, media table" ) ;
108+ await Inspect ( database , "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY 1" , "root_preferences db, all tables" ) ;
109+
110+ var registryPath = App . AppModel . GoogleDrivePath ;
111+ if ( ! AddMyDriveToPathAndValidate ( ref registryPath ) )
112+ yield break ;
113+ yield return new CloudProvider ( CloudProviders . GoogleDrive )
114+ {
115+ Name = "Google Drive" ,
116+ SyncFolder = registryPath ,
117+ IconData = iconFile is not null ? await iconFile . ToByteArrayAsync ( ) : null
118+ } ;
119+ }
120+
121+ private static async Task Inspect ( SqliteConnection database , string sqlCommand , string targetDescription )
122+ {
123+ await using var cmdTablesAll = new SqliteCommand ( sqlCommand , database ) ;
124+ var reader = await cmdTablesAll . ExecuteReaderAsync ( ) ;
125+ var colNamesList = Enumerable . Range ( 0 , reader . FieldCount ) . Select ( i => reader . GetName ( i ) ) . ToList ( ) ;
126+
127+ Debug . WriteLine ( $ "BEGIN LOGGING of { targetDescription } ") ;
128+
129+ for ( int rowIdx = 0 ; reader . Read ( ) is not false ; rowIdx ++ )
130+ {
131+ var colVals = new object [ reader . FieldCount ] ;
132+ reader . GetValues ( colVals ) ;
133+
134+ colVals . Select ( ( val , colIdx ) => $ "row { rowIdx } : column { colIdx } : { colNamesList [ colIdx ] } : { val } ")
135+ . ToList ( ) . ForEach ( s => Debug . WriteLine ( s ) ) ;
136+ }
137+
138+ Debug . WriteLine ( $ "END LOGGING of { targetDescription } contents") ;
139+ }
140+
141+ private static JsonDocument ? GetGoogleDriveRegValJson ( )
142+ {
143+ // This will be null if the key name is not found.
144+ using var googleDriveRegKey = Registry . CurrentUser . OpenSubKey ( _googleDriveRegKeyName ) ;
145+
146+ if ( googleDriveRegKey is null )
147+ {
148+ _logger . LogWarning ( $ "Google Drive registry key for key name '{ _googleDriveRegKeyName } ' not found.") ;
149+ return null ;
150+ }
151+
152+ var googleDriveRegVal = googleDriveRegKey . GetValue ( _googleDriveRegValName ) ;
153+
154+ if ( googleDriveRegVal is null )
155+ {
156+ _logger . LogWarning ( $ "Google Drive registry value for value name '{ _googleDriveRegValName } ' not found.") ;
157+ return null ;
158+ }
159+
160+ JsonDocument ? googleDriveRegValueJson = null ;
161+ try
162+ {
163+ googleDriveRegValueJson = JsonDocument . Parse ( googleDriveRegVal . ToString ( ) ?? "" ) ;
164+ }
165+ catch ( JsonException je )
166+ {
167+ _logger . LogWarning ( je , $ "Google Drive registry value for value name '{ _googleDriveRegValName } ' could not be parsed as a JsonDocument.") ;
168+ }
169+
170+ return googleDriveRegValueJson ;
171+ }
172+
173+ public static string ? GetRegistryBasePath ( )
174+ {
175+ var googleDriveRegValJson = GetGoogleDriveRegValJson ( ) ;
176+
177+ if ( googleDriveRegValJson is null )
178+ return null ;
179+
180+ var googleDriveRegValJsonProperty = googleDriveRegValJson
181+ . RootElement . EnumerateObject ( )
182+ . FirstOrDefault ( ) ;
183+
184+ // A default JsonProperty struct has an "Undefined" Value#ValueKind and throws an
185+ // error if you try to call EnumerateArray on its Value.
186+ if ( googleDriveRegValJsonProperty . Value . ValueKind == JsonValueKind . Undefined )
187+ {
188+ _logger . LogWarning ( $ "Root element of Google Drive registry value for value name '{ _googleDriveRegValName } ' was empty.") ;
189+ return null ;
190+ }
191+
192+ Debug . WriteLine ( "REGISTRY LOGGING" ) ;
193+ Debug . WriteLine ( googleDriveRegValJsonProperty . ToString ( ) ) ;
194+
195+ var item = googleDriveRegValJsonProperty . Value . EnumerateArray ( ) . FirstOrDefault ( ) ;
196+ if ( item . ValueKind == JsonValueKind . Undefined )
197+ {
198+ _logger . LogWarning ( $ "Array in the root element of Google Drive registry value for value name '{ _googleDriveRegValName } ' was empty.") ;
199+ return null ;
200+ }
201+
202+ if ( ! item . TryGetProperty ( _googleDriveRegValPropName , out var googleDriveRegValProp ) )
203+ {
204+ _logger . LogWarning ( $ "First element in the Google Drive Registry Root Array did not have property named { _googleDriveRegValPropName } ") ;
205+ return null ;
206+ }
207+
208+ if ( ! googleDriveRegValProp . TryGetProperty ( _googleDriveRegValPropPropName , out var googleDriveRegValPropProp ) )
209+ {
210+ _logger . LogWarning ( $ "Value from { _googleDriveRegValPropName } did not have property named { _googleDriveRegValPropPropName } ") ;
211+ return null ;
212+ }
213+
214+ var path = googleDriveRegValPropProp . GetString ( ) ;
215+ if ( path is not null )
216+ return ConvertDriveLetterToPathAndValidate ( ref path ) ? path : null ;
217+
218+ _logger . LogWarning ( $ "Could not get string from value from { _googleDriveRegValPropPropName } ") ;
219+ return null ;
220+ }
221+
222+ /// <summary>
223+ /// If Google Drive is mounted as a drive, then the path found in the registry will be
224+ /// *just* the drive letter (e.g. just "G" as opposed to "G:\"), and therefore must be
225+ /// reformatted as a valid path.
226+ /// </summary>
227+ private static bool ConvertDriveLetterToPathAndValidate ( ref string path )
228+ {
229+ if ( path . Length > 1 )
230+ return ValidatePath ( path ) ;
231+
232+ DriveInfo driveInfo ;
233+ try
234+ {
235+ driveInfo = new DriveInfo ( path ) ;
236+ }
237+ catch ( ArgumentException e )
238+ {
239+ _logger . LogWarning ( e , $ "Could not resolve drive letter '{ path } ' to a valid drive.") ;
240+ return false ;
241+ }
242+
243+ path = driveInfo . RootDirectory . Name ;
244+ return true ;
245+ }
246+
247+ private static bool ValidatePath ( string path )
248+ {
249+ if ( Directory . Exists ( path ) )
250+ return true ;
251+ _logger . LogWarning ( $ "Invalid path: { path } ") ;
252+ return false ;
253+ }
254+
255+ private static async Task < StorageFile ? > GetGoogleDriveIconFileAsync ( )
256+ {
257+ var programFilesEnvVar = Environment . GetEnvironmentVariable ( "ProgramFiles" ) ;
258+
259+ if ( programFilesEnvVar is null )
260+ return null ;
261+
262+ var iconPath = Path . Combine ( programFilesEnvVar , "Google" , "Drive File Stream" , "drive_fs.ico" ) ;
263+
264+ return await FilesystemTasks . Wrap ( ( ) => StorageFile . GetFileFromPathAsync ( iconPath ) . AsTask ( ) ) ;
265+ }
266+
267+ private static bool AddMyDriveToPathAndValidate ( ref string path )
268+ {
269+ // If `path` contains a shortcut named "My Drive", store its target in `shellFolderBaseFirst`.
270+ // This happens when "My Drive syncing options" is set to "Mirror files".
271+ // TODO: Avoid to use Vanara (#15000)
272+ using var rootFolder = ShellFolderExtensions . GetShellItemFromPathOrPIDL ( path ) as ShellFolder ;
273+ var myDriveFolder = Environment . ExpandEnvironmentVariables ( (
274+ rootFolder ? . FirstOrDefault ( si =>
275+ si . Name ? . Equals ( "My Drive" ) ?? false ) as ShellLink ) ? . TargetPath
276+ ?? string . Empty ) ;
277+
278+ Debug . WriteLine ( "SHELL FOLDER LOGGING" ) ;
279+ rootFolder ? . ForEach ( si => Debug . WriteLine ( si . Name ) ) ;
280+
281+ if ( ! string . IsNullOrEmpty ( myDriveFolder ) )
282+ {
283+ path = myDriveFolder ;
284+ return true ;
285+ }
286+
287+ path = Path . Combine ( path , "My Drive" ) ;
288+ return ValidatePath ( path ) ;
92289 }
93290 }
94291}
0 commit comments