diff --git a/src/Files.App/Services/Storage/StorageDevicesService.cs b/src/Files.App/Services/Storage/StorageDevicesService.cs index e415b0a9fb09..8493a260c03c 100644 --- a/src/Files.App/Services/Storage/StorageDevicesService.cs +++ b/src/Files.App/Services/Storage/StorageDevicesService.cs @@ -19,11 +19,20 @@ public IStorageDeviceWatcher CreateWatcher() public async IAsyncEnumerable GetDrivesAsync() { var list = DriveInfo.GetDrives(); - var googleDrivePath = App.AppModel.GoogleDrivePath; var pCloudDrivePath = App.AppModel.PCloudDrivePath; + var sw = Stopwatch.StartNew(); + var googleDrivePath = GoogleDriveCloudDetector.GetRegistryBasePath(); + sw.Stop(); + Debug.WriteLine($"In RemovableDrivesService: Time elapsed for registry check: {sw.Elapsed}"); + App.AppModel.GoogleDrivePath = googleDrivePath ?? string.Empty; + foreach (var drive in list) { + // We don't want cloud drives to appear in a plain "Drives" section. + if (drive.Name.Equals(googleDrivePath) || drive.Name.Equals(pCloudDrivePath)) + continue; + var res = await FilesystemTasks.Wrap(() => StorageFolder.GetFolderFromPathAsync(drive.Name).AsTask()); if (res.ErrorCode is FileSystemStatusCode.Unauthorized) { @@ -43,10 +52,6 @@ public async IAsyncEnumerable GetDrivesAsync() var label = DriveHelpers.GetExtendedDriveLabel(drive); var driveItem = await DriveItem.CreateFromPropertiesAsync(res.Result, drive.Name.TrimEnd('\\'), label, type, thumbnail); - // Don't add here because Google Drive is already displayed under cloud drives - if (drive.Name == googleDrivePath || drive.Name == pCloudDrivePath) - continue; - App.Logger.LogInformation($"Drive added: {driveItem.Path}, {driveItem.Type}"); yield return driveItem; diff --git a/src/Files.App/Utils/Cloud/Detector/GoogleDriveCloudDetector.cs b/src/Files.App/Utils/Cloud/Detector/GoogleDriveCloudDetector.cs index 31c03bacd6b8..8e60637cb153 100644 --- a/src/Files.App/Utils/Cloud/Detector/GoogleDriveCloudDetector.cs +++ b/src/Files.App/Utils/Cloud/Detector/GoogleDriveCloudDetector.cs @@ -2,16 +2,26 @@ // Licensed under the MIT License. See the LICENSE. using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Logging; +using Microsoft.Win32; using System.IO; using Windows.Storage; +using Vanara.Windows.Shell; namespace Files.App.Utils.Cloud { /// - /// Provides an utility for Google Drive Cloud detection. + /// Provides a utility for Google Drive Cloud detection. /// public sealed class GoogleDriveCloudDetector : AbstractCloudDetector { + private static readonly ILogger _logger = Ioc.Default.GetRequiredService>(); + + private const string _googleDriveRegKeyName = @"Software\Google\DriveFS"; + private const string _googleDriveRegValName = "PerAccountPreferences"; + private const string _googleDriveRegValPropName = "value"; + private const string _googleDriveRegValPropPropName = "mount_point_path"; + protected override async IAsyncEnumerable GetProviders() { // 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 var folder = await StorageFolder.GetFolderFromPathAsync(path); string title = reader["title"]?.ToString() ?? folder.Name; - App.AppModel.GoogleDrivePath = path; + Debug.WriteLine("YIELD RETURNING from `GoogleDriveCloudDetector.GetProviders()` (roots): "); + Debug.WriteLine($"Name: Google Drive ({title}); SyncFolder: {path}"); yield return new CloudProvider(CloudProviders.GoogleDrive) { @@ -65,6 +76,7 @@ await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(Path.Combine(a }; } + var iconFile = await GetGoogleDriveIconFileAsync(); // Google virtual drive reader = cmdMedia.ExecuteReader(); @@ -74,13 +86,14 @@ await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(Path.Combine(a if (string.IsNullOrWhiteSpace(path)) continue; + if (!AddMyDriveToPathAndValidate(ref path)) + continue; + var folder = await StorageFolder.GetFolderFromPathAsync(path); string title = reader["name"]?.ToString() ?? folder.Name; - string iconPath = Path.Combine(Environment.GetEnvironmentVariable("ProgramFiles"), "Google", "Drive File Stream", "drive_fs.ico"); - - App.AppModel.GoogleDrivePath = path; - StorageFile iconFile = await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(iconPath).AsTask()); + Debug.WriteLine("YIELD RETURNING from `GoogleDriveCloudDetector.GetProviders` (media): "); + Debug.WriteLine($"Name: {title}; SyncFolder: {path}"); yield return new CloudProvider(CloudProviders.GoogleDrive) { @@ -89,6 +102,190 @@ await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(Path.Combine(a IconData = iconFile is not null ? await iconFile.ToByteArrayAsync() : null, }; } + + await Inspect(database, "SELECT * FROM roots", "root_preferences db, roots table"); + await Inspect(database, "SELECT * FROM media", "root_preferences db, media table"); + await Inspect(database, "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY 1", "root_preferences db, all tables"); + + var registryPath = App.AppModel.GoogleDrivePath; + if (!AddMyDriveToPathAndValidate(ref registryPath)) + yield break; + yield return new CloudProvider(CloudProviders.GoogleDrive) + { + Name = "Google Drive", + SyncFolder = registryPath, + IconData = iconFile is not null ? await iconFile.ToByteArrayAsync() : null + }; + } + + private static async Task Inspect(SqliteConnection database, string sqlCommand, string targetDescription) + { + await using var cmdTablesAll = new SqliteCommand(sqlCommand, database); + var reader = await cmdTablesAll.ExecuteReaderAsync(); + var colNamesList = Enumerable.Range(0, reader.FieldCount).Select(i => reader.GetName(i)).ToList(); + + Debug.WriteLine($"BEGIN LOGGING of {targetDescription}"); + + for (int rowIdx = 0; reader.Read() is not false; rowIdx++) + { + var colVals = new object[reader.FieldCount]; + reader.GetValues(colVals); + + colVals.Select((val, colIdx) => $"row {rowIdx}: column {colIdx}: {colNamesList[colIdx]}: {val}") + .ToList().ForEach(s => Debug.WriteLine(s)); + } + + Debug.WriteLine($"END LOGGING of {targetDescription} contents"); + } + + private static JsonDocument? GetGoogleDriveRegValJson() + { + // This will be null if the key name is not found. + using var googleDriveRegKey = Registry.CurrentUser.OpenSubKey(_googleDriveRegKeyName); + + if (googleDriveRegKey is null) + { + _logger.LogWarning($"Google Drive registry key for key name '{_googleDriveRegKeyName}' not found."); + return null; + } + + var googleDriveRegVal = googleDriveRegKey.GetValue(_googleDriveRegValName); + + if (googleDriveRegVal is null) + { + _logger.LogWarning($"Google Drive registry value for value name '{_googleDriveRegValName}' not found."); + return null; + } + + JsonDocument? googleDriveRegValueJson = null; + try + { + googleDriveRegValueJson = JsonDocument.Parse(googleDriveRegVal.ToString() ?? ""); + } + catch (JsonException je) + { + _logger.LogWarning(je, $"Google Drive registry value for value name '{_googleDriveRegValName}' could not be parsed as a JsonDocument."); + } + + return googleDriveRegValueJson; + } + + public static string? GetRegistryBasePath() + { + var googleDriveRegValJson = GetGoogleDriveRegValJson(); + + if (googleDriveRegValJson is null) + return null; + + var googleDriveRegValJsonProperty = googleDriveRegValJson + .RootElement.EnumerateObject() + .FirstOrDefault(); + + // A default JsonProperty struct has an "Undefined" Value#ValueKind and throws an + // error if you try to call EnumerateArray on its Value. + if (googleDriveRegValJsonProperty.Value.ValueKind == JsonValueKind.Undefined) + { + _logger.LogWarning($"Root element of Google Drive registry value for value name '{_googleDriveRegValName}' was empty."); + return null; + } + + Debug.WriteLine("REGISTRY LOGGING"); + Debug.WriteLine(googleDriveRegValJsonProperty.ToString()); + + var item = googleDriveRegValJsonProperty.Value.EnumerateArray().FirstOrDefault(); + if (item.ValueKind == JsonValueKind.Undefined) + { + _logger.LogWarning($"Array in the root element of Google Drive registry value for value name '{_googleDriveRegValName}' was empty."); + return null; + } + + if (!item.TryGetProperty(_googleDriveRegValPropName, out var googleDriveRegValProp)) + { + _logger.LogWarning($"First element in the Google Drive Registry Root Array did not have property named {_googleDriveRegValPropName}"); + return null; + } + + if (!googleDriveRegValProp.TryGetProperty(_googleDriveRegValPropPropName, out var googleDriveRegValPropProp)) + { + _logger.LogWarning($"Value from {_googleDriveRegValPropName} did not have property named {_googleDriveRegValPropPropName}"); + return null; + } + + var path = googleDriveRegValPropProp.GetString(); + if (path is not null) + return ConvertDriveLetterToPathAndValidate(ref path) ? path : null; + + _logger.LogWarning($"Could not get string from value from {_googleDriveRegValPropPropName}"); + return null; + } + + /// + /// If Google Drive is mounted as a drive, then the path found in the registry will be + /// *just* the drive letter (e.g. just "G" as opposed to "G:\"), and therefore must be + /// reformatted as a valid path. + /// + private static bool ConvertDriveLetterToPathAndValidate(ref string path) + { + if (path.Length > 1) + return ValidatePath(path); + + DriveInfo driveInfo; + try + { + driveInfo = new DriveInfo(path); + } + catch (ArgumentException e) + { + _logger.LogWarning(e, $"Could not resolve drive letter '{path}' to a valid drive."); + return false; + } + + path = driveInfo.RootDirectory.Name; + return true; + } + + private static bool ValidatePath(string path) + { + if (Directory.Exists(path)) + return true; + _logger.LogWarning($"Invalid path: {path}"); + return false; + } + + private static async Task GetGoogleDriveIconFileAsync() + { + var programFilesEnvVar = Environment.GetEnvironmentVariable("ProgramFiles"); + + if (programFilesEnvVar is null) + return null; + + var iconPath = Path.Combine(programFilesEnvVar, "Google", "Drive File Stream", "drive_fs.ico"); + + return await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(iconPath).AsTask()); + } + + private static bool AddMyDriveToPathAndValidate(ref string path) + { + // If `path` contains a shortcut named "My Drive", store its target in `shellFolderBaseFirst`. + // This happens when "My Drive syncing options" is set to "Mirror files". + // TODO: Avoid to use Vanara (#15000) + using var rootFolder = ShellFolderExtensions.GetShellItemFromPathOrPIDL(path) as ShellFolder; + var myDriveFolder = Environment.ExpandEnvironmentVariables(( + rootFolder?.FirstOrDefault(si => + si.Name?.Equals("My Drive") ?? false) as ShellLink)?.TargetPath + ?? string.Empty); + + Debug.WriteLine("SHELL FOLDER LOGGING"); + rootFolder?.ForEach(si => Debug.WriteLine(si.Name)); + + if (!string.IsNullOrEmpty(myDriveFolder)) + { + path = myDriveFolder; + return true; + } + + path = Path.Combine(path, "My Drive"); + return ValidatePath(path); } } }