import core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit; import core.memory, core.time, core.thread; import std.getopt, std.file, std.path, std.process, std.stdio, std.conv, std.algorithm.searching, std.string, std.regex; import config, itemdb, monitor, onedrive, selective, sync, util; import std.net.curl: CurlException; import core.stdc.signal; import std.traits, std.format; import std.concurrency: receiveTimeout; static import log; OneDriveApi oneDrive; ItemDatabase itemDb; bool onedriveInitialised = false; const int EXIT_UNAUTHORIZED = 3; const int EXIT_RESYNC_REQUIRED = 126; enum MONITOR_LOG_SILENT = 2; enum MONITOR_LOG_QUIET = 1; enum LOG_NORMAL = 0; int main(string[] args) { // Disable buffering on stdout stdout.setvbuf(0, _IONBF); // main function variables string confdirOption; string configFilePath; string syncListFilePath; string databaseFilePath; string businessSharedFolderFilePath; string currentConfigHash; string currentSyncListHash; string previousConfigHash; string previousSyncListHash; string configHashFile; string syncListHashFile; string configBackupFile; string syncDir; string logOutputMessage; string currentBusinessSharedFoldersHash; string previousBusinessSharedFoldersHash; string businessSharedFoldersHashFile; string databaseFilePathDryRunGlobal; bool configOptionsDifferent = false; bool businessSharedFoldersDifferent = false; bool syncListConfigured = false; bool syncListDifferent = false; bool syncDirDifferent = false; bool skipFileDifferent = false; bool skipDirDifferent = false; bool online = false; bool performSyncOK = false; bool displayMemoryUsage = false; bool displaySyncOptions = false; bool cleanupLocalFilesGlobal = false; // hash file permission values string hashPermissionValue = "600"; auto convertedPermissionValue = parse!long(hashPermissionValue, 8); // Define scopes scope(exit) { // Display memory details if (displayMemoryUsage) { log.displayMemoryUsagePreGC(); } // if initialised, shut down the HTTP instance if (onedriveInitialised) { oneDrive.shutdown(); } // was itemDb initialised? if (itemDb !is null) { // Make sure the .wal file is incorporated into the main db before we exit itemDb.performVacuum(); destroy(itemDb); } // cleanup any dry-run data cleanupDryRunDatabase(databaseFilePathDryRunGlobal); // free API instance if (oneDrive !is null) { destroy(oneDrive); } // Perform Garbage Cleanup GC.collect(); // Display memory details if (displayMemoryUsage) { log.displayMemoryUsagePostGC(); } } scope(failure) { // Display memory details if (displayMemoryUsage) { log.displayMemoryUsagePreGC(); } // if initialised, shut down the HTTP instance if (onedriveInitialised) { oneDrive.shutdown(); } // was itemDb initialised? if (itemDb !is null) { // Make sure the .wal file is incorporated into the main db before we exit itemDb.performVacuum(); destroy(itemDb); } // cleanup any dry-run data cleanupDryRunDatabase(databaseFilePathDryRunGlobal); // free API instance if (oneDrive !is null) { destroy(oneDrive); } // Perform Garbage Cleanup GC.collect(); // Display memory details if (displayMemoryUsage) { log.displayMemoryUsagePostGC(); } } // read in application options as passed in try { bool printVersion = false; auto opt = getopt( args, std.getopt.config.passThrough, std.getopt.config.bundling, std.getopt.config.caseSensitive, "confdir", "Set the directory used to store the configuration files", &confdirOption, "verbose|v+", "Print more details, useful for debugging (repeat for extra debugging)", &log.verbose, "version", "Print the version and exit", &printVersion ); // print help and exit if (opt.helpWanted) { args ~= "--help"; } // print the version and exit if (printVersion) { writeln("onedrive ", strip(import("version"))); return EXIT_SUCCESS; } } catch (GetOptException e) { // option errors log.error(e.msg); log.error("Try 'onedrive --help' for more information"); return EXIT_FAILURE; } catch (Exception e) { // generic error log.error(e.msg); log.error("Try 'onedrive --help' for more information"); return EXIT_FAILURE; } // confdirOption must be a directory, not a file // - By default ~/.config/onedrive will be used // - If the user is using --confdir , the confdirOption needs to be evaluated when trying to load any file // load configuration file if available auto cfg = new config.Config(confdirOption); if (!cfg.initialize()) { // There was an error loading the configuration // Error message already printed return EXIT_FAILURE; } // How was this application started - what options were passed in log.vdebug("passed in options: ", args); log.vdebug("note --confdir and --verbose not listed in args"); // set memory display displayMemoryUsage = cfg.getValueBool("display_memory"); // set display sync options displaySyncOptions = cfg.getValueBool("display_sync_options"); // update configuration from command line args cfg.update_from_args(args); // --resync should be a 'last resort item' .. the user needs to 'accept' to proceed if ((cfg.getValueBool("resync")) && (!cfg.getValueBool("display_config"))) { // what is the risk acceptance? bool resyncRiskAcceptance = false; if (!cfg.getValueBool("resync_auth")) { // need to prompt user char response; // warning message writeln("\nThe use of --resync will remove your local 'onedrive' client state, thus no record will exist regarding your current 'sync status'"); writeln("This has the potential to overwrite local versions of files with potentially older versions downloaded from OneDrive which can lead to data loss"); writeln("If in-doubt, backup your local data first before proceeding with --resync"); write("\nAre you sure you wish to proceed with --resync? [Y/N] "); try { // Attempt to read user response readf(" %c\n", &response); } catch (std.format.FormatException e) { // Caught an error return EXIT_FAILURE; } // Evaluate user repsonse if ((to!string(response) == "y") || (to!string(response) == "Y")) { // User has accepted --resync risk to proceed resyncRiskAcceptance = true; // Are you sure you wish .. does not use writeln(); write("\n"); } } else { // resync_auth is true resyncRiskAcceptance = true; } // Action based on response if (!resyncRiskAcceptance){ // --resync risk not accepted return EXIT_FAILURE; } } // Initialise normalised file paths configFilePath = buildNormalizedPath(cfg.configDirName ~ "/config"); syncListFilePath = buildNormalizedPath(cfg.configDirName ~ "/sync_list"); databaseFilePath = buildNormalizedPath(cfg.configDirName ~ "/items.db"); businessSharedFolderFilePath = buildNormalizedPath(cfg.configDirName ~ "/business_shared_folders"); // Has any of our configuration that would require a --resync been changed? // 1. sync_list file modification // 2. config file modification - but only if sync_dir, skip_dir, skip_file or drive_id was modified // 3. CLI input overriding configured config file option configHashFile = buildNormalizedPath(cfg.configDirName ~ "/.config.hash"); syncListHashFile = buildNormalizedPath(cfg.configDirName ~ "/.sync_list.hash"); configBackupFile = buildNormalizedPath(cfg.configDirName ~ "/.config.backup"); businessSharedFoldersHashFile = buildNormalizedPath(cfg.configDirName ~ "/.business_shared_folders.hash"); // Does a config file exist with a valid hash file if ((exists(configFilePath)) && (!exists(configHashFile))) { // Hash of config file needs to be created std.file.write(configHashFile, computeQuickXorHash(configFilePath)); // Hash file should only be readable by the user who created it - 0600 permissions needed configHashFile.setAttributes(to!int(convertedPermissionValue)); } // Does a sync_list file exist with a valid hash file if ((exists(syncListFilePath)) && (!exists(syncListHashFile))) { // Hash of sync_list file needs to be created std.file.write(syncListHashFile, computeQuickXorHash(syncListFilePath)); // Hash file should only be readable by the user who created it - 0600 permissions needed syncListHashFile.setAttributes(to!int(convertedPermissionValue)); } // check if business_shared_folders & business_shared_folders hash exists if ((exists(businessSharedFolderFilePath)) && (!exists(businessSharedFoldersHashFile))) { // Hash of business_shared_folders file needs to be created std.file.write(businessSharedFoldersHashFile, computeQuickXorHash(businessSharedFolderFilePath)); // Hash file should only be readable by the user who created it - 0600 permissions needed businessSharedFoldersHashFile.setAttributes(to!int(convertedPermissionValue)); } // If hash files exist, but config files do not ... remove the hash, but only if --resync was issued as now the application will use 'defaults' which 'may' be different if ((!exists(configFilePath)) && (exists(configHashFile))) { // if --resync safe remove config.hash and config.backup if (cfg.getValueBool("resync")) { safeRemove(configHashFile); safeRemove(configBackupFile); } } // If sync_list hash file exists, but sync_list file does not ... remove the hash, but only if --resync was issued as now the application will use 'defaults' which 'may' be different if ((!exists(syncListFilePath)) && (exists(syncListHashFile))) { // if --resync safe remove sync_list.hash if (cfg.getValueBool("resync")) safeRemove(syncListHashFile); } if ((!exists(businessSharedFolderFilePath)) && (exists(businessSharedFoldersHashFile))) { // if --resync safe remove business_shared_folders.hash if (cfg.getValueBool("resync")) safeRemove(businessSharedFoldersHashFile); } // Read config hashes if they exist if (exists(configFilePath)) currentConfigHash = computeQuickXorHash(configFilePath); if (exists(syncListFilePath)) currentSyncListHash = computeQuickXorHash(syncListFilePath); if (exists(businessSharedFolderFilePath)) currentBusinessSharedFoldersHash = computeQuickXorHash(businessSharedFolderFilePath); if (exists(configHashFile)) { try { previousConfigHash = readText(configHashFile); } catch (std.file.FileException e) { // Unable to access required file log.error("ERROR: Unable to access ", e.msg); // Use exit scopes to shutdown API return EXIT_FAILURE; } } if (exists(syncListHashFile)) { try { previousSyncListHash = readText(syncListHashFile); } catch (std.file.FileException e) { // Unable to access required file log.error("ERROR: Unable to access ", e.msg); // Use exit scopes to shutdown API return EXIT_FAILURE; } } if (exists(businessSharedFoldersHashFile)) { try { previousBusinessSharedFoldersHash = readText(businessSharedFoldersHashFile); } catch (std.file.FileException e) { // Unable to access required file log.error("ERROR: Unable to access ", e.msg); // Use exit scopes to shutdown API return EXIT_FAILURE; } } // Was sync_list file updated? if (currentSyncListHash != previousSyncListHash) { // Debugging output to assist what changed log.vdebug("sync_list file has been updated, --resync needed"); syncListDifferent = true; } // Was business_shared_folders updated? if (currentBusinessSharedFoldersHash != previousBusinessSharedFoldersHash) { // Debugging output to assist what changed log.vdebug("business_shared_folders file has been updated, --resync needed"); businessSharedFoldersDifferent = true; } // Was config file updated between last execution ang this execution? if (currentConfigHash != previousConfigHash) { // config file was updated, however we only want to trigger a --resync requirement if sync_dir, skip_dir, skip_file or drive_id was modified if (!cfg.getValueBool("display_config")){ // only print this message if we are not using --display-config log.log("config file has been updated, checking if --resync needed"); } if (exists(configBackupFile)) { // check backup config what has changed for these configuration options if anything // # sync_dir = "~/OneDrive" // # skip_file = "~*|.~*|*.tmp" // # skip_dir = "" // # drive_id = "" string[string] stringValues; stringValues["sync_dir"] = ""; stringValues["skip_file"] = ""; stringValues["skip_dir"] = ""; stringValues["drive_id"] = ""; auto configBackupFileHandle = File(configBackupFile, "r"); string lineBuffer; auto range = configBackupFileHandle.byLine(); // read configBackupFile line by line foreach (line; range) { lineBuffer = stripLeft(line).to!string; if (lineBuffer.length == 0 || lineBuffer[0] == ';' || lineBuffer[0] == '#') continue; auto c = lineBuffer.matchFirst(cfg.configRegex); if (!c.empty) { c.popFront(); // skip the whole match string key = c.front.dup; auto p = key in stringValues; if (p) { c.popFront(); // compare this key if ((key == "sync_dir") && (c.front.dup != cfg.getValueString("sync_dir"))) { log.vdebug(key, " was modified since the last time the application was successfully run, --resync needed"); configOptionsDifferent = true; } if ((key == "skip_file") && (c.front.dup != cfg.getValueString("skip_file"))){ log.vdebug(key, " was modified since the last time the application was successfully run, --resync needed"); configOptionsDifferent = true; } if ((key == "skip_dir") && (c.front.dup != cfg.getValueString("skip_dir"))){ log.vdebug(key, " was modified since the last time the application was successfully run, --resync needed"); configOptionsDifferent = true; } if ((key == "drive_id") && (c.front.dup != cfg.getValueString("drive_id"))){ log.vdebug(key, " was modified since the last time the application was successfully run, --resync needed"); configOptionsDifferent = true; } } } } // close file if open if (configBackupFileHandle.isOpen()){ // close open file configBackupFileHandle.close(); } } else { // no backup to check log.vdebug("WARNING: no backup config file was found, unable to validate if any changes made"); } // If there was a backup, any modified values we need to worry about would been detected if (!cfg.getValueBool("display_config")) { // we are not testing the configuration if (!configOptionsDifferent) { // no options are different if (!cfg.getValueBool("dry_run")) { // we are not in a dry-run scenario // update config hash log.vdebug("updating config hash as it is out of date"); std.file.write(configHashFile, computeQuickXorHash(configFilePath)); // Hash file should only be readable by the user who created it - 0600 permissions needed configHashFile.setAttributes(to!int(convertedPermissionValue)); // create backup copy of current config file log.vdebug("making backup of config file as it is out of date"); std.file.copy(configFilePath, configBackupFile); // File Copy should only be readable by the user who created it - 0600 permissions needed configBackupFile.setAttributes(to!int(convertedPermissionValue)); } } } } // Is there a backup of the config file if the config file exists? if ((exists(configFilePath)) && (!exists(configBackupFile))) { // create backup copy of current config file std.file.copy(configFilePath, configBackupFile); // File Copy should only be readable by the user who created it - 0600 permissions needed configBackupFile.setAttributes(to!int(convertedPermissionValue)); } // config file set options can be changed via CLI input, specifically these will impact sync and --resync will be needed: // --syncdir ARG // --skip-file ARG // --skip-dir ARG if (exists(configFilePath)) { // config file exists // was the sync_dir updated by CLI? if (cfg.configFileSyncDir != "") { // sync_dir was set in config file if (cfg.configFileSyncDir != cfg.getValueString("sync_dir")) { // config file was set and CLI input changed this log.vdebug("sync_dir: CLI override of config file option, --resync needed"); syncDirDifferent = true; } } // was the skip_file updated by CLI? if (cfg.configFileSkipFile != "") { // skip_file was set in config file if (cfg.configFileSkipFile != cfg.getValueString("skip_file")) { // config file was set and CLI input changed this log.vdebug("skip_file: CLI override of config file option, --resync needed"); skipFileDifferent = true; } } // was the skip_dir updated by CLI? if (cfg.configFileSkipDir != "") { // skip_dir was set in config file if (cfg.configFileSkipDir != cfg.getValueString("skip_dir")) { // config file was set and CLI input changed this log.vdebug("skip_dir: CLI override of config file option, --resync needed"); skipDirDifferent = true; } } } // Has anything triggered a --resync requirement? if (configOptionsDifferent || syncListDifferent || syncDirDifferent || skipFileDifferent || skipDirDifferent || businessSharedFoldersDifferent) { // --resync needed, is the user performing any operation where a --resync is not required? // flag to ignore --resync requirement bool ignoreResyncRequirement = false; // These flags do not need --resync as no sync operation is needed: --display-config, --list-shared-folders, --get-O365-drive-id, --get-file-link if (cfg.getValueBool("display_config")) ignoreResyncRequirement = true; if (cfg.getValueBool("list_business_shared_folders")) ignoreResyncRequirement = true; if ((!cfg.getValueString("get_o365_drive_id").empty)) ignoreResyncRequirement = true; if ((!cfg.getValueString("get_file_link").empty)) ignoreResyncRequirement = true; // Do we need to ignore a --resync requirement? if (!ignoreResyncRequirement) { // We are not ignoring --requirement if (!cfg.getValueBool("resync")) { // --resync not issued, fail fast log.error("An application configuration change has been detected where a --resync is required"); return EXIT_RESYNC_REQUIRED; } else { // --resync issued, update hashes of config files if they exist if (!cfg.getValueBool("dry_run")) { // not doing a dry run, update hash files if config & sync_list exist if (exists(configFilePath)) { // update hash log.vdebug("updating config hash as --resync issued"); std.file.write(configHashFile, computeQuickXorHash(configFilePath)); // Hash file should only be readable by the user who created it - 0600 permissions needed configHashFile.setAttributes(to!int(convertedPermissionValue)); // create backup copy of current config file log.vdebug("making backup of config file as --resync issued"); std.file.copy(configFilePath, configBackupFile); // File copy should only be readable by the user who created it - 0600 permissions needed configBackupFile.setAttributes(to!int(convertedPermissionValue)); } if (exists(syncListFilePath)) { // update sync_list hash log.vdebug("updating sync_list hash as --resync issued"); std.file.write(syncListHashFile, computeQuickXorHash(syncListFilePath)); // Hash file should only be readable by the user who created it - 0600 permissions needed syncListHashFile.setAttributes(to!int(convertedPermissionValue)); } if (exists(businessSharedFolderFilePath)) { // update business_shared_folders hash log.vdebug("updating business_shared_folders hash as --resync issued"); std.file.write(businessSharedFoldersHashFile, computeQuickXorHash(businessSharedFolderFilePath)); // Hash file should only be readable by the user who created it - 0600 permissions needed businessSharedFoldersHashFile.setAttributes(to!int(convertedPermissionValue)); } } } } } // --dry-run operation notification and database setup // Are we performing any of the following operations? // --dry-run, --list-shared-folders, --get-O365-drive-id, --get-file-link if ((cfg.getValueBool("dry_run")) || (cfg.getValueBool("list_business_shared_folders")) || (!cfg.getValueString("get_o365_drive_id").empty) || (!cfg.getValueString("get_file_link").empty)) { // is this a --list-shared-folders, --get-O365-drive-id, --get-file-link operation if (cfg.getValueBool("dry_run")) { // this is a --dry-run operation log.log("DRY-RUN Configured. Output below shows what 'would' have occurred."); } else { // is this a --list-shared-folders, --get-O365-drive-id, --get-file-link operation log.log("Using dry-run database copy for OneDrive API query"); } // configure databaseFilePathDryRunGlobal databaseFilePathDryRunGlobal = cfg.databaseFilePathDryRun; string dryRunShmFile = databaseFilePathDryRunGlobal ~ "-shm"; string dryRunWalFile = databaseFilePathDryRunGlobal ~ "-wal"; // If the dry run database exists, clean this up if (exists(databaseFilePathDryRunGlobal)) { // remove the existing file log.vdebug("Removing items-dryrun.sqlite3 as it still exists for some reason"); safeRemove(databaseFilePathDryRunGlobal); } // silent cleanup of shm and wal files if they exist if (exists(dryRunShmFile)) { // remove items-dryrun.sqlite3-shm safeRemove(dryRunShmFile); } if (exists(dryRunWalFile)) { // remove items-dryrun.sqlite3-wal safeRemove(dryRunWalFile); } // Make a copy of the original items.sqlite3 for use as the dry run copy if it exists if (exists(cfg.databaseFilePath)) { // in a --dry-run --resync scenario, we should not copy the existing database file if (!cfg.getValueBool("resync")) { // copy the existing DB file to the dry-run copy log.vdebug("Copying items.sqlite3 to items-dryrun.sqlite3 to use for dry run operations"); copy(cfg.databaseFilePath,databaseFilePathDryRunGlobal); } else { // no database copy due to --resync log.vdebug("No database copy created for --dry-run due to --resync also being used"); } } } // sync_dir environment handling to handle ~ expansion properly bool shellEnvSet = false; if ((environment.get("SHELL") == "") && (environment.get("USER") == "")){ log.vdebug("sync_dir: No SHELL or USER environment variable configuration detected"); // No shell or user set, so expandTilde() will fail - usually headless system running under init.d / systemd or potentially Docker // Does the 'currently configured' sync_dir include a ~ if (canFind(cfg.getValueString("sync_dir"), "~")) { // A ~ was found in sync_dir log.vdebug("sync_dir: A '~' was found in sync_dir, using the calculated 'homePath' to replace '~' as no SHELL or USER environment variable set"); syncDir = cfg.homePath ~ strip(cfg.getValueString("sync_dir"), "~"); } else { // No ~ found in sync_dir, use as is log.vdebug("sync_dir: Getting syncDir from config value sync_dir"); syncDir = cfg.getValueString("sync_dir"); } } else { // A shell and user is set, expand any ~ as this will be expanded correctly if present shellEnvSet = true; log.vdebug("sync_dir: Getting syncDir from config value sync_dir"); if (canFind(cfg.getValueString("sync_dir"), "~")) { log.vdebug("sync_dir: A '~' was found in configured sync_dir, automatically expanding as SHELL and USER environment variable is set"); syncDir = expandTilde(cfg.getValueString("sync_dir")); } else { syncDir = cfg.getValueString("sync_dir"); } } // vdebug syncDir as set and calculated log.vdebug("syncDir: ", syncDir); // Configure the logging directory if different from application default // log_dir environment handling to handle ~ expansion properly string logDir = cfg.getValueString("log_dir"); if (logDir != cfg.defaultLogFileDir) { // user modified log_dir entry // if 'log_dir' contains a '~' this needs to be expanded correctly if (canFind(cfg.getValueString("log_dir"), "~")) { // ~ needs to be expanded correctly if (!shellEnvSet) { // No shell or user set, so expandTilde() will fail - usually headless system running under init.d / systemd or potentially Docker log.vdebug("log_dir: A '~' was found in log_dir, using the calculated 'homePath' to replace '~' as no SHELL or USER environment variable set"); logDir = cfg.homePath ~ strip(cfg.getValueString("log_dir"), "~"); } else { // A shell and user is set, expand any ~ as this will be expanded correctly if present log.vdebug("log_dir: A '~' was found in log_dir, using SHELL or USER environment variable to expand '~'"); logDir = expandTilde(cfg.getValueString("log_dir")); } } else { // '~' not found in log_dir entry, use as is logDir = cfg.getValueString("log_dir"); } // update log_dir with normalised path, with '~' expanded correctly cfg.setValueString("log_dir", logDir); } // Configure logging only if enabled if (cfg.getValueBool("enable_logging")){ // Initialise using the configured logging directory log.vlog("Using logfile dir: ", logDir); log.init(logDir); } // Configure whether notifications are used log.setNotifications(cfg.getValueBool("monitor") && !cfg.getValueBool("disable_notifications")); // Application upgrades - skilion version etc if (exists(databaseFilePath)) { if (!cfg.getValueBool("dry_run")) { safeRemove(databaseFilePath); } log.logAndNotify("Database schema changed, resync needed"); cfg.setValueBool("resync", true); } // Handle --logout as separate item, do not 'resync' on a --logout if (cfg.getValueBool("logout")) { log.vdebug("--logout requested"); log.log("Deleting the saved authentication status ..."); if (!cfg.getValueBool("dry_run")) { safeRemove(cfg.refreshTokenFilePath); } // Exit return EXIT_SUCCESS; } // Handle --reauth to re-authenticate the client if (cfg.getValueBool("reauth")) { log.vdebug("--reauth requested"); log.log("Deleting the saved authentication status ... re-authentication requested"); if (!cfg.getValueBool("dry_run")) { safeRemove(cfg.refreshTokenFilePath); } } // Display current application configuration if ((cfg.getValueBool("display_config")) || (cfg.getValueBool("display_running_config"))) { if (cfg.getValueBool("display_running_config")) { writeln("--------------- Application Runtime Configuration ---------------"); } // Display application version writeln("onedrive version = ", strip(import("version"))); // Display all of the pertinent configuration options writeln("Config path = ", cfg.configDirName); // Does a config file exist or are we using application defaults writeln("Config file found in config path = ", exists(configFilePath)); // Is config option drive_id configured? if (cfg.getValueString("drive_id") != ""){ writeln("Config option 'drive_id' = ", cfg.getValueString("drive_id")); } // Config Options as per 'config' file writeln("Config option 'sync_dir' = ", syncDir); // logging and notifications writeln("Config option 'enable_logging' = ", cfg.getValueBool("enable_logging")); writeln("Config option 'log_dir' = ", cfg.getValueString("log_dir")); writeln("Config option 'disable_notifications' = ", cfg.getValueBool("disable_notifications")); writeln("Config option 'min_notify_changes' = ", cfg.getValueLong("min_notify_changes")); // skip files and directory and 'matching' policy writeln("Config option 'skip_dir' = ", cfg.getValueString("skip_dir")); writeln("Config option 'skip_dir_strict_match' = ", cfg.getValueBool("skip_dir_strict_match")); writeln("Config option 'skip_file' = ", cfg.getValueString("skip_file")); writeln("Config option 'skip_dotfiles' = ", cfg.getValueBool("skip_dotfiles")); writeln("Config option 'skip_symlinks' = ", cfg.getValueBool("skip_symlinks")); // --monitor sync process options writeln("Config option 'monitor_interval' = ", cfg.getValueLong("monitor_interval")); writeln("Config option 'monitor_log_frequency' = ", cfg.getValueLong("monitor_log_frequency")); writeln("Config option 'monitor_fullscan_frequency' = ", cfg.getValueLong("monitor_fullscan_frequency")); // sync process and method writeln("Config option 'read_only_auth_scope' = ", cfg.getValueBool("read_only_auth_scope")); writeln("Config option 'dry_run' = ", cfg.getValueBool("dry_run")); writeln("Config option 'upload_only' = ", cfg.getValueBool("upload_only")); writeln("Config option 'download_only' = ", cfg.getValueBool("download_only")); writeln("Config option 'local_first' = ", cfg.getValueBool("local_first")); writeln("Config option 'check_nosync' = ", cfg.getValueBool("check_nosync")); writeln("Config option 'check_nomount' = ", cfg.getValueBool("check_nomount")); writeln("Config option 'resync' = ", cfg.getValueBool("resync")); writeln("Config option 'resync_auth' = ", cfg.getValueBool("resync_auth")); writeln("Config option 'cleanup_local_files' = ", cfg.getValueBool("cleanup_local_files")); // data integrity writeln("Config option 'classify_as_big_delete' = ", cfg.getValueLong("classify_as_big_delete")); writeln("Config option 'disable_upload_validation' = ", cfg.getValueBool("disable_upload_validation")); writeln("Config option 'bypass_data_preservation' = ", cfg.getValueBool("bypass_data_preservation")); writeln("Config option 'no_remote_delete' = ", cfg.getValueBool("no_remote_delete")); writeln("Config option 'remove_source_files' = ", cfg.getValueBool("remove_source_files")); writeln("Config option 'sync_dir_permissions' = ", cfg.getValueLong("sync_dir_permissions")); writeln("Config option 'sync_file_permissions' = ", cfg.getValueLong("sync_file_permissions")); writeln("Config option 'space_reservation' = ", cfg.getValueLong("space_reservation")); // curl operations writeln("Config option 'application_id' = ", cfg.getValueString("application_id")); writeln("Config option 'azure_ad_endpoint' = ", cfg.getValueString("azure_ad_endpoint")); writeln("Config option 'azure_tenant_id' = ", cfg.getValueString("azure_tenant_id")); writeln("Config option 'user_agent' = ", cfg.getValueString("user_agent")); writeln("Config option 'force_http_11' = ", cfg.getValueBool("force_http_11")); writeln("Config option 'debug_https' = ", cfg.getValueBool("debug_https")); writeln("Config option 'rate_limit' = ", cfg.getValueLong("rate_limit")); writeln("Config option 'operation_timeout' = ", cfg.getValueLong("operation_timeout")); // Is sync_list configured ? writeln("Config option 'sync_root_files' = ", cfg.getValueBool("sync_root_files")); if (exists(syncListFilePath)){ writeln("Selective sync 'sync_list' configured = true"); writeln("sync_list contents:"); // Output the sync_list contents auto syncListFile = File(syncListFilePath, "r"); auto range = syncListFile.byLine(); foreach (line; range) { writeln(line); } } else { writeln("Selective sync 'sync_list' configured = false"); } // Is business_shared_folders enabled and configured ? writeln("Config option 'sync_business_shared_folders' = ", cfg.getValueBool("sync_business_shared_folders")); if (exists(businessSharedFolderFilePath)){ writeln("Business Shared Folders configured = true"); writeln("business_shared_folders contents:"); // Output the business_shared_folders contents auto businessSharedFolderFileList = File(businessSharedFolderFilePath, "r"); auto range = businessSharedFolderFileList.byLine(); foreach (line; range) { writeln(line); } } else { writeln("Business Shared Folders configured = false"); } // Are webhooks enabled? writeln("Config option 'webhook_enabled' = ", cfg.getValueBool("webhook_enabled")); if (cfg.getValueBool("webhook_enabled")) { writeln("Config option 'webhook_public_url' = ", cfg.getValueString("webhook_public_url")); writeln("Config option 'webhook_listening_host' = ", cfg.getValueString("webhook_listening_host")); writeln("Config option 'webhook_listening_port' = ", cfg.getValueLong("webhook_listening_port")); writeln("Config option 'webhook_expiration_interval' = ", cfg.getValueLong("webhook_expiration_interval")); writeln("Config option 'webhook_renewal_interval' = ", cfg.getValueLong("webhook_renewal_interval")); } if (cfg.getValueBool("display_running_config")) { writeln("-----------------------------------------------------------------"); } // Do we exit? We only exit if --display-config has been used if (cfg.getValueBool("display_config")) { return EXIT_SUCCESS; } } // --upload-only and --download-only are mutually exclusive and cannot be used together if ((cfg.getValueBool("upload_only")) && (cfg.getValueBool("download_only"))) { // both cannot be true at the same time writeln("ERROR: --upload-only and --download-only are mutually exclusive and cannot be used together.\n"); return EXIT_FAILURE; } // Handle the actual --resync to remove local files if (cfg.getValueBool("resync")) { log.vdebug("--resync requested"); log.vdebug("Testing if we have exclusive access to local database file"); // Are we the only running instance? Test that we can open the database file path itemDb = new ItemDatabase(cfg.databaseFilePath); // did we successfully initialise the database class? if (!itemDb.isDatabaseInitialised()) { // no .. destroy class itemDb = null; // exit application return EXIT_FAILURE; } // If we have exclusive access we will not have exited // destroy access test destroy(itemDb); // delete application sync state log.log("Deleting the saved application sync status ..."); if (!cfg.getValueBool("dry_run")) { safeRemove(cfg.databaseFilePath); safeRemove(cfg.deltaLinkFilePath); safeRemove(cfg.uploadStateFilePath); } } // Test if OneDrive service can be reached, exit if it cant be reached log.vdebug("Testing network to ensure network connectivity to Microsoft OneDrive Service"); online = testNetwork(); if (!online) { // Cant initialise the API as we are not online if (!cfg.getValueBool("monitor")) { // Running as --synchronize log.error("Unable to reach Microsoft OneDrive API service, unable to initialize application\n"); return EXIT_FAILURE; } else { // Running as --monitor log.error("Unable to reach Microsoft OneDrive API service at this point in time, re-trying network tests\n"); // re-try network connection to OneDrive // https://github.com/abraunegg/onedrive/issues/1184 // Back off & retry with incremental delay int retryCount = 10000; int retryAttempts = 1; int backoffInterval = 1; int maxBackoffInterval = 3600; bool retrySuccess = false; while (!retrySuccess){ // retry to access OneDrive API backoffInterval++; int thisBackOffInterval = retryAttempts*backoffInterval; log.vdebug(" Retry Attempt: ", retryAttempts); if (thisBackOffInterval <= maxBackoffInterval) { log.vdebug(" Retry In (seconds): ", thisBackOffInterval); Thread.sleep(dur!"seconds"(thisBackOffInterval)); } else { log.vdebug(" Retry In (seconds): ", maxBackoffInterval); Thread.sleep(dur!"seconds"(maxBackoffInterval)); } // perform the re-rty online = testNetwork(); if (online) { // We are now online log.log("Internet connectivity to Microsoft OneDrive service has been restored"); retrySuccess = true; } else { // We are still offline if (retryAttempts == retryCount) { // we have attempted to re-connect X number of times // false set this to true to break out of while loop retrySuccess = true; } } // Increment & loop around retryAttempts++; } if (!online) { // Not online after 1.2 years of trying log.error("ERROR: Was unable to reconnect to the Microsoft OneDrive service after 10000 attempts lasting over 1.2 years!"); return EXIT_FAILURE; } } } // Check application version and Initialize OneDrive API, check for authorization if (online) { // Check Application Version log.vlog("Checking Application Version ..."); checkApplicationVersion(); // we can only initialise if we are online log.vlog("Initializing the OneDrive API ..."); oneDrive = new OneDriveApi(cfg); onedriveInitialised = oneDrive.init(); oneDrive.printAccessToken = cfg.getValueBool("print_token"); } if (!onedriveInitialised) { log.error("Could not initialize the OneDrive API"); // Use exit scopes to shutdown API return EXIT_UNAUTHORIZED; } // if --synchronize or --monitor not passed in, configure the flag to display help & exit if (cfg.getValueBool("synchronize") || cfg.getValueBool("monitor")) { performSyncOK = true; } // create-directory, remove-directory, source-directory, destination-directory // these are activities that dont perform a sync, so to not generate an error message for these items either if (((cfg.getValueString("create_directory") != "") || (cfg.getValueString("remove_directory") != "")) || ((cfg.getValueString("source_directory") != "") && (cfg.getValueString("destination_directory") != "")) || (cfg.getValueString("get_file_link") != "") || (cfg.getValueString("modified_by") != "") || (cfg.getValueString("create_share_link") != "") || (cfg.getValueString("get_o365_drive_id") != "") || cfg.getValueBool("display_sync_status") || cfg.getValueBool("list_business_shared_folders")) { performSyncOK = true; } // Were acceptable sync operations provided? Was --synchronize or --monitor passed in if (!performSyncOK) { // was the application just authorised? if (cfg.applicationAuthorizeResponseUri) { // Application was just authorised if (exists(cfg.refreshTokenFilePath)) { // OneDrive refresh token exists log.log("\nApplication has been successfully authorised, however no additional command switches were provided.\n"); log.log("Please use 'onedrive --help' for further assistance in regards to running this application.\n"); // Use exit scopes to shutdown API return EXIT_SUCCESS; } else { // we just authorised, but refresh_token does not exist .. probably an auth error log.log("\nApplication has not been successfully authorised. Please check your URI response entry and try again.\n"); return EXIT_FAILURE; } } else { // Application was not just authorised log.log("\n--synchronize or --monitor switches missing from your command line input. Please add one (not both) of these switches to your command line or use 'onedrive --help' for further assistance.\n"); log.log("No OneDrive sync will be performed without one of these two arguments being present.\n"); // Use exit scopes to shutdown API return EXIT_FAILURE; } } // if --synchronize && --monitor passed in, exit & display help as these conflict with each other if (cfg.getValueBool("synchronize") && cfg.getValueBool("monitor")) { writeln(); writeln("ERROR: --synchronize and --monitor cannot be used together"); writeln(); writeln("Please use 'onedrive --help' for further assistance in regards to running this application."); writeln(); // Use exit scopes to shutdown API return EXIT_FAILURE; } // Initialize the item database log.vlog("Opening the item database ..."); // Are we performing any of the following operations? // --dry-run, --list-shared-folders, --get-O365-drive-id, --get-file-link if ((cfg.getValueBool("dry_run")) || (cfg.getValueBool("list_business_shared_folders")) || (!cfg.getValueString("get_o365_drive_id").empty) || (!cfg.getValueString("get_file_link").empty)) { // Load the items-dryrun.sqlite3 file as the database log.vdebug("Using database file: ", asNormalizedPath(databaseFilePathDryRunGlobal)); itemDb = new ItemDatabase(databaseFilePathDryRunGlobal); } else { // Not a dry-run scenario or trying to query O365 Library - should be the default scenario // Load the items.sqlite3 file as the database log.vdebug("Using database file: ", asNormalizedPath(cfg.databaseFilePath)); itemDb = new ItemDatabase(cfg.databaseFilePath); } // did we successfully initialise the database class? if (!itemDb.isDatabaseInitialised()) { // no .. destroy class itemDb = null; // exit application return EXIT_FAILURE; } // What are the permission that have been set for the application? // These are relevant for: // - The ~/OneDrive parent folder or 'sync_dir' configured item // - Any new folder created under ~/OneDrive or 'sync_dir' // - Any new file created under ~/OneDrive or 'sync_dir' // valid permissions are 000 -> 777 - anything else is invalid if ((cfg.getValueLong("sync_dir_permissions") < 0) || (cfg.getValueLong("sync_file_permissions") < 0) || (cfg.getValueLong("sync_dir_permissions") > 777) || (cfg.getValueLong("sync_file_permissions") > 777)) { log.error("ERROR: Invalid 'User|Group|Other' permissions set within config file. Please check."); return EXIT_FAILURE; } else { // debug log output what permissions are being set to log.vdebug("Configuring default new folder permissions as: ", cfg.getValueLong("sync_dir_permissions")); cfg.configureRequiredDirectoryPermisions(); log.vdebug("Configuring default new file permissions as: ", cfg.getValueLong("sync_file_permissions")); cfg.configureRequiredFilePermisions(); } // configure the sync direcory based on syncDir config option log.vlog("All operations will be performed in: ", syncDir); if (!exists(syncDir)) { log.vdebug("syncDir: Configured syncDir is missing. Creating: ", syncDir); try { // Attempt to create the sync dir we have been configured with mkdirRecurse(syncDir); // Configure the applicable permissions for the folder log.vdebug("Setting directory permissions for: ", syncDir); syncDir.setAttributes(cfg.returnRequiredDirectoryPermisions()); } catch (std.file.FileException e) { // Creating the sync directory failed log.error("ERROR: Unable to create local OneDrive syncDir - ", e.msg); // Use exit scopes to shutdown API return EXIT_FAILURE; } } // Change the working directory to the 'sync_dir' configured item chdir(syncDir); // Configure selective sync by parsing and getting a regex for skip_file config component auto selectiveSync = new SelectiveSync(); // load sync_list if it exists if (exists(syncListFilePath)){ log.vdebug("Loading user configured sync_list file ..."); syncListConfigured = true; // list what will be synced auto syncListFile = File(syncListFilePath, "r"); auto range = syncListFile.byLine(); foreach (line; range) { log.vdebug("sync_list: ", line); } // close syncListFile if open if (syncListFile.isOpen()){ // close open file syncListFile.close(); } } selectiveSync.load(syncListFilePath); // load business_shared_folders if it exists if (exists(businessSharedFolderFilePath)){ log.vdebug("Loading user configured business_shared_folders file ..."); // list what will be synced auto businessSharedFolderFileList = File(businessSharedFolderFilePath, "r"); auto range = businessSharedFolderFileList.byLine(); foreach (line; range) { log.vdebug("business_shared_folders: ", line); } } selectiveSync.loadSharedFolders(businessSharedFolderFilePath); // Configure skip_dir, skip_file, skip-dir-strict-match & skip_dotfiles from config entries // Handle skip_dir configuration in config file log.vdebug("Configuring skip_dir ..."); log.vdebug("skip_dir: ", cfg.getValueString("skip_dir")); selectiveSync.setDirMask(cfg.getValueString("skip_dir")); // Was --skip-dir-strict-match configured? log.vdebug("Configuring skip_dir_strict_match ..."); log.vdebug("skip_dir_strict_match: ", cfg.getValueBool("skip_dir_strict_match")); if (cfg.getValueBool("skip_dir_strict_match")) { selectiveSync.setSkipDirStrictMatch(); } // Was --skip-dot-files configured? log.vdebug("Configuring skip_dotfiles ..."); log.vdebug("skip_dotfiles: ", cfg.getValueBool("skip_dotfiles")); if (cfg.getValueBool("skip_dotfiles")) { selectiveSync.setSkipDotfiles(); } // Handle skip_file configuration in config file log.vdebug("Configuring skip_file ..."); // Validate skip_file to ensure that this does not contain an invalid configuration // Do not use a skip_file entry of .* as this will prevent correct searching of local changes to process. foreach(entry; cfg.getValueString("skip_file").split("|")){ if (entry == ".*") { // invalid entry element detected log.logAndNotify("ERROR: Invalid skip_file entry '.*' detected"); return EXIT_FAILURE; } } // All skip_file entries are valid log.vdebug("skip_file: ", cfg.getValueString("skip_file")); selectiveSync.setFileMask(cfg.getValueString("skip_file")); // Implement https://github.com/abraunegg/onedrive/issues/1129 // Force a synchronization of a specific folder, only when using --synchronize --single-directory and ignoring all non-default skip_dir and skip_file rules if ((cfg.getValueBool("synchronize")) && (cfg.getValueString("single_directory") != "") && (cfg.getValueBool("force_sync"))) { log.log("\nWARNING: Overriding application configuration to use application defaults for skip_dir and skip_file due to --synchronize --single-directory --force-sync being used"); // performing this action could have undesirable effects .. the user must accept this risk // what is the risk acceptance? bool resyncRiskAcceptance = false; // need to prompt user char response; // warning message writeln("\nThe use of --force-sync will reconfigure the application to use defaults. This may have untold and unknown future impacts."); writeln("By proceeding in using this option you accept any impacts including any data loss that may occur as a result of using --force-sync."); write("\nAre you sure you wish to proceed with --force-sync [Y/N] "); try { // Attempt to read user response readf(" %c\n", &response); } catch (std.format.FormatException e) { // Caught an error return EXIT_FAILURE; } // Evaluate user repsonse if ((to!string(response) == "y") || (to!string(response) == "Y")) { // User has accepted --force-sync risk to proceed resyncRiskAcceptance = true; // Are you sure you wish .. does not use writeln(); write("\n"); } // Action based on response if (!resyncRiskAcceptance){ // --force-sync not accepted return EXIT_FAILURE; } else { // --force-sync risk accepted // reset set config using function to use application defaults cfg.resetSkipToDefaults(); // update sync engine regex with reset defaults selectiveSync.setDirMask(cfg.getValueString("skip_dir")); selectiveSync.setFileMask(cfg.getValueString("skip_file")); } } // Initialize the sync engine auto sync = new SyncEngine(cfg, oneDrive, itemDb, selectiveSync); try { if (!initSyncEngine(sync)) { // Use exit scopes to shutdown API return EXIT_FAILURE; } else { if ((cfg.getValueString("get_file_link") == "") && (cfg.getValueString("create_share_link") == "")) { // Print out that we are initializing the engine only if we are not grabbing the file link or creating a shareable link log.logAndNotify("Initializing the Synchronization Engine ..."); } } } catch (CurlException e) { if (!cfg.getValueBool("monitor")) { log.log("\nNo Internet connection."); // Use exit scopes to shutdown API return EXIT_FAILURE; } } // if sync list is configured, set to true now that the sync engine is initialised if (syncListConfigured) { sync.setSyncListConfigured(); } // Do we need to configure specific --upload-only options? if (cfg.getValueBool("upload_only")) { // --upload-only was passed in or configured log.vdebug("Configuring uploadOnly flag to TRUE as --upload-only passed in or configured"); sync.setUploadOnly(); // was --no-remote-delete passed in or configured if (cfg.getValueBool("no_remote_delete")) { // Configure the noRemoteDelete flag log.vdebug("Configuring noRemoteDelete flag to TRUE as --no-remote-delete passed in or configured"); sync.setNoRemoteDelete(); } // was --remove-source-files passed in or configured if (cfg.getValueBool("remove_source_files")) { // Configure the localDeleteAfterUpload flag log.vdebug("Configuring localDeleteAfterUpload flag to TRUE as --remove-source-files passed in or configured"); sync.setLocalDeleteAfterUpload(); } } // Do we configure to disable the upload validation routine if (cfg.getValueBool("disable_upload_validation")) sync.setDisableUploadValidation(); // Do we configure to disable the download validation routine if (cfg.getValueBool("disable_download_validation")) sync.setDisableDownloadValidation(); // Has the user enabled to bypass data preservation of renaming local files when there is a conflict? if (cfg.getValueBool("bypass_data_preservation")) { log.log("WARNING: Application has been configured to bypass local data preservation in the event of file conflict."); log.log("WARNING: Local data loss MAY occur in this scenario."); sync.setBypassDataPreservation(); } // Do we configure to clean up local files if using --download-only ? if ((cfg.getValueBool("download_only")) && (cfg.getValueBool("cleanup_local_files"))) { // --download-only and --cleanup-local-files were passed in log.log("WARNING: Application has been configured to cleanup local files that are not present online."); log.log("WARNING: Local data loss MAY occur in this scenario if you are expecting data to remain archived locally."); sync.setCleanupLocalFiles(); // Set the global flag as we will use this as thhe item to be passed into the sync function below cleanupLocalFilesGlobal = true; } // Are we configured to use a National Cloud Deployment if (cfg.getValueString("azure_ad_endpoint") != "") { // value is configured, is it a valid value? if ((cfg.getValueString("azure_ad_endpoint") == "USL4") || (cfg.getValueString("azure_ad_endpoint") == "USL5") || (cfg.getValueString("azure_ad_endpoint") == "DE") || (cfg.getValueString("azure_ad_endpoint") == "CN")) { // valid entries to flag we are using a National Cloud Deployment // National Cloud Deployments do not support /delta as a query // https://docs.microsoft.com/en-us/graph/deployments#supported-features // Flag that we have a valid National Cloud Deployment that cannot use /delta queries sync.setNationalCloudDeployment(); } } // Are we forcing to use /children scan instead of /delta to simulate National Cloud Deployment use of /children? if (cfg.getValueBool("force_children_scan")) { log.vdebug("Forcing client to use /children scan rather than /delta to simulate National Cloud Deployment use of /children"); sync.setNationalCloudDeployment(); } // Do we need to validate the syncDir to check for the presence of a '.nosync' file if (cfg.getValueBool("check_nomount")) { // we were asked to check the mounts if (exists(syncDir ~ "/.nosync")) { log.logAndNotify("ERROR: .nosync file found. Aborting synchronization process to safeguard data."); // Use exit scopes to shutdown API return EXIT_FAILURE; } } // Do we need to create or remove a directory? if ((cfg.getValueString("create_directory") != "") || (cfg.getValueString("remove_directory") != "")) { // create directory if (cfg.getValueString("create_directory") != "") { // create a directory on OneDrive sync.createDirectoryNoSync(cfg.getValueString("create_directory")); } //remove directory if (cfg.getValueString("remove_directory") != "") { // remove a directory on OneDrive sync.deleteDirectoryNoSync(cfg.getValueString("remove_directory")); } } // Are we renaming or moving a directory? if ((cfg.getValueString("source_directory") != "") && (cfg.getValueString("destination_directory") != "")) { // We are renaming or moving a directory sync.renameDirectoryNoSync(cfg.getValueString("source_directory"), cfg.getValueString("destination_directory")); } // Are we obtaining the Office 365 Drive ID for a given Office 365 SharePoint Shared Library? if (cfg.getValueString("get_o365_drive_id") != "") { sync.querySiteCollectionForDriveID(cfg.getValueString("get_o365_drive_id")); // Exit application // Use exit scopes to shutdown API and cleanup data return EXIT_SUCCESS; } // --create-share-link - Are we createing a shareable link for an existing file on OneDrive? if (cfg.getValueString("create_share_link") != "") { // Query OneDrive for the file, and if valid, create a shareable link for the file // By default, the shareable link will be read-only. // If the user adds: // --with-editing-perms // this will create a writeable link bool writeablePermissions = cfg.getValueBool("with_editing_perms"); sync.createShareableLinkForFile(cfg.getValueString("create_share_link"), writeablePermissions); // Exit application // Use exit scopes to shutdown API return EXIT_SUCCESS; } // --get-file-link - Are we obtaining the URL path for a synced file? if (cfg.getValueString("get_file_link") != "") { // Query OneDrive for the file link sync.queryOneDriveForFileDetails(cfg.getValueString("get_file_link"), syncDir, "URL"); // Exit application // Use exit scopes to shutdown API return EXIT_SUCCESS; } // --modified-by - Are we listing the modified-by details of a provided path? if (cfg.getValueString("modified_by") != "") { // Query OneDrive for the file link sync.queryOneDriveForFileDetails(cfg.getValueString("modified_by"), syncDir, "ModifiedBy"); // Exit application // Use exit scopes to shutdown API return EXIT_SUCCESS; } // Are we listing OneDrive Business Shared Folders if (cfg.getValueBool("list_business_shared_folders")) { // Is this a business account type? if (sync.getAccountType() == "business"){ // List OneDrive Business Shared Folders sync.listOneDriveBusinessSharedFolders(); } else { log.error("ERROR: Unsupported account type for listing OneDrive Business Shared Folders"); } // Exit application // Use exit scopes to shutdown API return EXIT_SUCCESS; } // Are we going to sync OneDrive Business Shared Folders if (cfg.getValueBool("sync_business_shared_folders")) { // Is this a business account type? if (sync.getAccountType() == "business"){ // Configure flag to sync business folders sync.setSyncBusinessFolders(); } else { log.error("ERROR: Unsupported account type for syncing OneDrive Business Shared Folders"); } } // Are we displaying the sync status of the client? if (cfg.getValueBool("display_sync_status")) { string remotePath = "/"; // Are we doing a single directory check? if (cfg.getValueString("single_directory") != ""){ // Need two different path strings here remotePath = cfg.getValueString("single_directory"); } sync.queryDriveForChanges(remotePath); } // Are we performing a sync, or monitor operation? if ((cfg.getValueBool("synchronize")) || (cfg.getValueBool("monitor"))) { // Initialise the monitor class, so that we can do more granular inotify handling when performing the actual sync // needed for --synchronize and --monitor handling Monitor m = new Monitor(selectiveSync); if (cfg.getValueBool("synchronize")) { if (online) { // Check user entry for local path - the above chdir means we are already in ~/OneDrive/ thus singleDirectory is local to this path if (cfg.getValueString("single_directory") != "") { // Does the directory we want to sync actually exist? if (!exists(cfg.getValueString("single_directory"))) { // The requested path to use with --single-directory does not exist locally within the configured 'sync_dir' log.logAndNotify("WARNING: The requested path for --single-directory does not exist locally. Creating requested path within ", syncDir); // Make the required --single-directory path locally string singleDirectoryPath = cfg.getValueString("single_directory"); mkdirRecurse(singleDirectoryPath); // Configure the applicable permissions for the folder log.vdebug("Setting directory permissions for: ", singleDirectoryPath); singleDirectoryPath.setAttributes(cfg.returnRequiredDirectoryPermisions()); } } // perform a --synchronize sync // fullScanRequired = false, for final true-up // but if we have sync_list configured, use syncListConfigured which = true performSync(sync, cfg.getValueString("single_directory"), cfg.getValueBool("download_only"), cfg.getValueBool("local_first"), cfg.getValueBool("upload_only"), LOG_NORMAL, false, syncListConfigured, displaySyncOptions, cfg.getValueBool("monitor"), m, cleanupLocalFilesGlobal); // Write WAL and SHM data to file for this sync log.vdebug("Merge contents of WAL and SHM files into main database file"); itemDb.performVacuum(); } } if (cfg.getValueBool("monitor")) { log.logAndNotify("Initializing monitor ..."); log.log("OneDrive monitor interval (seconds): ", cfg.getValueLong("monitor_interval")); m.onDirCreated = delegate(string path) { // Handle .folder creation if skip_dotfiles is enabled if ((cfg.getValueBool("skip_dotfiles")) && (selectiveSync.isDotFile(path))) { log.vlog("[M] Skipping watching local path - .folder found & --skip-dot-files enabled: ", path); } else { log.vlog("[M] Local directory created: ", path); try { sync.scanForDifferences(path); } catch (CurlException e) { log.vlog("Offline, cannot create remote dir!"); } catch(Exception e) { log.logAndNotify("Cannot create remote directory: ", e.msg); } } }; m.onFileChanged = delegate(string path) { log.vlog("[M] Local file changed: ", path); try { sync.scanForDifferences(path); } catch (CurlException e) { log.vlog("Offline, cannot upload changed item!"); } catch(Exception e) { log.logAndNotify("Cannot upload file changes/creation: ", e.msg); } }; m.onDelete = delegate(string path) { log.log("Received inotify delete event from operating system .. attempting item deletion as requested"); log.vlog("[M] Local item deleted: ", path); try { sync.deleteByPath(path); } catch (CurlException e) { log.vlog("Offline, cannot delete item!"); } catch(SyncException e) { if (e.msg == "The item to delete is not in the local database") { log.vlog("Item cannot be deleted from OneDrive because it was not found in the local database"); } else { log.logAndNotify("Cannot delete remote item: ", e.msg); } } catch(Exception e) { log.logAndNotify("Cannot delete remote item: ", e.msg); } }; m.onMove = delegate(string from, string to) { log.vlog("[M] Local item moved: ", from, " -> ", to); try { // Handle .folder -> folder if skip_dotfiles is enabled if ((cfg.getValueBool("skip_dotfiles")) && (selectiveSync.isDotFile(from))) { // .folder -> folder handling - has to be handled as a new folder sync.scanForDifferences(to); } else { sync.uploadMoveItem(from, to); } } catch (CurlException e) { log.vlog("Offline, cannot move item!"); } catch(Exception e) { log.logAndNotify("Cannot move item: ", e.msg); } }; signal(SIGINT, &exitHandler); signal(SIGTERM, &exitHandler); // attempt to initialise monitor class if (!cfg.getValueBool("download_only")) { try { m.init(cfg, cfg.getValueLong("verbose") > 0, cfg.getValueBool("skip_symlinks"), cfg.getValueBool("check_nosync")); } catch (MonitorException e) { // monitor initialisation failed log.error("ERROR: ", e.msg); oneDrive.shutdown(); return EXIT_FAILURE; } } // monitor loop bool performMonitor = true; ulong monitorLoopFullCount = 0; immutable auto checkInterval = dur!"seconds"(cfg.getValueLong("monitor_interval")); immutable auto githubCheckInterval = dur!"seconds"(86400); immutable long logInterval = cfg.getValueLong("monitor_log_frequency"); immutable long fullScanFrequency = cfg.getValueLong("monitor_fullscan_frequency"); MonoTime lastCheckTime = MonoTime.currTime(); MonoTime lastGitHubCheckTime = MonoTime.currTime(); long logMonitorCounter = 0; long fullScanCounter = 0; // set fullScanRequired to true so that at application startup we perform a full walk bool fullScanRequired = true; bool syncListConfiguredFullScanOverride = false; // if sync list is configured, set to true if (syncListConfigured) { // sync list is configured syncListConfiguredFullScanOverride = true; } immutable bool webhookEnabled = cfg.getValueBool("webhook_enabled"); while (performMonitor) { if (!cfg.getValueBool("download_only")) { try { m.update(online); } catch (MonitorException e) { // Catch any exceptions thrown by inotify / monitor engine log.error("ERROR: The following inotify error was generated: ", e.msg); } } // Check for notifications pushed from Microsoft to the webhook bool notificationReceived = false; if (webhookEnabled) { // Create a subscription on the first run, or renew the subscription // on subsequent runs when it is about to expire. oneDrive.createOrRenewSubscription(); // Process incoming notifications if any. // Empirical evidence shows that Microsoft often sends multiple // notifications for one single change, so we need a loop to exhaust // all signals that were queued up by the webhook. The notifications // do not contain any actual changes, and we will always rely do the // delta endpoint to sync to latest. Therefore, only one sync run is // good enough to catch up for multiple notifications. for (int signalCount = 0;; signalCount++) { const auto signalExists = receiveTimeout(dur!"seconds"(-1), (ulong _) {}); if (signalExists) { notificationReceived = true; } else { if (notificationReceived) { log.log("Received ", signalCount," refresh signals from the webhook"); } break; } } } auto currTime = MonoTime.currTime(); // has monitor_interval elapsed or are we at application startup / monitor startup? // in a --resync scenario, if we have not 're-populated' the database, valid changes will get skipped: // Monitor directory: ./target // Monitor directory: target/2eVPInOMTFNXzRXeNMEoJch5OR9XpGby // [M] Item moved: random_files/2eVPInOMTFNXzRXeNMEoJch5OR9XpGby -> target/2eVPInOMTFNXzRXeNMEoJch5OR9XpGby // Moving random_files/2eVPInOMTFNXzRXeNMEoJch5OR9XpGby to target/2eVPInOMTFNXzRXeNMEoJch5OR9XpGby // Skipping uploading this new file as parent path is not in the database: target/2eVPInOMTFNXzRXeNMEoJch5OR9XpGby // 'target' should be in the DB, it should also exist online, but because of --resync, it does not exist in the database thus parent check fails if (notificationReceived || (currTime - lastCheckTime > checkInterval) || (monitorLoopFullCount == 0)) { // Check Application Version against GitHub once per day if (currTime - lastGitHubCheckTime > githubCheckInterval) { // --monitor GitHub Application Version Check time expired checkApplicationVersion(); // update when we have performed this check lastGitHubCheckTime = MonoTime.currTime(); } // monitor sync loop logOutputMessage = "################################################## NEW LOOP ##################################################"; if (displaySyncOptions) { log.log(logOutputMessage); } else { log.vdebug(logOutputMessage); } // Increment monitorLoopFullCount monitorLoopFullCount++; // Display memory details at start of loop if (displayMemoryUsage) { log.displayMemoryUsagePreGC(); } // log monitor output suppression logMonitorCounter += 1; if (logMonitorCounter > logInterval) { logMonitorCounter = 1; } // do we perform a full scan of sync_dir and database integrity check? fullScanCounter += 1; // fullScanFrequency = 'monitor_fullscan_frequency' from config if (fullScanCounter > fullScanFrequency){ // 'monitor_fullscan_frequency' counter has exceeded fullScanCounter = 1; // set fullScanRequired = true due to 'monitor_fullscan_frequency' counter has been exceeded fullScanRequired = true; // are we using sync_list? if (syncListConfigured) { // sync list is configured syncListConfiguredFullScanOverride = true; } } if (displaySyncOptions) { // sync option handling per sync loop log.log("fullScanCounter = ", fullScanCounter); log.log("syncListConfigured = ", syncListConfigured); log.log("fullScanRequired = ", fullScanRequired); log.log("syncListConfiguredFullScanOverride = ", syncListConfiguredFullScanOverride); } else { // sync option handling per sync loop via debug log.vdebug("fullScanCounter = ", fullScanCounter); log.vdebug("syncListConfigured = ", syncListConfigured); log.vdebug("fullScanRequired = ", fullScanRequired); log.vdebug("syncListConfiguredFullScanOverride = ", syncListConfiguredFullScanOverride); } try { if (!initSyncEngine(sync)) { // Use exit scopes to shutdown API return EXIT_FAILURE; } try { string startMessage = "Starting a sync with OneDrive"; string finishMessage = "Sync with OneDrive is complete"; // perform a --monitor sync if ((cfg.getValueLong("verbose") > 0) || (logMonitorCounter == logInterval) || (fullScanRequired) ) { // log to console and log file if enabled log.log(startMessage); } else { // log file only if enabled so we know when a sync started when not using --verbose log.fileOnly(startMessage); } performSync(sync, cfg.getValueString("single_directory"), cfg.getValueBool("download_only"), cfg.getValueBool("local_first"), cfg.getValueBool("upload_only"), (logMonitorCounter == logInterval ? MONITOR_LOG_QUIET : MONITOR_LOG_SILENT), fullScanRequired, syncListConfiguredFullScanOverride, displaySyncOptions, cfg.getValueBool("monitor"), m, cleanupLocalFilesGlobal); if (!cfg.getValueBool("download_only")) { // discard all events that may have been generated by the sync that have not already been handled try { m.update(false); } catch (MonitorException e) { // Catch any exceptions thrown by inotify / monitor engine log.error("ERROR: The following inotify error was generated: ", e.msg); } } if ((cfg.getValueLong("verbose") > 0) || (logMonitorCounter == logInterval) || (fullScanRequired) ) { // log to console and log file if enabled log.log(finishMessage); } else { // log file only if enabled so we know when a sync completed when not using --verbose log.fileOnly(finishMessage); } } catch (CurlException e) { // we already tried three times in the performSync routine // if we still have problems, then the sync handle might have // gone stale and we need to re-initialize the sync engine log.log("Persistent connection errors, reinitializing connection"); sync.reset(); } } catch (CurlException e) { log.log("Cannot initialize connection to OneDrive"); } // performSync complete, set lastCheckTime to current time lastCheckTime = MonoTime.currTime(); // Display memory details before cleanup if (displayMemoryUsage) log.displayMemoryUsagePreGC(); // Perform Garbage Cleanup GC.collect(); // Display memory details after cleanup if (displayMemoryUsage) log.displayMemoryUsagePostGC(); // If we did a full scan, make sure we merge the conents of the WAL and SHM to disk if (fullScanRequired) { // Write WAL and SHM data to file for this loop log.vdebug("Merge contents of WAL and SHM files into main database file"); itemDb.performVacuum(); } // reset fullScanRequired and syncListConfiguredFullScanOverride fullScanRequired = false; if (syncListConfigured) syncListConfiguredFullScanOverride = false; // monitor loop complete logOutputMessage = "################################################ LOOP COMPLETE ###############################################"; // Handle display options if (displaySyncOptions) { log.log(logOutputMessage); } else { log.vdebug(logOutputMessage); } // Developer break via config option if (cfg.getValueLong("monitor_max_loop") > 0) { // developer set option to limit --monitor loops if (monitorLoopFullCount == (cfg.getValueLong("monitor_max_loop"))) { performMonitor = false; log.log("Exiting after ", monitorLoopFullCount, " loops due to developer set option"); } } } // Sleep the monitor thread for 1 second, loop around and pick up any inotify changes Thread.sleep(dur!"seconds"(1)); } } } // Exit application // Use exit scopes to shutdown API return EXIT_SUCCESS; } void cleanupDryRunDatabase(string databaseFilePathDryRun) { // cleanup dry-run data log.vdebug("Running cleanupDryRunDatabase"); string dryRunShmFile = databaseFilePathDryRun ~ "-shm"; string dryRunWalFile = databaseFilePathDryRun ~ "-wal"; if (exists(databaseFilePathDryRun)) { // remove the file log.vdebug("Removing items-dryrun.sqlite3 as dry run operations complete"); // remove items-dryrun.sqlite3 safeRemove(databaseFilePathDryRun); } // silent cleanup of shm and wal files if they exist if (exists(dryRunShmFile)) { // remove items-dryrun.sqlite3-shm safeRemove(dryRunShmFile); } if (exists(dryRunWalFile)) { // remove items-dryrun.sqlite3-wal safeRemove(dryRunWalFile); } } bool initSyncEngine(SyncEngine sync) { try { sync.init(); } catch (OneDriveException e) { if (e.httpStatusCode == 400 || e.httpStatusCode == 401) { // Authorization is invalid log.log("\nAuthorization token invalid, use --reauth to authorize the client again\n"); return false; } if (e.httpStatusCode >= 500) { // There was a HTTP 5xx Server Side Error, message already printed return false; } } return true; } // try to synchronize the folder three times void performSync(SyncEngine sync, string singleDirectory, bool downloadOnly, bool localFirst, bool uploadOnly, long logLevel, bool fullScanRequired, bool syncListConfiguredFullScanOverride, bool displaySyncOptions, bool monitorEnabled, Monitor m, bool cleanupLocalFiles) { int count; string remotePath = "/"; string localPath = "."; string logOutputMessage; // performSync API scan triggers log.vdebug("performSync API scan triggers"); log.vdebug("-----------------------------"); log.vdebug("fullScanRequired = ", fullScanRequired); log.vdebug("syncListConfiguredFullScanOverride = ", syncListConfiguredFullScanOverride); log.vdebug("-----------------------------"); // Are we doing a single directory sync? if (singleDirectory != ""){ // Need two different path strings here remotePath = singleDirectory; localPath = singleDirectory; // Set flag for singleDirectoryScope for change handling sync.setSingleDirectoryScope(); } // Due to Microsoft Sharepoint 'enrichment' of files, we try to download the Microsoft modified file automatically // Set flag if we are in upload only state to handle this differently // See: https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details if (uploadOnly) sync.setUploadOnly(); do { try { // starting a sync logOutputMessage = "################################################## NEW SYNC ##################################################"; if (displaySyncOptions) { log.log(logOutputMessage); } else { log.vdebug(logOutputMessage); } if (singleDirectory != ""){ // we were requested to sync a single directory log.vlog("Syncing changes from this selected path: ", singleDirectory); if (uploadOnly){ // Upload Only of selected single directory if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from selected local path only - NOT syncing data changes from OneDrive ..."); sync.scanForDifferences(localPath); } else { // No upload only if (localFirst) { // Local First if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from selected local path first before downloading changes from OneDrive ..."); sync.scanForDifferences(localPath); sync.applyDifferencesSingleDirectory(remotePath); } else { // OneDrive First if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from selected OneDrive path ..."); sync.applyDifferencesSingleDirectory(remotePath); // Is this a --download-only --cleanup-local-files request? // If yes, scan for local changes - but --cleanup-local-files is being used, a further flag will trigger local file deletes rather than attempt to upload files to OneDrive if (cleanupLocalFiles) { // --download-only and --cleanup-local-files were passed in log.log("Searching local filesystem for extra files and folders which need to be removed"); sync.scanForDifferencesFilesystemScan(localPath); } else { // is this a --download-only request? if (!downloadOnly) { // process local changes sync.scanForDifferences(localPath); // ensure that the current remote state is updated locally sync.applyDifferencesSingleDirectory(remotePath); } } } } } else { // no single directory sync if (uploadOnly){ // Upload Only of entire sync_dir if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from local path only - NOT syncing data changes from OneDrive ..."); sync.scanForDifferences(localPath); } else { // No upload only string syncCallLogOutput; if (localFirst) { // sync local files first before downloading from OneDrive if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from local path first before downloading changes from OneDrive ..."); sync.scanForDifferences(localPath); // if syncListConfiguredFullScanOverride = true if (syncListConfiguredFullScanOverride) { // perform a full walk of OneDrive objects sync.applyDifferences(syncListConfiguredFullScanOverride); } else { // perform a walk based on if a full scan is required sync.applyDifferences(fullScanRequired); } } else { // sync from OneDrive first before uploading files to OneDrive if ((logLevel < MONITOR_LOG_SILENT) || (fullScanRequired)) log.log("Syncing changes from OneDrive ..."); // For the initial sync, always use the delta link so that we capture all the right delta changes including adds, moves & deletes logOutputMessage = "Initial Scan: Call OneDrive Delta API for delta changes as compared to last successful sync."; syncCallLogOutput = "Calling sync.applyDifferences(false);"; if (displaySyncOptions) { log.log(logOutputMessage); log.log(syncCallLogOutput); } else { log.vdebug(logOutputMessage); log.vdebug(syncCallLogOutput); } sync.applyDifferences(false); // Is this a --download-only --cleanup-local-files request? // If yes, scan for local changes - but --cleanup-local-files is being used, a further flag will trigger local file deletes rather than attempt to upload files to OneDrive if (cleanupLocalFiles) { // --download-only and --cleanup-local-files were passed in log.log("Searching local filesystem for extra files and folders which need to be removed"); sync.scanForDifferencesFilesystemScan(localPath); } else { // is this a --download-only request? if (!downloadOnly) { // process local changes walking the entire path checking for changes // in monitor mode all local changes are captured via inotify // thus scanning every 'monitor_interval' (default 300 seconds) for local changes is excessive and not required logOutputMessage = "Process local filesystem (sync_dir) for file changes as compared to database entries"; syncCallLogOutput = "Calling sync.scanForDifferences(localPath);"; if (displaySyncOptions) { log.log(logOutputMessage); log.log(syncCallLogOutput); } else { log.vdebug(logOutputMessage); log.vdebug(syncCallLogOutput); } // What sort of local scan do we want to do? // In --monitor mode, when performing the DB scan, a race condition occurs where by if a file or folder is moved during this process // the inotify event is discarded once performSync() is finished (see m.update(false) above), so these events need to be handled // This can be remediated by breaking the DB and file system scan into separate processes, and handing any applicable inotify events in between if (!monitorEnabled) { // --synchronize in use // standard process flow sync.scanForDifferences(localPath); } else { // --monitor in use // Use individual calls with inotify checks between to avoid a race condition between these 2 functions // Database scan integrity check to compare DB data vs actual content on disk to ensure what we think is local, is local // and that the data 'hash' as recorded in the DB equals the hash of the actual content // This process can be extremely expensive time and CPU processing wise // // fullScanRequired is set to TRUE when the application starts up, or the config option 'monitor_fullscan_frequency' count is reached // By default, 'monitor_fullscan_frequency' = 12, and 'monitor_interval' = 300, meaning that by default, a full database consistency check // is done once an hour. // // To change this behaviour adjust 'monitor_interval' and 'monitor_fullscan_frequency' to desired values in the application config file if (fullScanRequired) { log.log("Performing a database consistency and integrity check on locally stored data ... "); sync.scanForDifferencesDatabaseScan(localPath); // handle any inotify events that occured 'whilst' we were scanning the database m.update(true); } else { log.vdebug("NOT performing Database Integrity Check .. fullScanRequired = FALSE"); m.update(true); } // Filesystem walk to find new files not uploaded log.vdebug("Searching local filesystem for new data"); sync.scanForDifferencesFilesystemScan(localPath); // handle any inotify events that occured 'whilst' we were scanning the local filesystem m.update(true); } // At this point, all OneDrive changes / local changes should be uploaded and in sync // This MAY not be the case when using sync_list, thus a full walk of OneDrive ojects is required // --synchronize & no sync_list : fullScanRequired = false, syncListConfiguredFullScanOverride = false // --synchronize & sync_list in use : fullScanRequired = false, syncListConfiguredFullScanOverride = true // --monitor loops around 12 iterations. On the 1st loop, sets fullScanRequired = true, syncListConfiguredFullScanOverride = true if requried // --monitor & no sync_list (loop #1) : fullScanRequired = true, syncListConfiguredFullScanOverride = false // --monitor & no sync_list (loop #2 - #12) : fullScanRequired = false, syncListConfiguredFullScanOverride = false // --monitor & sync_list in use (loop #1) : fullScanRequired = true, syncListConfiguredFullScanOverride = true // --monitor & sync_list in use (loop #2 - #12) : fullScanRequired = false, syncListConfiguredFullScanOverride = false // Do not perform a full walk of the OneDrive objects if ((!fullScanRequired) && (!syncListConfiguredFullScanOverride)){ logOutputMessage = "Final True-Up: Do not perform a full walk of the OneDrive objects - not required"; syncCallLogOutput = "Calling sync.applyDifferences(false);"; if (displaySyncOptions) { log.log(logOutputMessage); log.log(syncCallLogOutput); } else { log.vdebug(logOutputMessage); log.vdebug(syncCallLogOutput); } sync.applyDifferences(false); } // Perform a full walk of OneDrive objects because sync_list is in use / or trigger was set in --monitor loop if ((!fullScanRequired) && (syncListConfiguredFullScanOverride)){ logOutputMessage = "Final True-Up: Perform a full walk of OneDrive objects because sync_list is in use / or trigger was set in --monitor loop"; syncCallLogOutput = "Calling sync.applyDifferences(true);"; if (displaySyncOptions) { log.log(logOutputMessage); log.log(syncCallLogOutput); } else { log.vdebug(logOutputMessage); log.vdebug(syncCallLogOutput); } sync.applyDifferences(true); } // Perform a full walk of OneDrive objects because a full scan was required if ((fullScanRequired) && (!syncListConfiguredFullScanOverride)){ logOutputMessage = "Final True-Up: Perform a full walk of OneDrive objects because a full scan was required"; syncCallLogOutput = "Calling sync.applyDifferences(true);"; if (displaySyncOptions) { log.log(logOutputMessage); log.log(syncCallLogOutput); } else { log.vdebug(logOutputMessage); log.vdebug(syncCallLogOutput); } sync.applyDifferences(true); } // Perform a full walk of OneDrive objects because a full scan was required and sync_list is in use and trigger was set in --monitor loop if ((fullScanRequired) && (syncListConfiguredFullScanOverride)){ logOutputMessage = "Final True-Up: Perform a full walk of OneDrive objects because a full scan was required and sync_list is in use and trigger was set in --monitor loop"; syncCallLogOutput = "Calling sync.applyDifferences(true);"; if (displaySyncOptions) { log.log(logOutputMessage); log.log(syncCallLogOutput); } else { log.vdebug(logOutputMessage); log.vdebug(syncCallLogOutput); } sync.applyDifferences(true); } } } } } } // sync is complete logOutputMessage = "################################################ SYNC COMPLETE ###############################################"; if (displaySyncOptions) { log.log(logOutputMessage); } else { log.vdebug(logOutputMessage); } count = -1; } catch (Exception e) { if (++count == 3) { log.log("Giving up on sync after three attempts: ", e.msg); throw e; } else log.log("Retry sync count: ", count, ": ", e.msg); } } while (count != -1); } // getting around the @nogc problem // https://p0nce.github.io/d-idioms/#Bypassing-@nogc auto assumeNoGC(T) (T t) if (isFunctionPointer!T || isDelegate!T) { enum attrs = functionAttributes!T | FunctionAttribute.nogc; return cast(SetFunctionAttributes!(T, functionLinkage!T, attrs)) t; } extern(C) nothrow @nogc @system void exitHandler(int value) { try { assumeNoGC ( () { log.log("Got termination signal, performing clean up"); // if initialised, shut down the HTTP instance if (onedriveInitialised) { log.log("Shutting down the HTTP instance"); oneDrive.shutdown(); } // was itemDb initialised? if (itemDb.isDatabaseInitialised()) { // Make sure the .wal file is incorporated into the main db before we exit log.log("Shutting down db connection and merging temporary data"); itemDb.performVacuum(); destroy(itemDb); } })(); } catch(Exception e) {} exit(0); }