Skip to content

Commit

Permalink
Fix: Fixed an issue where Google Drive wasn't displayed when mounted …
Browse files Browse the repository at this point in the history
…as a folder (#15771)
  • Loading branch information
wharvex authored Aug 14, 2024
1 parent ee9532f commit 9ecf257
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 11 deletions.
15 changes: 10 additions & 5 deletions src/Files.App/Services/Storage/StorageDevicesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,20 @@ public IStorageDeviceWatcher CreateWatcher()
public async IAsyncEnumerable<ILocatableFolder> 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)
{
Expand All @@ -43,10 +52,6 @@ public async IAsyncEnumerable<ILocatableFolder> 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;
Expand Down
209 changes: 203 additions & 6 deletions src/Files.App/Utils/Cloud/Detector/GoogleDriveCloudDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// Provides an utility for Google Drive Cloud detection.
/// Provides a utility for Google Drive Cloud detection.
/// </summary>
public sealed class GoogleDriveCloudDetector : AbstractCloudDetector
{
private static readonly ILogger _logger = Ioc.Default.GetRequiredService<ILogger<App>>();

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<ICloudProvider> GetProviders()
{
// Google Drive's sync database can be in a couple different locations. Go find it.
Expand Down Expand Up @@ -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)
{
Expand All @@ -65,6 +76,7 @@ await FilesystemTasks.Wrap(() => StorageFile.GetFileFromPathAsync(Path.Combine(a
};
}

var iconFile = await GetGoogleDriveIconFileAsync();
// Google virtual drive
reader = cmdMedia.ExecuteReader();

Expand All @@ -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)
{
Expand All @@ -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;
}

/// <summary>
/// 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.
/// </summary>
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<StorageFile?> 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);
}
}
}

0 comments on commit 9ecf257

Please sign in to comment.