From 4fd7b88173be685cf682d2b41fac00a32c2183ab Mon Sep 17 00:00:00 2001 From: abraunegg Date: Mon, 26 Feb 2024 14:48:33 +1100 Subject: [PATCH] Initial commit for #1300 * Remove legacy & unused items for Shared Business Items * Add option: --list-shared-items * Add option: --sync-shared-files --- src/clientSideFiltering.d | 20 ------- src/config.d | 111 +++++++++++++++----------------------- src/main.d | 30 ++++++++--- src/onedrive.d | 7 +++ src/sync.d | 93 ++++++++++++++++++++++++++++++++ 5 files changed, 166 insertions(+), 95 deletions(-) diff --git a/src/clientSideFiltering.d b/src/clientSideFiltering.d index d20ba2f8..5e9a70fc 100644 --- a/src/clientSideFiltering.d +++ b/src/clientSideFiltering.d @@ -20,7 +20,6 @@ class ClientSideFiltering { // Class variables ApplicationConfig appConfig; string[] paths; - string[] businessSharedItemsList; Regex!char fileMask; Regex!char directoryMask; bool skipDirStrictMatch = false; @@ -41,11 +40,6 @@ class ClientSideFiltering { loadSyncList(appConfig.syncListFilePath); } - // Load the Business Shared Items file if it exists - if (exists(appConfig.businessSharedItemsFilePath)){ - loadBusinessSharedItems(appConfig.businessSharedItemsFilePath); - } - // Configure skip_dir, skip_file, skip-dir-strict-match & skip_dotfiles from config entries // Handle skip_dir configuration in config file addLogEntry("Configuring skip_dir ...", ["debug"]); @@ -91,7 +85,6 @@ class ClientSideFiltering { void shutdown() { object.destroy(appConfig); object.destroy(paths); - object.destroy(businessSharedItemsList); object.destroy(fileMask); object.destroy(directoryMask); } @@ -109,19 +102,6 @@ class ClientSideFiltering { file.close(); } - // load business_shared_folders file - void loadBusinessSharedItems(string filepath) { - // open file as read only - auto file = File(filepath, "r"); - auto range = file.byLine(); - foreach (line; range) { - // Skip comments in file - if (line.length == 0 || line[0] == ';' || line[0] == '#') continue; - businessSharedItemsList ~= buildNormalizedPath(line); - } - file.close(); - } - // Configure the regex that will be used for 'skip_file' void setFileMask(const(char)[] mask) { fileMask = wild2regex(mask); diff --git a/src/config.d b/src/config.d index 2166c132..e94e9b1a 100644 --- a/src/config.d +++ b/src/config.d @@ -41,6 +41,8 @@ class ApplicationConfig { immutable string defaultLogFileDir = "/var/log/onedrive"; // - Default configuration directory immutable string defaultConfigDirName = "~/.config/onedrive"; + // - Default 'OneDrive Business Shared Files' Folder Name + immutable string defaultBusinessSharedFilesDirectoryName = "Files Shared With Me"; // Microsoft Requirements // - Default Application ID (abraunegg) @@ -106,7 +108,6 @@ class ApplicationConfig { bool debugLogging = false; long verbosityCount = 0; - // Was the application just authorised - paste of response uri bool applicationAuthorizeResponseUri = false; @@ -121,6 +122,7 @@ class ApplicationConfig { // Store the 'session_upload.CRC32-HASH' file path string uploadSessionFilePath = ""; + // API initialisation flags bool apiWasInitialised = false; bool syncEngineWasInitialised = false; @@ -161,25 +163,23 @@ class ApplicationConfig { private string applicableConfigFilePath = ""; // - Store the 'sync_list' file path string syncListFilePath = ""; - // - Store the 'business_shared_items' file path - string businessSharedItemsFilePath = ""; + // OneDrive Business Shared File handling - what directory will be used? + string configuredBusinessSharedFilesDirectoryName = ""; + // Hash files so that we can detect when the configuration has changed, in items that will require a --resync private string configHashFile = ""; private string configBackupFile = ""; private string syncListHashFile = ""; - private string businessSharedItemsHashFile = ""; // Store the actual 'runtime' hash private string currentConfigHash = ""; private string currentSyncListHash = ""; - private string currentBusinessSharedItemsHash = ""; // Store the previous config files hash values (file contents) private string previousConfigHash = ""; private string previousSyncListHash = ""; - private string previousBusinessSharedItemsHash = ""; - + // Store items that come in from the 'config' file, otherwise these need to be set the the defaults private string configFileSyncDir = defaultSyncDir; private string configFileSkipFile = defaultSkipFile; @@ -197,7 +197,6 @@ class ApplicationConfig { string[string] stringValues; long[string] longValues; bool[string] boolValues; - bool shellEnvironmentSet = false; // Initialise the application configuration @@ -275,7 +274,7 @@ class ApplicationConfig { longValues["ip_protocol_version"] = defaultIpProtocol; // 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only // Number of concurrent threads - longValues["threads"] = defaultConcurrentThreads; // Default is 8, user can increase or decrease + longValues["threads"] = defaultConcurrentThreads; // Default is 8, user can increase to max of 16 or decrease // - Do we wish to upload only? boolValues["upload_only"] = false; @@ -469,9 +468,6 @@ class ApplicationConfig { // - What is the full path for the system 'config' file if it is required systemConfigFilePath = buildNormalizedPath(buildPath(systemConfigDirName, "config")); - // - What is the full path for the 'business_shared_items' - businessSharedItemsFilePath = buildNormalizedPath(buildPath(configDirName, "business_shared_items")); - // To determine if any configuration items has changed, where a --resync would be required, we need to have a hash file for the following items // - 'config.backup' file // - applicable 'config' file @@ -480,8 +476,7 @@ class ApplicationConfig { configBackupFile = buildNormalizedPath(buildPath(configDirName, ".config.backup")); configHashFile = buildNormalizedPath(buildPath(configDirName, ".config.hash")); syncListHashFile = buildNormalizedPath(buildPath(configDirName, ".sync_list.hash")); - businessSharedItemsHashFile = buildNormalizedPath(buildPath(configDirName, ".business_shared_items.hash")); - + // Debug Output for application set variables based on configDirName addLogEntry("refreshTokenFilePath = " ~ refreshTokenFilePath, ["debug"]); addLogEntry("deltaLinkFilePath = " ~ deltaLinkFilePath, ["debug"]); @@ -494,8 +489,6 @@ class ApplicationConfig { addLogEntry("configBackupFile = " ~ configBackupFile, ["debug"]); addLogEntry("configHashFile = " ~ configHashFile, ["debug"]); addLogEntry("syncListHashFile = " ~ syncListHashFile, ["debug"]); - addLogEntry("businessSharedItemsFilePath = " ~ businessSharedItemsFilePath, ["debug"]); - addLogEntry("businessSharedItemsHashFile = " ~ businessSharedItemsHashFile, ["debug"]); // Configure the Hash and Backup File Permission Value string valueToConvert = to!string(defaultFilePermissionMode); @@ -900,6 +893,7 @@ class ApplicationConfig { boolValues["synchronize"] = false; boolValues["force"] = false; boolValues["list_business_shared_items"] = false; + boolValues["sync_business_shared_files"] = false; boolValues["force_sync"] = false; boolValues["with_editing_perms"] = false; @@ -995,6 +989,12 @@ class ApplicationConfig { "get-O365-drive-id", "Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library (DEPRECIATED)", &stringValues["sharepoint_library_name"], + "list-shared-items", + "List OneDrive Business Shared Items", + &boolValues["list_business_shared_items"], + "sync-shared-files", + "Sync OneDrive Business Shared Files to the local filesystem", + &boolValues["sync_business_shared_files"], "local-first", "Synchronize from the local directory source first, before downloading changes from OneDrive.", &boolValues["local_first"], @@ -1365,20 +1365,7 @@ class ApplicationConfig { // Is sync_business_shared_items enabled and configured ? addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering addLogEntry("Config option 'sync_business_shared_items' = " ~ to!string(getValueBool("sync_business_shared_items"))); - - if (exists(businessSharedItemsFilePath)){ - addLogEntry("Selective Business Shared Items configured = true"); - addLogEntry("sync_business_shared_items contents:"); - // Output the sync_business_shared_items contents - auto businessSharedItemsFileList = File(businessSharedItemsFilePath, "r"); - auto range = businessSharedItemsFileList.byLine(); - foreach (line; range) - { - addLogEntry(to!string(line)); - } - } else { - addLogEntry("Selective Business Shared Items configured = false"); - } + addLogEntry("Config option 'Shared Files Directory' = " ~ configuredBusinessSharedFilesDirectoryName); // Are webhooks enabled? addLogEntry(); // used instead of an empty 'writeln();' to ensure the line break is correct in the buffered console output ordering @@ -1518,9 +1505,6 @@ class ApplicationConfig { if (currentSyncListHash != previousSyncListHash) logAndSetDifference("sync_list file has been updated, --resync needed", 0); - if (currentBusinessSharedItemsHash != previousBusinessSharedItemsHash) - logAndSetDifference("business_shared_folders file has been updated, --resync needed", 1); - // Check for updates in the config file if (currentConfigHash != previousConfigHash) { addLogEntry("Application configuration file has been updated, checking if --resync needed"); @@ -1665,7 +1649,14 @@ class ApplicationConfig { break; } } - + + // Final override + // In certain situations, regardless of config 'resync' needed status, ignore this so that the application can display 'non-syncable' information + // Options that should now be looked at are: + // --list-shared-items + if (getValueBool("list_business_shared_items")) resyncRequired = false; + + // Return the calculated boolean return resyncRequired; } @@ -1676,7 +1667,6 @@ class ApplicationConfig { addLogEntry("Cleaning up configuration hash files", ["debug"]); safeRemove(configHashFile); safeRemove(syncListHashFile); - safeRemove(businessSharedItemsHashFile); } else { // --dry-run scenario ... technically we should not be making any local file changes ....... addLogEntry("DRY RUN: Not removing hash files as --dry-run has been used"); @@ -1704,17 +1694,6 @@ class ApplicationConfig { // Hash file should only be readable by the user who created it - 0600 permissions needed syncListHashFile.setAttributes(convertedPermissionValue); } - - - // Update 'update business_shared_items' files - if (exists(businessSharedItemsFilePath)) { - // update business_shared_folders hash - addLogEntry("Updating business_shared_items hash", ["debug"]); - std.file.write(businessSharedItemsHashFile, computeQuickXorHash(businessSharedItemsFilePath)); - // Hash file should only be readable by the user who created it - 0600 permissions needed - businessSharedItemsHashFile.setAttributes(convertedPermissionValue); - } - } else { // --dry-run scenario ... technically we should not be making any local file changes ....... addLogEntry("DRY RUN: Not updating hash files as --dry-run has been used"); @@ -1746,18 +1725,6 @@ class ApplicationConfig { // Generate the runtime hash for the 'sync_list' file currentSyncListHash = computeQuickXorHash(syncListFilePath); } - - // Does a 'business_shared_items' file exist with a valid hash file - if (exists(businessSharedItemsFilePath)) { - if (!exists(businessSharedItemsHashFile)) { - // no existing hash file exists - std.file.write(businessSharedItemsHashFile, "initial-hash"); - // Hash file should only be readable by the user who created it - 0600 permissions needed - businessSharedItemsHashFile.setAttributes(convertedPermissionValue); - } - // Generate the runtime hash for the 'sync_list' file - currentBusinessSharedItemsHash = computeQuickXorHash(businessSharedItemsFilePath); - } } // Read in the text values of the previous configurations @@ -1783,16 +1750,7 @@ class ApplicationConfig { return EXIT_FAILURE; } } - if (exists(businessSharedItemsHashFile)) { - try { - previousBusinessSharedItemsHash = readText(businessSharedItemsHashFile); - } catch (std.file.FileException e) { - // Unable to access required hash file - addLogEntry("ERROR: Unable to access " ~ e.msg); - // Use exit scopes to shutdown API - return EXIT_FAILURE; - } - } + return 0; } @@ -1850,10 +1808,22 @@ class ApplicationConfig { // --list-shared-folders cannot be used with --resync and/or --resync-auth if ((getValueBool("list_business_shared_items")) && ((getValueBool("resync")) || (getValueBool("resync_auth")))) { - addLogEntry("ERROR: --list-shared-folders cannot be used with --resync or --resync-auth"); + addLogEntry("ERROR: --list-shared-items cannot be used with --resync or --resync-auth"); operationalConflictDetected = true; } + // --list-shared-folders cannot be used with --sync or --monitor + if ((getValueBool("list_business_shared_items")) && ((getValueBool("synchronize")) || (getValueBool("monitor")))) { + addLogEntry("ERROR: --list-shared-items cannot be used with --sync or --monitor"); + operationalConflictDetected = true; + } + + // --sync-shared-files can ONLY be used with sync_business_shared_items + if ((getValueBool("sync_business_shared_files")) && (!getValueBool("sync_business_shared_items"))) { + addLogEntry("ERROR: The --sync-shared-files option can only be utilised if the 'sync_business_shared_items' configuration setting is enabled."); + operationalConflictDetected = true; + } + // --display-sync-status cannot be used with --resync and/or --resync-auth if ((getValueBool("display_sync_status")) && ((getValueBool("resync")) || (getValueBool("resync_auth")))) { addLogEntry("ERROR: --display-sync-status cannot be used with --resync or --resync-auth"); @@ -2057,6 +2027,9 @@ class ApplicationConfig { // What will runtimeSyncDirectory be actually set to? addLogEntry("sync_dir: runtimeSyncDirectory set to: " ~ runtimeSyncDirectory, ["debug"]); + // Configure configuredBusinessSharedFilesDirectoryName + configuredBusinessSharedFilesDirectoryName = buildNormalizedPath(buildPath(runtimeSyncDirectory, defaultBusinessSharedFilesDirectoryName)); + return runtimeSyncDirectory; } diff --git a/src/main.d b/src/main.d index 019f2e75..26a6e1f7 100644 --- a/src/main.d +++ b/src/main.d @@ -346,10 +346,13 @@ int main(string[] cliArgs) { return EXIT_RESYNC_REQUIRED; } else { // No configuration change that requires a --resync to be issued - // Make a backup of the applicable configuration file - appConfig.createBackupConfigFile(); - // Update hash files and generate a new config backup - appConfig.updateHashContentsForConfigFiles(); + // Special cases need to be checked - if these options were enabled, it creates a false 'Resync Required' flag, so do not create a backup + if ((!appConfig.getValueBool("list_business_shared_items"))) { + // Make a backup of the applicable configuration file + appConfig.createBackupConfigFile(); + // Update hash files and generate a new config backup + appConfig.updateHashContentsForConfigFiles(); + } } } @@ -446,8 +449,9 @@ int main(string[] cliArgs) { // Are we performing some sort of 'no-sync' task? // - Are we obtaining the Office 365 Drive ID for a given Office 365 SharePoint Shared Library? // - Are we displaying the sync satus? - // - Are we getting the URL for a file online - // - Are we listing who modified a file last online + // - Are we getting the URL for a file online? + // - Are we listing who modified a file last online? + // - Are we listing OneDrive Business Shared Items? // - Are we createing a shareable link for an existing file on OneDrive? // - Are we just creating a directory online, without any sync being performed? // - Are we just deleting a directory online, without any sync being performed? @@ -499,6 +503,20 @@ int main(string[] cliArgs) { return EXIT_SUCCESS; } + // --list-shared-items - Are we listing OneDrive Business Shared Items + if (appConfig.getValueBool("list_business_shared_items")) { + // Is this a business account type? + if (appConfig.accountType == "business") { + // List OneDrive Business Shared Items + syncEngineInstance.listBusinessSharedObjects(); + } else { + addLogEntry("ERROR: Unsupported account type for listing OneDrive Business Shared Items"); + } + // Exit application + // Use exit scopes to shutdown API + return EXIT_SUCCESS; + } + // --create-share-link - Are we createing a shareable link for an existing file on OneDrive? if (appConfig.getValueString("create_share_link") != "") { // Query OneDrive for the file, and if valid, create a shareable link for the file diff --git a/src/onedrive.d b/src/onedrive.d index 8b73b7c5..59b3b377 100644 --- a/src/onedrive.d +++ b/src/onedrive.d @@ -512,6 +512,13 @@ class OneDriveApi { return get(url); } + // Return all the items that are shared with the user + // https://docs.microsoft.com/en-us/graph/api/drive-sharedwithme + JSONValue getSharedWithMe() { + checkAccessTokenExpired(); + return get(sharedWithMeUrl); + } + // Create a shareable link for an existing file on OneDrive based on the accessScope JSON permissions // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createlink JSONValue createShareableLink(string driveId, string id, JSONValue accessScope) { diff --git a/src/sync.d b/src/sync.d index 45363ec2..02e5cd3f 100644 --- a/src/sync.d +++ b/src/sync.d @@ -8155,4 +8155,97 @@ class SyncEngine { addLogEntry("Creating|Updating into local database a DB Tie record: " ~ to!string(tieDBItem), ["debug"]); itemDB.upsert(tieDBItem); } + + void listBusinessSharedObjects() { + + JSONValue sharedWithMeItems; + + // Create a new API Instance for this thread and initialise it + OneDriveApi sharedWithMeOneDriveApiInstance; + sharedWithMeOneDriveApiInstance = new OneDriveApi(appConfig); + sharedWithMeOneDriveApiInstance.initialise(); + + try { + sharedWithMeItems = sharedWithMeOneDriveApiInstance.getSharedWithMe(); + } catch (OneDriveException e) { + + // Display error message + displayOneDriveErrorMessage(e.msg, getFunctionName!({})); + // Must exit here + sharedWithMeOneDriveApiInstance.shutdown(); + // Free object and memory + object.destroy(sharedWithMeOneDriveApiInstance); + } + + if (sharedWithMeItems.type() == JSONType.object) { + + if (count(sharedWithMeItems["value"].array) > 0) { + // No shared items + addLogEntry(); + addLogEntry("Listing available OneDrive Business Shared Items:"); + addLogEntry(); + + // Iterate through the array + foreach (searchResult; sharedWithMeItems["value"].array) { + + // loop variables for each item + string sharedByName; + string sharedByEmail; + + // Debug response output + addLogEntry("shared folder entry: " ~ to!string(searchResult), ["debug"]); + + // Configure 'who' this was shared by + if ("sharedBy" in searchResult["remoteItem"]["shared"]) { + // we have shared by details we can use + if ("displayName" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) { + sharedByName = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["displayName"].str; + } + if ("email" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) { + sharedByEmail = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["email"].str; + } + } + + // Output query result + addLogEntry("-----------------------------------------------------------------------------------"); + if (isItemFile(searchResult)) { + addLogEntry("Shared File: " ~ to!string(searchResult["name"].str)); + } else { + addLogEntry("Shared Folder: " ~ to!string(searchResult["name"].str)); + } + + // Detail 'who' shared this + if ((sharedByName != "") && (sharedByEmail != "")) { + addLogEntry("Shared By: " ~ sharedByName ~ " (" ~ sharedByEmail ~ ")"); + } else { + if (sharedByName != "") { + addLogEntry("Shared By: " ~ sharedByName); + } + } + + // More detail if --verbose is being used + addLogEntry("Item Id: " ~ searchResult["remoteItem"]["id"].str, ["verbose"]); + addLogEntry("Parent Drive Id: " ~ searchResult["remoteItem"]["parentReference"]["driveId"].str, ["verbose"]); + if ("id" in searchResult["remoteItem"]["parentReference"]) { + addLogEntry("Parent Item Id: " ~ searchResult["remoteItem"]["parentReference"]["id"].str, ["verbose"]); + } + } + + // Close out the loop + addLogEntry("-----------------------------------------------------------------------------------"); + addLogEntry(); + + } else { + // No shared items + addLogEntry(); + addLogEntry("No OneDrive Business Shared Folders were returned"); + addLogEntry(); + } + } + + // Shutdown API access + sharedWithMeOneDriveApiInstance.shutdown(); + // Free object and memory + object.destroy(sharedWithMeOneDriveApiInstance); + } } \ No newline at end of file