mirror of
https://github.com/abraunegg/onedrive
synced 2026-03-14 14:35:46 +01:00
2097 lines
93 KiB
D
2097 lines
93 KiB
D
// What is this module called?
|
|
module main;
|
|
|
|
// What does this module require to function?
|
|
import core.memory;
|
|
import core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit;
|
|
import core.sys.posix.signal;
|
|
import core.thread;
|
|
import core.time;
|
|
import std.algorithm;
|
|
import std.concurrency;
|
|
import std.conv;
|
|
import std.datetime;
|
|
import std.file;
|
|
import std.getopt;
|
|
import std.net.curl: CurlException;
|
|
import std.parallelism;
|
|
import std.path;
|
|
import std.process;
|
|
import std.socket: SocketException;
|
|
import std.stdio;
|
|
import std.string;
|
|
import std.traits;
|
|
|
|
// What other modules that we have created do we need to import?
|
|
import config;
|
|
import log;
|
|
import curlEngine;
|
|
import util;
|
|
import onedrive;
|
|
import syncEngine;
|
|
import itemdb;
|
|
import clientSideFiltering;
|
|
import monitor;
|
|
import webhook;
|
|
import intune;
|
|
import socketio;
|
|
|
|
// What other constant variables do we require?
|
|
const int EXIT_RESYNC_REQUIRED = 126;
|
|
|
|
// Class objects
|
|
ApplicationConfig appConfig;
|
|
OneDriveWebhook oneDriveWebhook;
|
|
SyncEngine syncEngineInstance;
|
|
ItemDatabase itemDB;
|
|
ClientSideFiltering selectiveSync;
|
|
Monitor filesystemMonitor;
|
|
OneDriveSocketIo oneDriveSocketIo;
|
|
|
|
// Class variables
|
|
// Flag for performing a synchronised shutdown
|
|
bool shutdownInProgress = false;
|
|
// Flag if a --dry-run is being performed, as, on shutdown, once config is destroyed, we have no reference here
|
|
bool dryRun = false;
|
|
// Configure the runtime database file path so that it is available to us on shutdown so objects can be destroyed and removed if required
|
|
// - Typically this will be the default, but in a --dry-run scenario, we use a separate database file
|
|
string runtimeDatabaseFile = "";
|
|
// Flag for if we are performing filesystem monitoring
|
|
bool performFileSystemMonitoring = false;
|
|
// Flag for if we perform a database vacuum. This gets set to false if we have not performed a 'no-sync' task
|
|
bool performDatabaseVacuum = true;
|
|
// Flag if SIGTERM is used
|
|
bool sigtermHandlerTriggered = false;
|
|
|
|
int main(string[] cliArgs) {
|
|
// Application Start Time - used during monitor loop to detail how long it has been running for
|
|
auto applicationStartTime = Clock.currTime();
|
|
// Disable buffering on stdout - this is needed so that when we are using plain write() it will go to the terminal without flushing
|
|
stdout.setvbuf(0, _IONBF);
|
|
|
|
// Required main function variables
|
|
string genericHelpMessage = "Please use 'onedrive --help' for further assistance in regards to running this application.";
|
|
// If the user passes in --confdir we need to store this as a variable
|
|
string confdirOption = "";
|
|
// running as what user?
|
|
string runtimeUserName = "";
|
|
// Are we online?
|
|
bool online = false;
|
|
// Does the operating environment have shell environment variables set
|
|
bool shellEnvSet = false;
|
|
// What is the runtime synchronisation directory that will be used
|
|
// Typically this will be '~/OneDrive' .. however tilde expansion is unreliable
|
|
string runtimeSyncDirectory = "";
|
|
// Verbosity Logging Count - this defines if verbose or debug logging is being used
|
|
long verbosityCount = 0;
|
|
// Monitor loop failures
|
|
bool monitorFailures = false;
|
|
// Help requested
|
|
bool helpRequested = false;
|
|
// Did the user specify --sync or --monitor
|
|
bool syncOrMonitorMissing = false;
|
|
// Was a no-sync type operation requested
|
|
bool noSyncTaskOperationRequested = false;
|
|
|
|
// DEVELOPER OPTIONS OUTPUT VARIABLES
|
|
bool displayMemoryUsage = false;
|
|
bool displaySyncOptions = false;
|
|
|
|
// Application Version
|
|
immutable string applicationVersion = "onedrive " ~ strip(import("version"));
|
|
|
|
// Define 'exit' and 'failure' scopes
|
|
scope(exit) {
|
|
// Detail what scope was called
|
|
if (debugLogging) {addLogEntry("Exit scope was called", ["debug"]);}
|
|
// Perform synchronised exit
|
|
performSynchronisedExitProcess("exitScope");
|
|
// Setup signal handling for the exit scope
|
|
setupExitScopeSignalHandler();
|
|
}
|
|
|
|
scope(failure) {
|
|
// Detail what scope was called
|
|
if (debugLogging) {addLogEntry("Failure scope was called", ["debug"]);}
|
|
// Perform synchronised exit
|
|
performSynchronisedExitProcess("failureScope");
|
|
// Setup signal handling for the exit scope
|
|
setupExitScopeSignalHandler();
|
|
}
|
|
|
|
// Read in application options as passed in
|
|
try {
|
|
bool printVersion = false;
|
|
auto cliOptions = getopt(
|
|
cliArgs,
|
|
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)", &verbosityCount,
|
|
"version", "Print the version and exit", &printVersion
|
|
);
|
|
|
|
// Print help and exit
|
|
if (cliOptions.helpWanted) {
|
|
cliArgs ~= "--help";
|
|
helpRequested = true;
|
|
}
|
|
// Print the version and exit
|
|
if (printVersion) {
|
|
writeln(applicationVersion);
|
|
exit(EXIT_SUCCESS);
|
|
}
|
|
} catch (GetOptException e) {
|
|
// Option errors
|
|
writeln(e.msg);
|
|
writeln(genericHelpMessage);
|
|
return EXIT_FAILURE;
|
|
} catch (Exception e) {
|
|
// Generic error
|
|
writeln(e.msg);
|
|
writeln(genericHelpMessage);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
// Determine the application logging verbosity
|
|
// - As these flags are used to reduce application processing when not required, specifically in a 'debug' scenario, both verboseLogging and debugLogging need to be enabled
|
|
if (verbosityCount == 1) { verboseLogging = true;} // set __gshared bool verboseLogging in log.d
|
|
if (verbosityCount >= 2) { verboseLogging = true; debugLogging = true;} // set __gshared bool verboseLogging & debugLogging in log.d
|
|
|
|
// Initialize the application logging class, as we know the application verbosity level
|
|
// If we need to enable logging to a file, we can only do this once we know the application configuration which is done slightly later on
|
|
initialiseLogging(verboseLogging, debugLogging);
|
|
|
|
// Log application start time, log line has start time
|
|
if (debugLogging) {addLogEntry("Application started", ["debug"]);}
|
|
|
|
// Who are we running as? This will print the ProcessID, UID, GID and username the application is running as
|
|
runtimeUserName = getUserName();
|
|
|
|
// Print the application version and how this was compiled as soon as possible
|
|
if (debugLogging) {
|
|
addLogEntry("Application Version: " ~ applicationVersion, ["debug"]);
|
|
addLogEntry("Application Compiled With: " ~ compilerDetails(), ["debug"]);
|
|
|
|
// How was this application started - what options were passed in
|
|
addLogEntry("Passed in 'cliArgs': " ~ to!string(cliArgs), ["debug"]);
|
|
addLogEntry("Note: --confdir and --verbose are not listed in 'cliArgs' array", ["debug"]);
|
|
addLogEntry("Passed in --confdir if present: " ~ confdirOption, ["debug"]);
|
|
addLogEntry("Passed in --verbose count if present: " ~ to!string(verbosityCount), ["debug"]);
|
|
}
|
|
|
|
// Create a new AppConfig object with default values,
|
|
appConfig = new ApplicationConfig();
|
|
// Update the default application configuration with the verbosity count so this can be used throughout the application as needed
|
|
appConfig.verbosityCount = verbosityCount;
|
|
|
|
// Initialise the application configuration, utilising --confdir if it was passed in
|
|
// Otherwise application defaults will be used to configure the application
|
|
if (!appConfig.initialise(confdirOption, helpRequested)) {
|
|
// There was an error loading the user specified application configuration
|
|
// Error message already printed
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
// Update the current runtime application configuration (default or 'config' file read in options) from any passed in command line arguments
|
|
appConfig.updateFromArgs(cliArgs);
|
|
|
|
// Set the default thread pool value based on configuration or maximum logical CPUs
|
|
setDefaultApplicationThreads();
|
|
|
|
// If --debug-https has been used, set the applicable flag
|
|
debugHTTPSResponse = appConfig.getValueBool("debug_https"); // set __gshared bool debugHTTPSResponse in log.d now that we have read-in any CLI arguments
|
|
|
|
// Read in the configured 'sync_dir' from appConfig with '~' if present correctly expanded based on the user environment
|
|
runtimeSyncDirectory = appConfig.initialiseRuntimeSyncDirectory();
|
|
|
|
// Are we doing a --sync or a --monitor operation? Both of these will be false if they are not set
|
|
if ((!appConfig.getValueBool("synchronize")) && (!appConfig.getValueBool("monitor"))) {
|
|
syncOrMonitorMissing = true; // --sync or --monitor is missing
|
|
}
|
|
|
|
// Has the client been configured to use Intune SSO via Microsoft Identity Broker (microsoft-identity-broker) dbus session
|
|
// This is ONLY possible on Linux, not FreeBSD or other platforms
|
|
version (linux) {
|
|
if (appConfig.getValueBool("use_intune_sso")) {
|
|
// The client is configured to use Intune SSO via Microsoft Identity Broker dbus session
|
|
addLogEntry("Client has been configured to use Intune SSO via Microsoft Identity Broker dbus session - checking usage criteria");
|
|
// We need to check that the available dbus is actually available
|
|
if(wait_for_broker()) {
|
|
// Usage criteria met, will attempt to use Intune SSO via dbus
|
|
addLogEntry("Intune SSO via Microsoft Identity Broker dbus session usage criteria met - will attempt to authenticate via Intune");
|
|
} else {
|
|
// Microsoft Identity Broker dbus is not available
|
|
addLogEntry();
|
|
addLogEntry("Required Microsoft Identity Broker dbus capability not found - disabling authentication via Intune SSO");
|
|
addLogEntry();
|
|
appConfig.setValueBool("use_intune_sso" , false);
|
|
}
|
|
}
|
|
} else {
|
|
// Ensure 'use_intune_sso' is disabled
|
|
appConfig.setValueBool("use_intune_sso" , false);
|
|
}
|
|
|
|
// Has the user configured to use the 'Recycle Bin' locally, for any files that are deleted online?
|
|
if (appConfig.getValueBool("use_recycle_bin")) {
|
|
// Configure the internal application paths which will be used to move rather than delete any online deletes to
|
|
appConfig.setRecycleBinPaths();
|
|
|
|
// If we are not using --display-config, test if the Recycle Bin Paths exist on the file system
|
|
if (!appConfig.getValueBool("display_config")) {
|
|
|
|
// We need to test that the configured 'Recycle Bin' path is not within the configured 'sync_dir'
|
|
if (appConfig.checkRecycleBinPathAsChildOfSyncDir) {
|
|
// ERROR: 'Recycle Bin' path is a child of the configured 'sync_dir'
|
|
addLogEntry();
|
|
addLogEntry("ERROR: The configured 'recycle_bin_path' (" ~ appConfig.recycleBinParentPath ~ ") is located within the configured 'sync_dir' (" ~ appConfig.runtimeSyncDirectory ~ ").", ["info", "notify"]);
|
|
addLogEntry(" This would cause locally recycled items to be re-uploaded to Microsoft OneDrive.");
|
|
addLogEntry(" Please set 'recycle_bin_path' to a location outside of 'sync_dir' and restart the client.");
|
|
addLogEntry();
|
|
return EXIT_FAILURE;
|
|
} else {
|
|
// 'Recycle Bin' path is not within the configured 'sync_dir'
|
|
// We need to ensure that the Recycle Bin Paths exist on the file system, and if they do not exist, create them
|
|
// Test for appConfig.recycleBinFilePath
|
|
if (!exists(appConfig.recycleBinFilePath)) {
|
|
try {
|
|
// Attempt to create the 'Recycle Bin' file path we have been configured with
|
|
mkdirRecurse(appConfig.recycleBinFilePath);
|
|
|
|
// Configure the applicable permissions for the folder
|
|
if (debugLogging) {addLogEntry("Setting directory permissions for: " ~ appConfig.recycleBinFilePath, ["debug"]);}
|
|
appConfig.recycleBinFilePath.setAttributes(octal!700); // Set to 0700 as Trash may contain sensitive and is the expected default permissions by GIO or KIO
|
|
|
|
} catch (std.file.FileException e) {
|
|
// Creating the 'Recycle Bin' file path failed
|
|
addLogEntry("ERROR: Unable to create the configured local 'Recycle Bin' file directory: " ~ e.msg, ["info", "notify"]);
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
|
|
// Test for appConfig.recycleBinInfoPath
|
|
if (!exists(appConfig.recycleBinInfoPath)) {
|
|
try {
|
|
// Attempt to create the 'Recycle Bin' info path we have been configured with
|
|
mkdirRecurse(appConfig.recycleBinInfoPath);
|
|
|
|
// Configure the applicable permissions for the folder
|
|
if (debugLogging) {addLogEntry("Setting directory permissions for: " ~ appConfig.recycleBinInfoPath, ["debug"]);}
|
|
appConfig.recycleBinInfoPath.setAttributes(octal!700); // Set to 0700 as Trash may contain sensitive and is the expected default permissions by GIO or KIO
|
|
|
|
|
|
} catch (std.file.FileException e) {
|
|
// Creating the 'Recycle Bin' info path failed
|
|
addLogEntry("ERROR: Unable to create the configured local 'Recycle Bin' info directory: " ~ e.msg, ["info", "notify"]);
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Are we performing some sort of 'no-sync' operation task?
|
|
noSyncTaskOperationRequested = appConfig.hasNoSyncOperationBeenRequested(); // returns true if we are
|
|
|
|
// If 'syncOrMonitorMissing' is true and 'noSyncTaskOperationRequested' is false (meaning we are not doing some 'no-sync' operation like '--display-sync-status', '--get-sharepoint-drive-id' or '--display-config'
|
|
// - fail fast here to avoid setting up all the other components, database, initialising the API as this is all pointless if we just fail out later
|
|
|
|
// If we are not using --display-config, perform this check
|
|
if (!appConfig.getValueBool("display_config")) {
|
|
if (syncOrMonitorMissing && !noSyncTaskOperationRequested) {
|
|
// Before failing fast, has the client been authenticated and does the 'refresh_token' contain data
|
|
if (exists(appConfig.refreshTokenFilePath) && getSize(appConfig.refreshTokenFilePath) > 0) {
|
|
// fail fast - print error message that --sync or --monitor are missing
|
|
printMissingOperationalSwitchesError();
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If --disable-notifications has not been used, check if everything exists to enable notifications
|
|
if (!appConfig.getValueBool("disable_notifications")) {
|
|
// If notifications was compiled in, we need to ensure that these variables are actually available before we enable GUI Notifications
|
|
flagEnvironmentVariablesAvailable(appConfig.validateGUINotificationEnvironmentVariables());
|
|
// If we are not using --display-config attempt to enable GUI notifications
|
|
if (!appConfig.getValueBool("display_config")) {
|
|
// Attempt to enable GUI Notifications
|
|
validateDBUSServerAvailability();
|
|
}
|
|
}
|
|
|
|
// cURL Version Compatibility Test
|
|
// - Common warning for cURL version issue
|
|
string distributionWarning = " Please report this to your distribution, requesting an update to a newer cURL version, or consider upgrading it yourself for optimal stability.";
|
|
// If 'force_http_11' = false, we need to check the curl version being used
|
|
if (!appConfig.getValueBool("force_http_11")) {
|
|
// get the curl version
|
|
string curlVersion = getCurlVersionNumeric();
|
|
|
|
// Is the version of curl or libcurl being used by the platform a known bad curl version for HTTP/2 support
|
|
if (isBadCurlVersion(curlVersion)) {
|
|
// add warning message
|
|
string curlWarningMessage = format("WARNING: Your cURL/libcurl version (%s) has known HTTP/2 bugs that impact the use of this client.", curlVersion);
|
|
addLogEntry();
|
|
addLogEntry(curlWarningMessage, ["info", "notify"]);
|
|
addLogEntry(distributionWarning);
|
|
addLogEntry(" Downgrading all client operations to use HTTP/1.1 to ensure maximum operational stability.");
|
|
addLogEntry(" Please read https://github.com/abraunegg/onedrive/blob/master/docs/usage.md#compatibility-with-curl for more information.");
|
|
addLogEntry();
|
|
appConfig.setValueBool("force_http_11" , true);
|
|
}
|
|
} else {
|
|
// get the curl version - a bad curl version may still be in use
|
|
string curlVersion = getCurlVersionNumeric();
|
|
|
|
// Is the version of curl or libcurl being used by the platform a known bad curl version
|
|
if (isBadCurlVersion(curlVersion)) {
|
|
// add warning message
|
|
string curlWarningMessage = format("WARNING: Your cURL/libcurl version (%s) has known operational bugs that impact the use of this client.", curlVersion);
|
|
addLogEntry();
|
|
addLogEntry(curlWarningMessage); // curl HTTP/1.1 downgrade in place meaning user took steps to remediate, perform standard logging with no GUI notification
|
|
addLogEntry(distributionWarning);
|
|
addLogEntry();
|
|
}
|
|
}
|
|
|
|
// In a debug scenario, to assist with understanding the run-time configuration, ensure this flag is set
|
|
if (debugLogging) {
|
|
appConfig.setValueBool("display_running_config", true);
|
|
}
|
|
|
|
// Configure dryRun so that this can be used here & during shutdown
|
|
dryRun = appConfig.getValueBool("dry_run");
|
|
|
|
// As early as possible, now re-configure the logging class, given that we have read in any applicable 'config' file and updated the application running config from CLI input:
|
|
// - Enable logging to a file if this is required
|
|
// - Disable GUI notifications if this has been configured
|
|
|
|
// Configure application logging to a log file only if this has been enabled
|
|
// This is the earliest point that this can be done, as the client configuration has been read in, and any CLI arguments have been processed.
|
|
// Either of those ('config' file, CLI arguments) could be enabling logging, thus this is the earliest point at which this can be validated and enabled.
|
|
// The buffered logging also ensures that all 'output' to this point is also captured and written out to the log file
|
|
if (appConfig.getValueBool("enable_logging")) {
|
|
// Calculate the application logging directory
|
|
string calculatedLogDirPath = appConfig.calculateLogDirectory();
|
|
string calculatedLogFilePath;
|
|
// Initialise using the configured logging directory
|
|
if (verboseLogging) {addLogEntry("Using the following path to store the runtime application log: " ~ calculatedLogDirPath, ["verbose"]);}
|
|
// Calculate the logfile name
|
|
if (calculatedLogDirPath != appConfig.defaultHomePath) {
|
|
// Log file is not going to the home directory
|
|
string logfileName = runtimeUserName ~ ".onedrive.log";
|
|
calculatedLogFilePath = buildNormalizedPath(buildPath(calculatedLogDirPath, logfileName));
|
|
} else {
|
|
// Log file is going to the users home directory
|
|
calculatedLogFilePath = buildNormalizedPath(buildPath(calculatedLogDirPath, "onedrive.log"));
|
|
}
|
|
// Update the logging class to use 'calculatedLogFilePath' for the application log file now that this has been determined
|
|
enableLogFileOutput(calculatedLogFilePath);
|
|
}
|
|
|
|
// Disable GUI Notifications if configured to do so
|
|
// - This option is reverse action. If 'disable_notifications' is 'true', we need to send 'false'
|
|
if (appConfig.getValueBool("disable_notifications")) {
|
|
// disable_notifications is true, ensure GUI notifications is initialised with false so that NO GUI notification is sent
|
|
disableGUINotifications(false);
|
|
addLogEntry("Disabling GUI notifications as per user configuration");
|
|
}
|
|
|
|
// Perform a deprecated options check now that the config file (if present) and CLI options have all been parsed to advise the user that their option usage might change
|
|
appConfig.checkDeprecatedOptions(cliArgs);
|
|
|
|
// Configure Client Side Filtering (selective sync) by parsing and getting a usable regex for skip_file, skip_dir and sync_list config components
|
|
selectiveSync = new ClientSideFiltering(appConfig);
|
|
if (!selectiveSync.initialise()) {
|
|
// exit here as something triggered a selective sync configuration failure
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
// Set runtimeDatabaseFile, this will get updated if we are using --dry-run
|
|
runtimeDatabaseFile = appConfig.databaseFilePath;
|
|
|
|
// DEVELOPER OPTIONS OUTPUT
|
|
// Set to display memory details as early as possible
|
|
displayMemoryUsage = appConfig.getValueBool("display_memory");
|
|
// set to display sync options
|
|
displaySyncOptions = appConfig.getValueBool("display_sync_options");
|
|
|
|
// Display the current application configuration (based on all defaults, 'config' file parsing and/or options passed in via the CLI) and exit if --display-config has been used
|
|
if ((appConfig.getValueBool("display_config")) || (appConfig.getValueBool("display_running_config"))) {
|
|
// Display the application configuration
|
|
appConfig.displayApplicationConfiguration();
|
|
// Do we exit? We exit only if '--display-config' has been used
|
|
if (appConfig.getValueBool("display_config")) {
|
|
return EXIT_SUCCESS;
|
|
}
|
|
}
|
|
|
|
// Check for basic application option conflicts - flags that should not be used together and/or flag combinations that conflict with each other, values that should be present and are not
|
|
if (appConfig.checkForBasicOptionConflicts) {
|
|
// Any error will have been printed by the function itself, but we need a small delay here to allow the buffered logging to output any error
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
// Check for --dry-run operation or a 'no-sync' operation where the 'dry-run' DB copy should be used
|
|
// If this has been requested, we need to ensure that all actions are performed against the dry-run database copy, and,
|
|
// no actual action takes place - such as deleting files if deleted online, moving files if moved online or local, downloading new & changed files, uploading new & changed files
|
|
if (dryRun || (noSyncTaskOperationRequested)) {
|
|
// Cleanup any existing dry-run elements ... these should never be left hanging around and should be cleaned up first
|
|
cleanupDatabaseFiles(appConfig.databaseFilePathDryRun);
|
|
|
|
// If --dry-run
|
|
if (dryRun) {
|
|
// This is a --dry-run operation
|
|
addLogEntry("DRY-RUN Configured. Output below shows what 'would' have occurred.");
|
|
|
|
// Make a copy of the original items.sqlite3 for use as the dry run copy if it exists
|
|
if (exists(appConfig.databaseFilePath)) {
|
|
// In a --dry-run --resync scenario, we should not copy the existing database file
|
|
if (!appConfig.getValueBool("resync")) {
|
|
// Copy the existing DB file to the dry-run copy
|
|
addLogEntry("DRY-RUN: Copying items.sqlite3 to items-dryrun.sqlite3 to use for dry run operations");
|
|
copy(appConfig.databaseFilePath,appConfig.databaseFilePathDryRun);
|
|
} else {
|
|
// No database copy due to --resync - an empty DB file will be used for the resync operation
|
|
addLogEntry("DRY-RUN: No database copy created for --dry-run due to --resync also being used");
|
|
}
|
|
}
|
|
|
|
// update runtimeDatabaseFile now that we are using the dry run path
|
|
runtimeDatabaseFile = appConfig.databaseFilePathDryRun;
|
|
}
|
|
} else {
|
|
// Cleanup any existing dry-run elements ... these should never be left hanging around
|
|
cleanupDatabaseFiles(appConfig.databaseFilePathDryRun);
|
|
}
|
|
|
|
// Handle --logout as separate item, do not 'resync' on a --logout
|
|
if (appConfig.getValueBool("logout")) {
|
|
if (debugLogging) {addLogEntry("--logout requested", ["debug"]);}
|
|
addLogEntry("Deleting the saved authentication status ...");
|
|
if (!dryRun) {
|
|
// Remove the 'refresh_token' file if present
|
|
safeRemove(appConfig.refreshTokenFilePath);
|
|
// Remove the 'intune_account' file if present
|
|
safeRemove(appConfig.intuneAccountDetailsFilePath);
|
|
} else {
|
|
// --dry-run scenario ... technically we should not be making any local file changes .......
|
|
addLogEntry("DRY-RUN: Not removing the saved authentication status");
|
|
}
|
|
// Exit
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
// Handle --reauth to re-authenticate the client
|
|
if (appConfig.getValueBool("reauth")) {
|
|
if (debugLogging) {addLogEntry("--reauth requested", ["debug"]);}
|
|
addLogEntry("Deleting the saved authentication status ... re-authentication requested");
|
|
if (!dryRun) {
|
|
// Remove the 'refresh_token' file if present
|
|
safeRemove(appConfig.refreshTokenFilePath);
|
|
// Remove the 'intune_account' file if present
|
|
safeRemove(appConfig.intuneAccountDetailsFilePath);
|
|
} else {
|
|
// --dry-run scenario ... technically we should not be making any local file changes .......
|
|
addLogEntry("DRY-RUN: Not removing the saved authentication status");
|
|
}
|
|
}
|
|
|
|
// --resync should be considered a 'last resort item' or if the application configuration has changed, where a resync is needed .. the user needs to 'accept' this warning to proceed
|
|
// If --resync has not been used (bool value is false), check the application configuration for 'changes' that require a --resync to ensure that the data locally reflects the users requested configuration
|
|
if (appConfig.getValueBool("resync")) {
|
|
// what is the risk acceptance for --resync?
|
|
bool resyncRiskAcceptance = appConfig.displayResyncRiskForAcceptance();
|
|
if (debugLogging) {addLogEntry("Returned --resync risk acceptance: " ~ to!string(resyncRiskAcceptance), ["debug"]);}
|
|
|
|
// Action based on user response
|
|
if (!resyncRiskAcceptance){
|
|
// --resync risk not accepted
|
|
return EXIT_FAILURE;
|
|
} else {
|
|
if (debugLogging) {addLogEntry("--resync issued and risk accepted", ["debug"]);}
|
|
// --resync risk accepted, perform a cleanup of items that require a cleanup
|
|
appConfig.cleanupHashFilesDueToResync();
|
|
// Make a backup of the applicable configuration file
|
|
appConfig.createBackupConfigFile();
|
|
// Update hash files and generate a new config backup
|
|
appConfig.updateHashContentsForConfigFiles();
|
|
// Remove the items database
|
|
processResyncDatabaseRemoval(runtimeDatabaseFile);
|
|
}
|
|
} else {
|
|
// Is the application currently authenticated? If not, it is pointless checking if a --resync is required until the application is authenticated
|
|
if (exists(appConfig.refreshTokenFilePath)) {
|
|
// Has any of our application configuration that would require a --resync been changed?
|
|
if (appConfig.applicationChangeWhereResyncRequired()) {
|
|
// Application configuration has changed however --resync not issued, fail fast
|
|
addLogEntry();
|
|
addLogEntry("An application configuration change has been detected where a --resync is required", ["info", "notify"]);
|
|
addLogEntry();
|
|
return EXIT_RESYNC_REQUIRED;
|
|
} else {
|
|
// No configuration change that requires a --resync to be issued
|
|
// 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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Implement https://github.com/abraunegg/onedrive/issues/1129
|
|
// Force a synchronisation of a specific folder, only when using --synchronize --single-directory and ignoring all non-default skip_dir and skip_file rules
|
|
if (appConfig.getValueBool("force_sync")) {
|
|
// appConfig.checkForBasicOptionConflicts() has already checked for the basic requirements for --force-sync
|
|
addLogEntry();
|
|
addLogEntry("WARNING: Overriding application configuration to use application defaults for skip_dir and skip_file due to --sync --single-directory --force-sync being used");
|
|
addLogEntry();
|
|
bool forceSyncRiskAcceptance = appConfig.displayForceSyncRiskForAcceptance();
|
|
if (debugLogging) {addLogEntry("Returned --force-sync risk acceptance: " ~ forceSyncRiskAcceptance, ["debug"]);}
|
|
|
|
// Action based on user response
|
|
if (!forceSyncRiskAcceptance){
|
|
// --force-sync risk not accepted
|
|
return EXIT_FAILURE;
|
|
} else {
|
|
// --force-sync risk accepted
|
|
// reset set config using function to use application defaults
|
|
appConfig.resetSkipToDefaults();
|
|
// update sync engine regex with reset defaults
|
|
selectiveSync.setDirMask(appConfig.getValueString("skip_dir"));
|
|
selectiveSync.setFileMask(appConfig.getValueString("skip_file"));
|
|
}
|
|
}
|
|
|
|
// What IP Protocol are we going to use to access the network with
|
|
appConfig.displayIPProtocol();
|
|
|
|
// Test if OneDrive service can be reached, exit if it cant be reached
|
|
if (debugLogging) {addLogEntry("Testing network to ensure network connectivity to Microsoft OneDrive Service", ["debug"]);}
|
|
online = testInternetReachability(appConfig);
|
|
|
|
// If we are not 'online' - how do we handle this situation?
|
|
if (!online) {
|
|
// We are unable to initialise the OneDrive API as we are not online
|
|
if (!appConfig.getValueBool("monitor")) {
|
|
// Running as --synchronize
|
|
addLogEntry();
|
|
addLogEntry("ERROR: Unable to reach the Microsoft OneDrive API service, unable to initialise application");
|
|
addLogEntry();
|
|
return EXIT_FAILURE;
|
|
} else {
|
|
// Running as --monitor
|
|
addLogEntry();
|
|
addLogEntry("Unable to reach the Microsoft OneDrive API service at this point in time, re-trying network tests based on applicable intervals");
|
|
addLogEntry();
|
|
// Run the re-try of Internet connectivity test
|
|
online = retryInternetConnectivityTest(appConfig);
|
|
}
|
|
}
|
|
|
|
// This needs to be a separate 'if' statement, as, if this was an 'if-else' from above, if we were originally offline and using --monitor, we would never get to this point
|
|
if (online) {
|
|
// Check Application Version
|
|
if (!appConfig.getValueBool("disable_version_check")) {
|
|
if (verboseLogging) {addLogEntry("Checking Application Version ...", ["verbose"]);}
|
|
checkApplicationVersion();
|
|
}
|
|
|
|
// Initialise the OneDrive API
|
|
if (verboseLogging) {addLogEntry("Attempting to initialise the OneDrive API ...", ["verbose"]);}
|
|
OneDriveApi oneDriveApiInstance = new OneDriveApi(appConfig);
|
|
appConfig.apiWasInitialised = oneDriveApiInstance.initialise();
|
|
|
|
// Did the API initialise successfully?
|
|
if (appConfig.apiWasInitialised) {
|
|
if (verboseLogging) {addLogEntry("The OneDrive API was initialised successfully", ["verbose"]);}
|
|
|
|
// Flag that we were able to initialise the API in the application config
|
|
oneDriveApiInstance.debugOutputConfiguredAPIItems();
|
|
oneDriveApiInstance.releaseCurlEngine();
|
|
object.destroy(oneDriveApiInstance);
|
|
oneDriveApiInstance = null;
|
|
|
|
// Need to configure the itemDB and syncEngineInstance for 'sync' and 'non-sync' operations
|
|
if (verboseLogging) {addLogEntry("Opening the item database ...", ["verbose"]);}
|
|
|
|
// Configure the Item Database
|
|
itemDB = new ItemDatabase(runtimeDatabaseFile);
|
|
// Was the database successfully initialised?
|
|
if (!itemDB.isDatabaseInitialised()) {
|
|
// no .. destroy class
|
|
itemDB = null;
|
|
// exit application
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
// Initialise the syncEngine
|
|
syncEngineInstance = new SyncEngine(appConfig, itemDB, selectiveSync);
|
|
appConfig.syncEngineWasInitialised = syncEngineInstance.initialise();
|
|
|
|
// Are we not doing a --sync or a --monitor operation?
|
|
if (syncOrMonitorMissing) { // this is 'true' if --sync or a --monitor were not used
|
|
|
|
// Do not perform a vacuum on exit, pointless
|
|
performDatabaseVacuum = false;
|
|
|
|
// 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 status?
|
|
// - 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 creating 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?
|
|
// - Are we renaming or moving a directory?
|
|
// - Are we displaying the quota information?
|
|
// - Did we just authorise the client?
|
|
|
|
// --get-sharepoint-drive-id - Get the SharePoint Library drive_id
|
|
if (appConfig.getValueString("sharepoint_library_name") != "") {
|
|
// Get the SharePoint Library drive_id
|
|
syncEngineInstance.querySiteCollectionForDriveID(appConfig.getValueString("sharepoint_library_name"));
|
|
// Exit application
|
|
// Use exit scopes to shutdown API and cleanup data
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
// --display-sync-status - Query the sync status
|
|
if (appConfig.getValueBool("display_sync_status")) {
|
|
// path to query variable
|
|
string pathToQueryStatusOn;
|
|
// What path do we query?
|
|
if (!appConfig.getValueString("single_directory").empty) {
|
|
pathToQueryStatusOn = "/" ~ appConfig.getValueString("single_directory");
|
|
} else {
|
|
pathToQueryStatusOn = "/";
|
|
}
|
|
// Query the sync status
|
|
syncEngineInstance.queryOneDriveForSyncStatus(pathToQueryStatusOn);
|
|
// Exit application
|
|
// Use exit scopes to shutdown API and cleanup data
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
// --get-file-link - Get the URL path for a synced file
|
|
if (appConfig.getValueString("get_file_link") != "") {
|
|
// Query the OneDrive API for the file link
|
|
syncEngineInstance.queryOneDriveForFileDetails(appConfig.getValueString("get_file_link"), runtimeSyncDirectory, "URL");
|
|
// Exit application
|
|
// Use exit scopes to shutdown API and cleanup data
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
// --modified-by - Get listing the modified-by details of a provided path
|
|
if (appConfig.getValueString("modified_by") != "") {
|
|
// Query the OneDrive API for the last modified by details
|
|
syncEngineInstance.queryOneDriveForFileDetails(appConfig.getValueString("modified_by"), runtimeSyncDirectory, "ModifiedBy");
|
|
// Exit application
|
|
// Use exit scopes to shutdown API and cleanup data
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
// --list-shared-items - Get 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 - Create a shareable link for an existing file, based on the local path
|
|
if (appConfig.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
|
|
syncEngineInstance.queryOneDriveForFileDetails(appConfig.getValueString("create_share_link"), runtimeSyncDirectory, "ShareableLink");
|
|
// Exit application
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
// --create-directory - Are we just creating a directory online, without any sync being performed?
|
|
if ((appConfig.getValueString("create_directory") != "")) {
|
|
// Handle the remote path creation and updating of the local database without performing a sync
|
|
syncEngineInstance.createDirectoryOnline(appConfig.getValueString("create_directory"));
|
|
// Exit application
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
// --remove-directory - Are we just deleting a directory online, without any sync being performed?
|
|
if ((appConfig.getValueString("remove_directory") != "")) {
|
|
// Handle the remote path deletion without performing a sync
|
|
syncEngineInstance.deleteByPathNoSync(appConfig.getValueString("remove_directory"));
|
|
// Exit application
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
// Are we renaming or moving a directory online?
|
|
// onedrive --source-directory 'path/as/source/' --destination-directory 'path/as/destination'
|
|
if ((appConfig.getValueString("source_directory") != "") && (appConfig.getValueString("destination_directory") != "")) {
|
|
// We are renaming or moving a directory
|
|
syncEngineInstance.moveOrRenameDirectoryOnline(appConfig.getValueString("source_directory"), appConfig.getValueString("destination_directory"));
|
|
// Exit application
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
// --display-quota - Are we displaying the quota information?
|
|
if (appConfig.getValueBool("display_quota")) {
|
|
// Query and respond with the quota details
|
|
syncEngineInstance.queryOneDriveForQuotaDetails();
|
|
// Exit application
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
// --download-file - Are we downloading a single file from Microsoft OneDrive
|
|
if ((appConfig.getValueString("download_single_file") != "")) {
|
|
// Handle downloading the single file
|
|
syncEngineInstance.downloadSingleFile(appConfig.getValueString("download_single_file"));
|
|
// Exit application
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
// If we get to this point, we have not performed a 'no-sync' task ..
|
|
|
|
// Did we just authorise the client?
|
|
if (appConfig.applicationAuthoriseResponseURIReceived) {
|
|
// Authorisation activity
|
|
if (exists(appConfig.refreshTokenFilePath)) {
|
|
// OneDrive refresh token exists
|
|
addLogEntry();
|
|
addLogEntry("The application has been successfully authorised, but no extra command options have been specified.");
|
|
addLogEntry();
|
|
addLogEntry(genericHelpMessage);
|
|
addLogEntry();
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_SUCCESS;
|
|
} else {
|
|
// We just authorised, but refresh_token does not exist .. probably an auth error?
|
|
addLogEntry();
|
|
addLogEntry("Your application's authorisation was unsuccessful. Please review your URI response entry, then attempt authorisation again with a new URI response.");
|
|
addLogEntry();
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_FAILURE;
|
|
}
|
|
} else {
|
|
// No authorisation activity - print error message
|
|
printMissingOperationalSwitchesError();
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
} else {
|
|
// API could not be initialised
|
|
addLogEntry("The OneDrive API could not be initialised");
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
|
|
// Configure the sync directory based on the runtimeSyncDirectory configured directory
|
|
if (verboseLogging) {addLogEntry("All application operations will be performed in the configured local 'sync_dir' directory: " ~ runtimeSyncDirectory, ["verbose"]);}
|
|
// Try and set the 'sync_dir', attempt to create if it does not exist
|
|
try {
|
|
if (!exists(runtimeSyncDirectory)) {
|
|
if (debugLogging) {addLogEntry("runtimeSyncDirectory: Configured 'sync_dir' is missing locally. Creating: " ~ runtimeSyncDirectory, ["debug"]);}
|
|
|
|
// At this point 'sync_dir' is missing and we have requested to create it
|
|
// However ... 'itemDB' is pointing to a valid database file
|
|
// If this database has any entries, an empty 'sync_dir' will cause the application to think that all content in 'sync_dir' has been deleted
|
|
// In this scenario, the application, depending on the options being used, may attempt to delete all files online - which is not desirable
|
|
// Do a sanity check here to ensure that there are no database entries
|
|
|
|
if (itemDB.getTotalRowCount() == 1) {
|
|
// Technically an 'empty database'
|
|
// An empty database will just have 1 row in it, that row being the account 'root' data added when the API is initially initialised above
|
|
try {
|
|
// Attempt to create the sync dir we have been configured with
|
|
mkdirRecurse(runtimeSyncDirectory);
|
|
// Configure the applicable permissions for the folder
|
|
if (debugLogging) {addLogEntry("Setting directory permissions for: " ~ runtimeSyncDirectory, ["debug"]);}
|
|
runtimeSyncDirectory.setAttributes(appConfig.returnRequiredDirectoryPermissions());
|
|
} catch (std.file.FileException e) {
|
|
// Creating the sync directory failed
|
|
addLogEntry("ERROR: Unable to create the configured local 'sync_dir' directory: " ~ e.msg, ["info", "notify"]);
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_FAILURE;
|
|
}
|
|
} else {
|
|
// Not an empty database
|
|
addLogEntry();
|
|
addLogEntry("An application cache state issue has been detected where a --resync is required", ["info", "notify"]);
|
|
addLogEntry();
|
|
return EXIT_RESYNC_REQUIRED;
|
|
}
|
|
}
|
|
} catch (std.file.FileException e) {
|
|
// Creating the sync directory failed
|
|
addLogEntry("ERROR: Unable to test for the existence of the configured local 'sync_dir' directory: " ~ e.msg);
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
// Try and change to the working directory to the 'sync_dir' as configured
|
|
try {
|
|
chdir(runtimeSyncDirectory);
|
|
// A FileSystem exception was thrown when attempting to change to the configured 'sync_dir'
|
|
} catch (FileException e) {
|
|
// Log error message
|
|
addLogEntry("FATAL: Unable to change to the configured local 'sync_dir' directory: " ~ runtimeSyncDirectory);
|
|
// A file system exception was generated
|
|
displayFileSystemErrorMessage(e.msg, strip(getFunctionName!({})), runtimeSyncDirectory, FsErrorSeverity.fatal);
|
|
// Use exit scopes to shutdown API as if we are unable to change to the 'sync_dir' we need to exit
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
// Do we need to validate the runtimeSyncDirectory to check for the presence of a '.nosync' file
|
|
checkForNoMountScenario();
|
|
|
|
// Is the sync engine initialised correctly?
|
|
if (appConfig.syncEngineWasInitialised) {
|
|
// Configure some initial variables
|
|
string singleDirectoryPath;
|
|
string localPath = ".";
|
|
string remotePath = "/";
|
|
|
|
// If not performing a --resync, check if there are interrupted downloads and/or uploads that need to be completed
|
|
if (!appConfig.getValueBool("resync")) {
|
|
// Check if there are any downloads that need to be resumed
|
|
if (syncEngineInstance.checkForResumableDownloads) {
|
|
// Need to re-process the the 'resumable data' to resume the download
|
|
addLogEntry("There are interrupted downloads that need to be resumed ...");
|
|
// Process the resumable download files
|
|
syncEngineInstance.processResumableDownloadFiles();
|
|
}
|
|
|
|
// Check if there are interrupted upload session(s)
|
|
if (syncEngineInstance.checkForInterruptedSessionUploads) {
|
|
// Need to re-process the session upload files to resume the failed session uploads
|
|
addLogEntry("There are interrupted session uploads that need to be resumed ...");
|
|
// Process the session upload files
|
|
syncEngineInstance.processInterruptedSessionUploads();
|
|
}
|
|
} else {
|
|
// Clean up any downloads that were due to be resumed, but will not be resumed due to --resync being used
|
|
syncEngineInstance.clearInterruptedDownloads();
|
|
|
|
// Clean up any uploads that were due to be resumed, but will not be resumed due to --resync being used
|
|
syncEngineInstance.clearInterruptedSessionUploads();
|
|
}
|
|
|
|
// Are we doing a single directory operation (--single-directory) ?
|
|
if (!appConfig.getValueString("single_directory").empty) {
|
|
// Ensure that the value stored for appConfig.getValueString("single_directory") does not contain any extra quotation marks
|
|
string originalSingleDirectoryValue = appConfig.getValueString("single_directory");
|
|
// Strip quotation marks from provided path to ensure no issues within a Docker environment when using passed in values
|
|
string updatedSingleDirectoryValue = strip(originalSingleDirectoryValue, "\"");
|
|
// Set singleDirectoryPath
|
|
singleDirectoryPath = updatedSingleDirectoryValue;
|
|
|
|
// Ensure that this is a normalised relative path to runtimeSyncDirectory
|
|
string normalisedRelativePath = replace(buildNormalizedPath(absolutePath(singleDirectoryPath)), buildNormalizedPath(absolutePath(runtimeSyncDirectory)), "." );
|
|
|
|
// The user provided a directory to sync within the configured 'sync_dir' path
|
|
// This also validates if the path being used exists online and/or does not have a 'case-insensitive match'
|
|
syncEngineInstance.setSingleDirectoryScope(normalisedRelativePath);
|
|
|
|
// Does the directory we want to sync actually exist locally?
|
|
if (!exists(singleDirectoryPath)) {
|
|
// The requested path to use with --single-directory does not exist locally within the configured 'sync_dir'
|
|
addLogEntry("WARNING: The requested path for --single-directory does not exist locally. Creating requested path within " ~ runtimeSyncDirectory, ["info", "notify"]);
|
|
// Attempt path creation
|
|
try {
|
|
// Attempt to create the required --single-directory path locally
|
|
mkdirRecurse(singleDirectoryPath);
|
|
// Configure the applicable permissions for the folder
|
|
if (debugLogging) {addLogEntry("Setting directory permissions for: " ~ singleDirectoryPath, ["debug"]);}
|
|
singleDirectoryPath.setAttributes(appConfig.returnRequiredDirectoryPermissions());
|
|
} catch (std.file.FileException e) {
|
|
// Creating the sync directory failed
|
|
addLogEntry("ERROR: Unable to create the required --single-directory path: " ~ e.msg, ["info", "notify"]);
|
|
// Use exit scopes to shutdown API
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
|
|
// Update the paths that we use to perform the sync actions
|
|
localPath = singleDirectoryPath;
|
|
remotePath = singleDirectoryPath;
|
|
|
|
// Display that we are syncing from a specific path due to --single-directory
|
|
if (verboseLogging) {addLogEntry("Syncing changes from this selected path: " ~ singleDirectoryPath, ["verbose"]);}
|
|
}
|
|
|
|
// Handle SIGINT, SIGTERM and SIGSEGV signals
|
|
setupSignalHandler();
|
|
|
|
// Are we doing a --sync operation? This includes doing any --single-directory operations
|
|
if (appConfig.getValueBool("synchronize")) {
|
|
// We are not using this, so destroy it early
|
|
object.destroy(filesystemMonitor);
|
|
filesystemMonitor = null;
|
|
|
|
// Did the user specify --upload-only?
|
|
if (appConfig.getValueBool("upload_only")) {
|
|
// Perform the --upload-only sync process
|
|
performUploadOnlySyncProcess(localPath);
|
|
}
|
|
|
|
// Did the user specify --download-only?
|
|
if (appConfig.getValueBool("download_only")) {
|
|
// Only download data from OneDrive
|
|
syncEngineInstance.syncOneDriveAccountToLocalDisk();
|
|
// Perform the DB consistency check
|
|
// This will also delete any out-of-sync flagged items if configured to do so
|
|
syncEngineInstance.performDatabaseConsistencyAndIntegrityCheck();
|
|
// Do we cleanup local files?
|
|
// - Deletes of data from online will already have been performed, but what we are now doing is searching the local filesystem
|
|
// for any new data locally, that usually would be uploaded to OneDrive, but instead, because of the options being
|
|
// used, will need to be deleted from the local filesystem
|
|
if (appConfig.getValueBool("cleanup_local_files")) {
|
|
// Perform the filesystem walk
|
|
syncEngineInstance.scanLocalFilesystemPathForNewData(localPath);
|
|
}
|
|
}
|
|
|
|
// If no use of --upload-only or --download-only
|
|
if ((!appConfig.getValueBool("upload_only")) && (!appConfig.getValueBool("download_only"))) {
|
|
// Perform the standard sync process
|
|
performStandardSyncProcess(localPath);
|
|
}
|
|
|
|
// Detail the outcome of the sync process
|
|
displaySyncOutcome();
|
|
}
|
|
|
|
// Are we doing a --monitor operation?
|
|
if (appConfig.getValueBool("monitor")) {
|
|
// Update the flag given we are running with --monitor
|
|
performFileSystemMonitoring = true;
|
|
|
|
// Set initial variable for when we last uploaded something or made an online change from a local inotify event
|
|
lastLocalWrite = MonoTime.currTime() - dur!"hours"(24);
|
|
|
|
// Is Display Manager Integration enabled?
|
|
if (appConfig.getValueBool("display_manager_integration")) {
|
|
// Attempt to configure the desktop integration whilst the client is running in --monitor mode
|
|
attemptFileManagerIntegration();
|
|
}
|
|
|
|
// If 'webhooks' are enabled, this is going to conflict with 'websockets' if the OS cURL library supports websockets
|
|
if (appConfig.getValueBool("webhook_enabled") && appConfig.curlSupportsWebSockets) {
|
|
// We have to disable 'websocket' support
|
|
addLogEntry();
|
|
addLogEntry("WARNING: WebSocket support has been disabled because Webhooks are already configured to monitor Microsoft Graph API changes.");
|
|
addLogEntry(" Only one API notification method can be active at a time.");
|
|
addLogEntry();
|
|
// Set the flag that this will not be used
|
|
appConfig.curlSupportsWebSockets = false;
|
|
} else {
|
|
// Double check scenario, this time 'false' checking 'webhook_enabled'
|
|
if ((!appConfig.getValueBool("webhook_enabled")) && (appConfig.curlSupportsWebSockets)) {
|
|
|
|
// If we are doing --upload-only however .. we need to 'ignore' online change
|
|
if (!appConfig.getValueBool("upload_only")) {
|
|
// Did the user configure to disable 'websocket' support?
|
|
if (!appConfig.getValueBool("disable_websocket_support")) {
|
|
// Log that we are attempting to enable WebSocket Support
|
|
addLogEntry("Attempting to enable WebSocket support to monitor Microsoft Graph API changes in near real-time.");
|
|
|
|
// Obtain the WebSocket Notification URL from the API endpoint
|
|
syncEngineInstance.obtainWebSocketNotificationURL();
|
|
|
|
// Were we able to correctly obtain the endpoint response and build the socket.io WS endpoint
|
|
if (appConfig.websocketNotificationUrlAvailable) {
|
|
// Notification URL is available
|
|
if (oneDriveSocketIo is null) {
|
|
oneDriveSocketIo = new OneDriveSocketIo(thisTid, appConfig);
|
|
oneDriveSocketIo.start();
|
|
}
|
|
addLogEntry("Enabled WebSocket support to monitor Microsoft Graph API changes in near real-time.");
|
|
} else {
|
|
addLogEntry("ERROR: Unable to configure WebSocket support to monitor Microsoft Graph API changes in near real-time.");
|
|
}
|
|
} else {
|
|
// WebSocket Support has been disabled
|
|
addLogEntry("WebSocket support has been disabled by user configuration.");
|
|
}
|
|
} else {
|
|
// --upload only being used
|
|
addLogEntry("Online changes will not be monitored by WebSocket support due to --upload-only");
|
|
// Set the flag that this will not be used
|
|
appConfig.curlSupportsWebSockets = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// What are the current values for the platform we are running on
|
|
string maxOpenFilesSoft = strip(to!string(getSoftOpenFilesLimit()));
|
|
string maxOpenFilesHard = strip(to!string(getHardOpenFilesLimit()));
|
|
// What is the currently configured maximum inotify watches that can be used
|
|
string maxInotifyWatches = strip(getMaxInotifyWatches());
|
|
|
|
// Start the monitor process
|
|
addLogEntry("OneDrive synchronisation interval (seconds): " ~ to!string(appConfig.getValueLong("monitor_interval")));
|
|
|
|
// If we are in a --download-only method of operation, the output of these is not required
|
|
if (!appConfig.getValueBool("download_only")) {
|
|
if (verboseLogging) {
|
|
addLogEntry("Maximum allowed open files (soft): " ~ maxOpenFilesSoft, ["verbose"]);
|
|
addLogEntry("Maximum allowed open files (hard): " ~ maxOpenFilesHard, ["verbose"]);
|
|
addLogEntry("Maximum allowed inotify user watches: " ~ maxInotifyWatches, ["verbose"]);
|
|
}
|
|
}
|
|
|
|
// Configure the monitor class
|
|
filesystemMonitor = new Monitor(appConfig, selectiveSync);
|
|
|
|
// Delegated function for when inotify detects a new local directory has been created
|
|
filesystemMonitor.onDirCreated = delegate(string path) {
|
|
// Handle .folder creation if skip_dotfiles is enabled
|
|
if ((appConfig.getValueBool("skip_dotfiles")) && (isDotFile(path))) {
|
|
if (verboseLogging) {addLogEntry("[M] Skipping watching local path - .folder found & --skip-dot-files enabled: " ~ path, ["verbose"]);}
|
|
} else {
|
|
if (verboseLogging) {addLogEntry("[M] Local directory created: " ~ path, ["verbose"]);}
|
|
try {
|
|
syncEngineInstance.scanLocalFilesystemPathForNewData(path);
|
|
markLocalWrite();
|
|
} catch (CurlException e) {
|
|
if (verboseLogging) {addLogEntry("Offline, cannot create remote dir: " ~ path, ["verbose"]);}
|
|
} catch (Exception e) {
|
|
addLogEntry("Cannot create remote directory: " ~ e.msg, ["info", "notify"]);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Delegated function for when inotify detects a local file has been changed
|
|
filesystemMonitor.onFileChanged = delegate(string[] changedLocalFilesToUploadToOneDrive) {
|
|
// Handle a potentially locally changed file
|
|
// Logging for this event moved to handleLocalFileTrigger() due to threading and false triggers from scanLocalFilesystemPathForNewData() above
|
|
syncEngineInstance.handleLocalFileTrigger(changedLocalFilesToUploadToOneDrive);
|
|
markLocalWrite();
|
|
if (verboseLogging) {addLogEntry("[M] Total number of local file(s) added or changed: " ~ to!string(changedLocalFilesToUploadToOneDrive.length), ["verbose"]);}
|
|
};
|
|
|
|
// Delegated function for when inotify detects a delete event
|
|
filesystemMonitor.onDelete = delegate(string path) {
|
|
if (verboseLogging) {addLogEntry("[M] Local item deleted: " ~ path, ["verbose"]);}
|
|
try {
|
|
// The path has been deleted .. we cannot use isDir or isFile to advise what was deleted. This is the best we can Do
|
|
addLogEntry("The operating system sent a deletion notification. Trying to delete this item as requested: " ~ path);
|
|
// perform the delete action
|
|
syncEngineInstance.deleteByPath(path);
|
|
markLocalWrite();
|
|
} catch (CurlException e) {
|
|
if (verboseLogging) {addLogEntry("Offline, cannot delete item: " ~ path, ["verbose"]);}
|
|
} catch (SyncException e) {
|
|
if (e.msg == "The item to delete is not in the local database") {
|
|
if (verboseLogging) {addLogEntry("Item cannot be deleted from Microsoft OneDrive because it was not found in the local database", ["verbose"]);}
|
|
} else {
|
|
addLogEntry("Cannot delete remote item: " ~ e.msg, ["info", "notify"]);
|
|
}
|
|
} catch (FileException e) {
|
|
// Path is gone locally, log and continue.
|
|
addLogEntry("ERROR: The local file system returned an error with the following message: " ~ e.msg, ["verbose"]);
|
|
} catch (Exception e) {
|
|
addLogEntry("Cannot delete remote item: " ~ e.msg, ["info", "notify"]);
|
|
}
|
|
};
|
|
|
|
// Delegated function for when inotify detects a move event
|
|
filesystemMonitor.onMove = delegate(string from, string to) {
|
|
if (verboseLogging) {addLogEntry("[M] Local item moved: " ~ from ~ " -> " ~ to, ["verbose"]);}
|
|
try {
|
|
// Handle .folder -> folder if skip_dotfiles is enabled
|
|
if ((appConfig.getValueBool("skip_dotfiles")) && (isDotFile(from))) {
|
|
// .folder -> folder handling - has to be handled as a new folder
|
|
syncEngineInstance.scanLocalFilesystemPathForNewData(to);
|
|
} else {
|
|
syncEngineInstance.uploadMoveItem(from, to);
|
|
}
|
|
markLocalWrite();
|
|
} catch (CurlException e) {
|
|
if (verboseLogging) {addLogEntry("Offline, cannot move item !", ["verbose"]);}
|
|
} catch (Exception e) {
|
|
addLogEntry("Cannot move item: " ~ e.msg, ["info", "notify"]);
|
|
}
|
|
};
|
|
|
|
// Initialise the local filesystem monitor class using inotify to monitor for local filesystem changes
|
|
// If we are in a --download-only method of operation, we do not enable local filesystem monitoring
|
|
if (!appConfig.getValueBool("download_only")) {
|
|
// Not using --download-only
|
|
try {
|
|
addLogEntry("Initialising filesystem inotify monitoring ...", ["info", "notify"]);
|
|
filesystemMonitor.initialise();
|
|
addLogEntry("Performing initial synchronisation to ensure consistent local state ...");
|
|
} catch (MonitorException e) {
|
|
// monitor class initialisation failed
|
|
addLogEntry("ERROR: " ~ e.msg);
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
|
|
// Filesystem monitor loop variables
|
|
// Immutables
|
|
immutable auto checkOnlineInterval = dur!"seconds"(appConfig.getValueLong("monitor_interval"));
|
|
immutable auto githubCheckInterval = dur!"seconds"(86400);
|
|
immutable auto localEchoDebounce = dur!"seconds"(10);
|
|
immutable ulong fullScanFrequency = appConfig.getValueLong("monitor_fullscan_frequency");
|
|
immutable ulong logOutputSuppressionInterval = appConfig.getValueLong("monitor_log_frequency");
|
|
immutable bool webhookEnabled = appConfig.getValueBool("webhook_enabled");
|
|
immutable string loopStartOutputMessage = "################################################## NEW LOOP ##################################################";
|
|
immutable string loopStopOutputMessage = "################################################ LOOP COMPLETE ###############################################";
|
|
|
|
// Changeable variables
|
|
ulong monitorLoopFullCount = 0;
|
|
ulong fullScanFrequencyLoopCount = 0;
|
|
ulong monitorLogOutputLoopCount = 0;
|
|
MonoTime lastCheckTime = MonoTime.currTime();
|
|
MonoTime lastGitHubCheckTime = MonoTime.currTime();
|
|
|
|
while (performFileSystemMonitoring) {
|
|
// Do we need to validate the runtimeSyncDirectory to check for the presence of a '.nosync' file - the disk may have been ejected ..
|
|
checkForNoMountScenario();
|
|
|
|
// If we are in a --download-only method of operation, there is no filesystem monitoring, so no inotify events to check
|
|
if (!appConfig.getValueBool("download_only")) {
|
|
// Process any inotify events
|
|
processInotifyEvents(true);
|
|
}
|
|
|
|
// WebSocket and Webhook Notification Handling
|
|
bool notificationReceived = false;
|
|
|
|
// If we are doing --upload-only however .. we need to 'ignore' online change
|
|
if (!appConfig.getValueBool("upload_only")) {
|
|
// Check for notifications pushed from Microsoft to the webhook
|
|
if (webhookEnabled) {
|
|
// Create a subscription on the first run, or renew the subscription
|
|
// on subsequent runs when it is about to expire.
|
|
if (oneDriveWebhook is null) {
|
|
oneDriveWebhook = new OneDriveWebhook(thisTid, appConfig);
|
|
oneDriveWebhook.serve();
|
|
} else {
|
|
oneDriveWebhook.createOrRenewSubscription();
|
|
}
|
|
} else {
|
|
// WebSocket support is enabled by default, but only if the version of libcurl supports it
|
|
if (appConfig.curlSupportsWebSockets) {
|
|
// Did the user configure to disable 'websocket' support?
|
|
if (!appConfig.getValueBool("disable_websocket_support")) {
|
|
// Do we need to renew the notification URL?
|
|
auto renewEarly = dur!"seconds"(120);
|
|
if (appConfig.websocketNotificationUrlAvailable && appConfig.websocketUrlExpiry.length) {
|
|
auto expiry = SysTime.fromISOExtString(appConfig.websocketUrlExpiry);
|
|
auto now = Clock.currTime(UTC());
|
|
if (expiry - now <= renewEarly) {
|
|
try {
|
|
// Obtain the WebSocket Notification URL from the API endpoint
|
|
syncEngineInstance.obtainWebSocketNotificationURL();
|
|
if (debugLogging) addLogEntry("Refreshed WebSocket notification URL prior to expiry", ["debug"]);
|
|
} catch (Exception e) {
|
|
if (debugLogging) addLogEntry("Failed to refresh WebSocket notification URL: " ~ e.msg, ["debug"]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get the current time this loop is starting
|
|
auto currentTime = MonoTime.currTime();
|
|
|
|
// Do we perform a sync with OneDrive?
|
|
if ((currentTime - lastCheckTime >= checkOnlineInterval) || (monitorLoopFullCount == 0)) {
|
|
// Increment relevant counters
|
|
monitorLoopFullCount++;
|
|
fullScanFrequencyLoopCount++;
|
|
monitorLogOutputLoopCount++;
|
|
|
|
// If full scan at a specific frequency enabled?
|
|
if (fullScanFrequency > 0) {
|
|
// Full Scan set for some 'frequency' - do we flag to perform a full scan of the online data?
|
|
if (fullScanFrequencyLoopCount > fullScanFrequency) {
|
|
// set full scan trigger for true up
|
|
if (debugLogging) {addLogEntry("Enabling Full Scan True Up (fullScanFrequencyLoopCount > fullScanFrequency), resetting fullScanFrequencyLoopCount = 1", ["debug"]);}
|
|
fullScanFrequencyLoopCount = 1;
|
|
appConfig.fullScanTrueUpRequired = true;
|
|
} else {
|
|
// unset full scan trigger for true up
|
|
if (debugLogging) {addLogEntry("Disabling Full Scan True Up", ["debug"]);}
|
|
appConfig.fullScanTrueUpRequired = false;
|
|
}
|
|
} else {
|
|
// No it is disabled - ensure this is false
|
|
appConfig.fullScanTrueUpRequired = false;
|
|
}
|
|
|
|
// Loop Start
|
|
if (debugLogging) {
|
|
addLogEntry(loopStartOutputMessage, ["debug"]);
|
|
addLogEntry("Total Run-Time Loop Number: " ~ to!string(monitorLoopFullCount), ["debug"]);
|
|
addLogEntry("Full Scan Frequency Loop Number: " ~ to!string(fullScanFrequencyLoopCount), ["debug"]);
|
|
}
|
|
SysTime startFunctionProcessingTime = Clock.currTime();
|
|
if (debugLogging) {addLogEntry("Start Monitor Loop Time: " ~ to!string(startFunctionProcessingTime), ["debug"]);}
|
|
|
|
// Do we perform any monitor console logging output suppression?
|
|
// 'monitor_log_frequency' controls how often, in a non-verbose application output mode, how often
|
|
// the full output of what is occurring is done. This is done to lessen the 'verbosity' of non-verbose
|
|
// logging, but only when running in --monitor
|
|
if (monitorLogOutputLoopCount > logOutputSuppressionInterval) {
|
|
// re-enable the logging output as required
|
|
monitorLogOutputLoopCount = 1;
|
|
if (debugLogging) {addLogEntry("Allowing initial sync log output", ["debug"]);}
|
|
appConfig.suppressLoggingOutput = false;
|
|
} else {
|
|
// do we suppress the logging output to absolute minimal
|
|
if (monitorLoopFullCount == 1) {
|
|
// application startup with --monitor
|
|
if (debugLogging) {addLogEntry("Allowing initial sync log output", ["debug"]);}
|
|
appConfig.suppressLoggingOutput = false;
|
|
} else {
|
|
// only suppress if we are not doing --verbose or higher
|
|
if (appConfig.verbosityCount == 0) {
|
|
if (debugLogging) {addLogEntry("Suppressing --monitor log output", ["debug"]);}
|
|
appConfig.suppressLoggingOutput = true;
|
|
} else {
|
|
if (debugLogging) {addLogEntry("Allowing log output", ["debug"]);}
|
|
appConfig.suppressLoggingOutput = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// How long has the application been running for?
|
|
auto elapsedTime = Clock.currTime() - applicationStartTime;
|
|
if (debugLogging) {addLogEntry("Application run-time thus far: " ~ to!string(elapsedTime), ["debug"]);}
|
|
|
|
// Need to re-validate that the client is still online for this loop
|
|
if (testInternetReachability(appConfig)) {
|
|
// Starting a sync - we are online
|
|
addLogEntry("Starting a sync with Microsoft OneDrive");
|
|
|
|
// Attempt to reset syncFailures from any prior loop
|
|
syncEngineInstance.resetSyncFailures();
|
|
|
|
// Update cached quota details from online as this may have changed online in the background outside of this application
|
|
syncEngineInstance.freshenCachedDriveQuotaDetails();
|
|
|
|
// Did the user specify --upload-only?
|
|
if (appConfig.getValueBool("upload_only")) {
|
|
// Perform the --upload-only sync process
|
|
performUploadOnlySyncProcess(localPath, filesystemMonitor);
|
|
} else {
|
|
// Perform the standard sync process
|
|
performStandardSyncProcess(localPath, filesystemMonitor);
|
|
}
|
|
|
|
// Handle any new inotify events
|
|
processInotifyEvents(true);
|
|
|
|
// Detail the outcome of the sync process
|
|
displaySyncOutcome();
|
|
|
|
// Cleanup sync process arrays
|
|
syncEngineInstance.cleanupArrays();
|
|
|
|
// Write WAL and SHM data to file for this loop and release memory used by in-memory processing
|
|
if (debugLogging) {addLogEntry("Merge contents of WAL and SHM files into main database file", ["debug"]);}
|
|
itemDB.performCheckpoint("PASSIVE");
|
|
} else {
|
|
// Not online
|
|
addLogEntry("Microsoft OneDrive service is not reachable at this time. Will re-try on next sync attempt.");
|
|
}
|
|
|
|
// Output end of loop processing times
|
|
SysTime endFunctionProcessingTime = Clock.currTime();
|
|
if (debugLogging) {
|
|
addLogEntry("End Monitor Loop Time: " ~ to!string(endFunctionProcessingTime), ["debug"]);
|
|
addLogEntry("Elapsed Monitor Loop Processing Time: " ~ to!string((endFunctionProcessingTime - startFunctionProcessingTime)), ["debug"]);
|
|
}
|
|
|
|
// Release all the curl instances used during this loop
|
|
// New curl instances will be established on next loop
|
|
if (debugLogging) {addLogEntry("CurlEngine Pool Size PRE Cleanup: " ~ to!string(curlEnginePoolLength()), ["debug"]);}
|
|
releaseAllCurlInstances(); // Release all CurlEngine instances
|
|
if (debugLogging) {addLogEntry("CurlEngine Pool Size POST Cleanup: " ~ to!string(curlEnginePoolLength()) , ["debug"]);}
|
|
|
|
// Display memory details before garbage collection
|
|
if (displayMemoryUsage) {
|
|
addLogEntry("Monitor Loop Count: " ~ to!string(monitorLoopFullCount));
|
|
// Get the current time in the local timezone
|
|
auto timeStamp = leftJustify(Clock.currTime().toString(), 28, '0');
|
|
addLogEntry("Timestamp: " ~ to!string(timeStamp));
|
|
addLogEntry("Application Run Time: " ~ to!string(elapsedTime));
|
|
// Display memory stats before GC cleanup
|
|
displayMemoryUsagePreGC();
|
|
}
|
|
// Perform Garbage Collection
|
|
GC.collect();
|
|
// Return free memory to the OS
|
|
GC.minimize();
|
|
// Display memory details after garbage collection
|
|
if (displayMemoryUsage) displayMemoryUsagePostGC();
|
|
|
|
// Log that this loop is complete
|
|
if (debugLogging) {addLogEntry(loopStopOutputMessage, ["debug"]);}
|
|
|
|
// performSync complete, set lastCheckTime to current time
|
|
lastCheckTime = MonoTime.currTime();
|
|
|
|
// Developer break via config option
|
|
if (appConfig.getValueLong("monitor_max_loop") > 0) {
|
|
// developer set option to limit --monitor loops
|
|
if (monitorLoopFullCount == (appConfig.getValueLong("monitor_max_loop"))) {
|
|
performFileSystemMonitoring = false;
|
|
addLogEntry("Exiting after " ~ to!string(monitorLoopFullCount) ~ " loops due to developer set option");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (performFileSystemMonitoring) {
|
|
auto nextCheckTime = lastCheckTime + checkOnlineInterval;
|
|
currentTime = MonoTime.currTime();
|
|
auto sleepTime = nextCheckTime - currentTime;
|
|
if (debugLogging) {addLogEntry("Sleep for " ~ to!string(sleepTime), ["debug"]);}
|
|
|
|
if (filesystemMonitor.initialised || webhookEnabled || oneDriveSocketIo !is null) {
|
|
|
|
if (filesystemMonitor.initialised) {
|
|
// If local monitor is on and is waiting (previous event was not from webhook)
|
|
|
|
// Obsidian Editor has been written in such a way that it is constantly writing each and every keystroke to a file.
|
|
// Not only is this really bad application behaviour, for this client, this means the application is constantly writing to disk, thus attempting to upload file changes.
|
|
// Unfortunately Obsidian on Linux does not provide a built-in way to disable atomic saves or switch to a backup-copy method via configuration.
|
|
if (appConfig.getValueBool("delay_inotify_processing")) {
|
|
Thread.sleep(dur!("seconds")(to!int(appConfig.getValueLong("inotify_delay"))));
|
|
}
|
|
|
|
// Start the filesystem monitor (inotify) worker and wait for inotify event
|
|
if (!notificationReceived) {
|
|
filesystemMonitor.send(true);
|
|
}
|
|
}
|
|
|
|
// Adjust sleepTime based on webhook/websocket only when NOT upload_only
|
|
if (!appConfig.getValueBool("upload_only")) {
|
|
if (webhookEnabled) {
|
|
Duration nextWebhookCheckDuration = oneDriveWebhook.getNextExpirationCheckDuration();
|
|
if (nextWebhookCheckDuration < sleepTime) sleepTime = nextWebhookCheckDuration;
|
|
notificationReceived = false;
|
|
} else if (!appConfig.getValueBool("disable_websocket_support") && appConfig.curlSupportsWebSockets) {
|
|
Duration nextWebsocketCheckDuration = oneDriveSocketIo.getNextExpirationCheckDuration();
|
|
if (nextWebsocketCheckDuration < sleepTime) sleepTime = nextWebsocketCheckDuration;
|
|
}
|
|
}
|
|
|
|
// ALWAYS wait for FS worker, but only track webhook/websocket if NOT '--upload-only'
|
|
int res = 1;
|
|
bool onlineSignal = false;
|
|
|
|
if (appConfig.getValueBool("upload_only")) {
|
|
receiveTimeout(sleepTime, (int msg) { res = msg; });
|
|
} else {
|
|
receiveTimeout(sleepTime, (int msg) { res = msg; }, (ulong _) { onlineSignal = true; });
|
|
}
|
|
|
|
// Debug logging of worker status
|
|
if (debugLogging) {
|
|
addLogEntry("worker status = " ~ to!string(res), ["debug"]);
|
|
if (!appConfig.getValueBool("upload_only")) {
|
|
addLogEntry("notificationReceived = " ~ to!string(onlineSignal), ["debug"]);
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
|
|
// Only process online notifications if NOT '--upload-only'
|
|
if (!appConfig.getValueBool("upload_only") && onlineSignal) {
|
|
int signalCount = 1;
|
|
while (true) {
|
|
auto more = receiveTimeout(dur!"seconds"(-1), (ulong _) {});
|
|
if (more) {
|
|
signalCount++;
|
|
} else {
|
|
auto now = MonoTime.currTime();
|
|
auto sinceLocal = now - lastLocalWrite;
|
|
if (sinceLocal < localEchoDebounce) {
|
|
if (debugLogging) {
|
|
addLogEntry(
|
|
"Debounced online refresh signal (" ~
|
|
to!string(sinceLocal.total!"msecs"()) ~ " ms since local write; threshold " ~
|
|
to!string(localEchoDebounce.total!"msecs"()) ~ " ms)",
|
|
["debug"]
|
|
);
|
|
}
|
|
|
|
// Ignore this reflection; skip the immediate online scan.
|
|
// Next push or the regular monitor cadence will pick up genuine remote changes.
|
|
break;
|
|
}
|
|
|
|
// Get the signal timestamp - this is as close as possible to when this was received
|
|
SysTime signalTimeStamp = Clock.currTime();
|
|
signalTimeStamp.fracSecs = Duration.zero;
|
|
|
|
// Log what signal we received
|
|
if (webhookEnabled) {
|
|
string webhookLogEntry = format("Received %s signal(s) from Webhook handler (%s)", to!string(signalCount), to!string(signalTimeStamp));
|
|
addLogEntry(webhookLogEntry);
|
|
} else {
|
|
string websocketLogEntry = format("Received %s signal(s) from WebSocket handler (%s)", to!string(signalCount), to!string(signalTimeStamp));
|
|
addLogEntry(websocketLogEntry);
|
|
}
|
|
|
|
// Perform online callback action
|
|
oneDriveOnlineCallback();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Worker failure remains outside '--upload-only' filter
|
|
if (res == -1) {
|
|
addLogEntry("ERROR: Monitor worker failed.");
|
|
monitorFailures = true;
|
|
performFileSystemMonitoring = false;
|
|
}
|
|
} else {
|
|
// no hooks available, nothing to check
|
|
Thread.sleep(sleepTime);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Exit application as the sync engine could not be initialised
|
|
addLogEntry("Application Sync Engine could not be initialised correctly");
|
|
// Use exit scope
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
// Exit application using exit scope
|
|
if (!syncEngineInstance.syncFailures && !monitorFailures) {
|
|
return EXIT_SUCCESS;
|
|
} else {
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
|
|
// Set default application threads
|
|
void setDefaultApplicationThreads() {
|
|
// Read in system values
|
|
int configuredThreads = to!int(appConfig.getValueLong("threads"));
|
|
int systemCPUs = totalCPUs;
|
|
|
|
// Warning if configuredThreads is too high
|
|
if (configuredThreads > systemCPUs) {
|
|
addLogEntry();
|
|
addLogEntry("WARNING: Configured 'threads = " ~ to!string(configuredThreads) ~ "' exceeds available CPU cores (" ~ to!string(systemCPUs) ~ ").");
|
|
addLogEntry(" This may lead to reduced performance, CPU contention, and instability. For best results, set 'threads' no higher than the number of physical CPU cores.");
|
|
addLogEntry();
|
|
}
|
|
|
|
// Set the default threads based on configured option
|
|
defaultPoolThreads(configuredThreads);
|
|
}
|
|
|
|
// Retrieves the maximum inotify watches allowed by the system
|
|
string getMaxInotifyWatches() {
|
|
// Predefined Versions
|
|
// https://dlang.org/spec/version.html#predefined-versions
|
|
version (linux) {
|
|
try {
|
|
// Read max inotify watches from procfs on Linux
|
|
return strip(readText("/proc/sys/fs/inotify/max_user_watches"));
|
|
} catch (Exception e) {
|
|
return "Unknown (Error reading /proc/sys/fs/inotify/max_user_watches)";
|
|
}
|
|
} else version (FreeBSD) {
|
|
// FreeBSD uses kqueue instead of inotify, no direct equivalent
|
|
return "N/A (uses kqueue)";
|
|
} else version (OpenBSD) {
|
|
// OpenBSD uses kqueue instead of inotify, no direct equivalent
|
|
return "N/A (uses kqueue)";
|
|
} else {
|
|
return "Unsupported platform";
|
|
}
|
|
}
|
|
|
|
// Print error message when --sync or --monitor has not been used and no valid 'no-sync' operation was requested
|
|
void printMissingOperationalSwitchesError() {
|
|
// notify the user that --sync or --monitor were missing
|
|
addLogEntry();
|
|
addLogEntry("Your command line input is missing either the '--sync' or '--monitor' switches. Please include one (but not both) of these switches in your command line, or refer to 'onedrive --help' for additional guidance.");
|
|
addLogEntry();
|
|
addLogEntry("It is important to note that you must include one of these two arguments in your command line for the application to perform a synchronisation with Microsoft OneDrive");
|
|
addLogEntry();
|
|
}
|
|
|
|
// Function used for WebSocket or Webhook callbacks to perform specific activities
|
|
void oneDriveOnlineCallback() {
|
|
// If we are in a --download-only method of operation, there is no filesystem monitoring, so no inotify events to check
|
|
if (!appConfig.getValueBool("download_only")) {
|
|
// Handle inotify events
|
|
processInotifyEvents(true);
|
|
}
|
|
|
|
// Sync any online change down to the local disk
|
|
// If we are doing --upload-only however .. we need to 'ignore' online change
|
|
if (!appConfig.getValueBool("upload_only")) {
|
|
// We are not doing an --upload-only scenario .. sync online change --> local
|
|
syncEngineInstance.syncOneDriveAccountToLocalDisk();
|
|
}
|
|
if (appConfig.getValueBool("monitor")) {
|
|
// Handle inotify events
|
|
processInotifyEvents(true);
|
|
}
|
|
}
|
|
|
|
// Perform only an upload of data when using --upload-only
|
|
void performUploadOnlySyncProcess(string localPath, Monitor filesystemMonitor = null) {
|
|
// Perform the local database consistency check, picking up locally modified data and uploading this to OneDrive
|
|
syncEngineInstance.performDatabaseConsistencyAndIntegrityCheck();
|
|
if (appConfig.getValueBool("monitor")) {
|
|
// Handle any inotify events whilst the DB was being scanned
|
|
processInotifyEvents(true);
|
|
}
|
|
|
|
// Scan the configured 'sync_dir' for new data to upload
|
|
syncEngineInstance.scanLocalFilesystemPathForNewData(localPath);
|
|
if (appConfig.getValueBool("monitor")) {
|
|
// Handle any new inotify events whilst the local filesystem was being scanned
|
|
processInotifyEvents(true);
|
|
}
|
|
}
|
|
|
|
// Perform the normal application sync process
|
|
void performStandardSyncProcess(string localPath, Monitor filesystemMonitor = null) {
|
|
// If we are performing log suppression, output this message so the user knows what is happening
|
|
if (appConfig.suppressLoggingOutput) {
|
|
addLogEntry("Syncing changes from Microsoft OneDrive ...");
|
|
}
|
|
|
|
// Zero out these arrays
|
|
syncEngineInstance.fileDownloadFailures = [];
|
|
syncEngineInstance.fileUploadFailures = [];
|
|
|
|
// Which way do we sync first?
|
|
// OneDrive first then local changes (normal operational process that uses OneDrive as the source of truth)
|
|
// Local First then OneDrive changes (alternate operation process to use local files as source of truth)
|
|
if (appConfig.getValueBool("local_first")) {
|
|
// Local data first
|
|
// Perform the local database consistency check, picking up locally modified data and uploading this to OneDrive
|
|
syncEngineInstance.performDatabaseConsistencyAndIntegrityCheck();
|
|
if (appConfig.getValueBool("monitor")) {
|
|
// Handle any inotify events whilst the DB was being scanned
|
|
processInotifyEvents(true);
|
|
}
|
|
|
|
// Scan the configured 'sync_dir' for new data to upload to OneDrive
|
|
syncEngineInstance.scanLocalFilesystemPathForNewData(localPath);
|
|
if (appConfig.getValueBool("monitor")) {
|
|
// Handle any new inotify events whilst the local filesystem was being scanned
|
|
processInotifyEvents(true);
|
|
}
|
|
|
|
// Download data from OneDrive last
|
|
syncEngineInstance.syncOneDriveAccountToLocalDisk();
|
|
if (appConfig.getValueBool("monitor")) {
|
|
// Cancel out any inotify events from downloading data
|
|
processInotifyEvents(false);
|
|
}
|
|
|
|
// At this point, we have done a sync from:
|
|
// local -> online
|
|
// online -> local
|
|
//
|
|
// Everything now should be 'in sync' and the database correctly populated with data
|
|
// If --resync was used, we need to unset this as sync.d performs certain queries depending on if 'resync' is set or not
|
|
if (appConfig.getValueBool("resync")) {
|
|
// unset 'resync' now that everything has been performed
|
|
appConfig.setValueBool("resync" , false);
|
|
}
|
|
} else {
|
|
// Normal sync process
|
|
// Download data from OneDrive first
|
|
syncEngineInstance.syncOneDriveAccountToLocalDisk();
|
|
if (appConfig.getValueBool("monitor")) {
|
|
// Cancel out any inotify events from downloading data
|
|
processInotifyEvents(false);
|
|
}
|
|
|
|
// Perform the local database consistency check, picking up locally modified data and uploading this to OneDrive
|
|
syncEngineInstance.performDatabaseConsistencyAndIntegrityCheck();
|
|
if (appConfig.getValueBool("monitor")) {
|
|
// Handle any inotify events whilst the DB was being scanned
|
|
processInotifyEvents(true);
|
|
}
|
|
|
|
// Is --download-only NOT configured?
|
|
if (!appConfig.getValueBool("download_only")) {
|
|
|
|
// Scan the configured 'sync_dir' for new data to upload to OneDrive
|
|
syncEngineInstance.scanLocalFilesystemPathForNewData(localPath);
|
|
if (appConfig.getValueBool("monitor")) {
|
|
// Handle any new inotify events whilst the local filesystem was being scanned
|
|
processInotifyEvents(true);
|
|
}
|
|
|
|
// If we are not doing a 'force_children_scan' perform a true-up
|
|
// 'force_children_scan' is used when using /children rather than /delta and it is not efficient to re-run this exact same process twice
|
|
if (!appConfig.getValueBool("force_children_scan")) {
|
|
// Perform the final true up scan to ensure we have correctly replicated the current online state locally
|
|
if (!appConfig.suppressLoggingOutput) {
|
|
addLogEntry("Performing a last examination of the most recent online data within Microsoft OneDrive to complete the reconciliation process");
|
|
}
|
|
// We pass in the 'appConfig.fullScanTrueUpRequired' value which then flags do we use the configured 'deltaLink'
|
|
// If 'appConfig.fullScanTrueUpRequired' is true, we do not use the 'deltaLink' if we are in --monitor mode, thus forcing a full scan true up
|
|
syncEngineInstance.syncOneDriveAccountToLocalDisk();
|
|
if (appConfig.getValueBool("monitor")) {
|
|
// Cancel out any inotify events from downloading data
|
|
processInotifyEvents(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// At this point, we have done a sync from:
|
|
// online -> local
|
|
// local -> online (if not doing --download-only)
|
|
// online -> local (if not doing --download-only)
|
|
//
|
|
// Everything now should be 'in sync' and the database correctly populated with data
|
|
// If --resync was used, we need to unset this as sync.d performs certain queries depending on if 'resync' is set or not
|
|
if (appConfig.getValueBool("resync")) {
|
|
// unset 'resync' now that everything has been performed
|
|
appConfig.setValueBool("resync" , false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process any inotify events
|
|
void processInotifyEvents(bool updateFlag) {
|
|
// Attempt to process or cancel inotify events
|
|
// filesystemMonitor.update will throw this, thus needs to be caught
|
|
// monitor.MonitorException@src/monitor.d(549): inotify queue overflow: some events may be lost (Interrupted system call)
|
|
try {
|
|
// Process any inotify events or cancel events based on flag value
|
|
// True = process
|
|
// False = cancel
|
|
filesystemMonitor.update(updateFlag);
|
|
} catch (MonitorException e) {
|
|
// Catch any exceptions thrown by inotify / monitor engine
|
|
addLogEntry("ERROR: The following inotify error was generated: " ~ e.msg);
|
|
}
|
|
}
|
|
|
|
// Display the sync outcome
|
|
void displaySyncOutcome() {
|
|
// Detail any download or upload transfer failures
|
|
syncEngineInstance.displaySyncFailures();
|
|
|
|
// Sync is either complete or partially complete
|
|
if (!syncEngineInstance.syncFailures) {
|
|
// No download or upload issues
|
|
if (!appConfig.getValueBool("monitor")) addLogEntry(); // Add an additional line break so that this is clear when using --sync
|
|
addLogEntry("Sync with Microsoft OneDrive is complete");
|
|
} else {
|
|
addLogEntry();
|
|
addLogEntry("Sync with Microsoft OneDrive has completed, however there are items that failed to sync.");
|
|
// Due to how the OneDrive API works 'changes' such as add new files online, rename files online, delete files online are only sent once when using the /delta API call.
|
|
// That we failed to download it, we need to track that, and then issue a --resync to download any of these failed files .. unfortunate, but there is no easy way here
|
|
if (!syncEngineInstance.fileDownloadFailures.empty) {
|
|
addLogEntry("To fix any download failures you may need to perform a --resync to ensure this system is correctly synced with your Microsoft OneDrive Account");
|
|
}
|
|
if (!syncEngineInstance.fileUploadFailures.empty) {
|
|
addLogEntry("To fix any upload failures you may need to perform a --resync to ensure this system is correctly synced with your Microsoft OneDrive Account");
|
|
}
|
|
// So that from a logging perspective these messages are clear, add a line break in
|
|
addLogEntry();
|
|
}
|
|
}
|
|
|
|
// Perform database file removal
|
|
void processResyncDatabaseRemoval(string databaseFilePathToRemove) {
|
|
// Log what we are doing
|
|
if (debugLogging) {addLogEntry("Testing if we have exclusive access to local database file", ["debug"]);}
|
|
|
|
// Are we the only running instance? Test that we can open the database file path
|
|
itemDB = new ItemDatabase(databaseFilePathToRemove);
|
|
|
|
// did we successfully initialise the database class?
|
|
if (!itemDB.isDatabaseInitialised()) {
|
|
// no .. destroy class
|
|
itemDB = null;
|
|
// exit application - void function, force exit this way
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// If we have exclusive access we will not have exited
|
|
// destroy access test
|
|
itemDB = null;
|
|
// delete application sync state
|
|
addLogEntry("Deleting the saved application sync status ...");
|
|
if (!dryRun) {
|
|
safeRemove(databaseFilePathToRemove);
|
|
} else {
|
|
// --dry-run scenario ... technically we should not be making any local file changes .......
|
|
addLogEntry("DRY-RUN: Not removing the saved application sync status");
|
|
}
|
|
}
|
|
|
|
// Clean up the local database files
|
|
void cleanupDatabaseFiles(string activeDatabaseFileName) {
|
|
// Temp variables
|
|
string databaseShmFile = activeDatabaseFileName ~ "-shm";
|
|
string databaseWalFile = activeDatabaseFileName ~ "-wal";
|
|
|
|
// Are we performing a --dry-run?
|
|
if (dryRun) {
|
|
// If the dry run database exists, clean this up
|
|
if (exists(activeDatabaseFileName)) {
|
|
// remove the dry run database file
|
|
if (debugLogging) {addLogEntry("DRY-RUN: Removing items-dryrun.sqlite3 as it still exists for some reason", ["debug"]);}
|
|
safeRemove(activeDatabaseFileName);
|
|
}
|
|
} else {
|
|
// we may have not been using --dry-run, however we may have been running some operations that use a dry-run database, and this needs to be explicitly cleaned up
|
|
if (exists(appConfig.databaseFilePathDryRun)) {
|
|
if (debugLogging) {addLogEntry("Removing items-dryrun.sqlite3 as it still exists for some reason post being used for non-dryrun operations", ["debug"]);}
|
|
safeRemove(appConfig.databaseFilePathDryRun);
|
|
}
|
|
}
|
|
|
|
// Silent cleanup of -shm file if it exists
|
|
if (exists(databaseShmFile)) {
|
|
// Configure the log message
|
|
string logMessage = "Removing " ~ baseName(databaseShmFile) ~ " as it still exists for some reason";
|
|
// Is this a --dry-run scenario
|
|
if (dryRun) {
|
|
logMessage = "DRY-RUN: " ~ logMessage;
|
|
}
|
|
|
|
// Remove -shm file
|
|
if (debugLogging) {addLogEntry(logMessage, ["debug"]);}
|
|
safeRemove(databaseShmFile);
|
|
}
|
|
|
|
// Silent cleanup of wal files if it exists
|
|
if (exists(databaseWalFile)) {
|
|
// Configure the log message
|
|
string logMessage = "Removing " ~ baseName(databaseWalFile) ~ " as it still exists for some reason";
|
|
// Is this a --dry-run scenario
|
|
if (dryRun) {
|
|
logMessage = "DRY-RUN: " ~ logMessage;
|
|
}
|
|
|
|
// Remove -wal file
|
|
if (debugLogging) {addLogEntry(logMessage, ["debug"]);}
|
|
safeRemove(databaseWalFile);
|
|
}
|
|
}
|
|
|
|
// Perform a check to see if this is a mount point, and if the 'mount' has gone
|
|
void checkForNoMountScenario() {
|
|
// If this is a 'mounted' folder, the 'mount point' should have this file to help the application stop any action to preserve data because the drive to mount is not currently mounted
|
|
if (appConfig.getValueBool("check_nomount")) {
|
|
// we were asked to check the mount point for the presence of a '.nosync' file
|
|
if (exists(".nosync")) {
|
|
addLogEntry("ERROR: .nosync file found in directory mount point. Aborting application startup process to safeguard data.", ["info", "notify"]);
|
|
// Perform the shutdown process
|
|
performSynchronisedExitProcess("check_nomount");
|
|
// Exit
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup a signal handler for catching SIGINT, SIGTERM and SIGSEGV (CTRL-C and others) during application execution
|
|
void setupSignalHandler() {
|
|
sigaction_t action;
|
|
action.sa_handler = &exitViaSignalHandler; // Direct function pointer assignment
|
|
sigemptyset(&action.sa_mask); // Initialize the signal set to empty
|
|
action.sa_flags = 0;
|
|
sigaction(SIGINT, &action, null); // Interrupt from keyboard
|
|
sigaction(SIGTERM, &action, null); // Termination signal
|
|
sigaction(SIGSEGV, &action, null); // Invalid Memory Access signal
|
|
}
|
|
|
|
// Catch SIGINT (CTRL-C), SIGTERM (kill) and SIGSEGV (invalid memory access), handle rapid repeat CTRL-C presses
|
|
extern(C) nothrow @nogc @system void exitViaSignalHandler(int signo) {
|
|
|
|
// Update global exitHandlerTriggered flag so that objects that depend on this know we are shutting down
|
|
exitHandlerTriggered = true;
|
|
|
|
// Catch the generation of SIGSEV post SIGINT or SIGTERM event
|
|
if (signo == SIGSEGV) {
|
|
// Was SIGTERM used?
|
|
if (!sigtermHandlerTriggered) {
|
|
// No .. so most likely SIGINT (CTRL-C)
|
|
printf("Due to a termination signal, internal processing stopped abruptly. The application will now exit in a unclean manner.\n");
|
|
exit(130);
|
|
} else {
|
|
// High probability of being shutdown by systemd, for example: systemctl --user stop onedrive
|
|
// Exit in a manner that does not trigger an exit failure in systemd
|
|
exit(0);
|
|
}
|
|
}
|
|
|
|
if (signo == SIGTERM) {
|
|
// systemd will use SIGTERM to terminate a running process
|
|
sigtermHandlerTriggered = true;
|
|
}
|
|
|
|
if (shutdownInProgress) {
|
|
return; // Ignore subsequent presses
|
|
} else {
|
|
// Disable logging suppression
|
|
appConfig.suppressLoggingOutput = false;
|
|
// Flag we are shutting down
|
|
shutdownInProgress = true;
|
|
|
|
try {
|
|
assumeNoGC ( () {
|
|
// Log that a termination signal was caught
|
|
addLogEntry("\nReceived termination signal, attempting to cleanly shutdown application");
|
|
// Try and shutdown in a safe and synchronised manner
|
|
performSynchronisedExitProcess("SIGINT-SIGTERM-HANDLER");
|
|
})();
|
|
} catch (Exception e) {
|
|
// Any output here will cause a GC allocation
|
|
// - Error: `@nogc` function `main.exitHandler` cannot call non-@nogc function `std.stdio.writeln!string.writeln`
|
|
// - Error: cannot use operator `~` in `@nogc` function `main.exitHandler`
|
|
// writeln("Exception during shutdown: " ~ e.msg);
|
|
}
|
|
// Exit the process with the provided exit code
|
|
exit(signo);
|
|
}
|
|
}
|
|
|
|
// Handle application exit
|
|
void performSynchronisedExitProcess(string scopeCaller = null) {
|
|
synchronized {
|
|
// Perform cleanup and shutdown of various services and resources
|
|
try {
|
|
// Log who called this function
|
|
if (debugLogging) {addLogEntry("performSynchronisedExitProcess called by: " ~ scopeCaller, ["debug"]);}
|
|
// Remove Desktop integration
|
|
if(performFileSystemMonitoring) {
|
|
// Was desktop integration enabled?
|
|
if (appConfig.getValueBool("display_manager_integration")) {
|
|
// Attempt removal
|
|
attemptFileManagerIntegrationRemoval();
|
|
}
|
|
}
|
|
// Shutdown the OneDrive Webhook instance
|
|
shutdownOneDriveWebhook();
|
|
// Shutdown the OneDrive WebSocket instance
|
|
shutdownOneDriveSocketIo();
|
|
// Shutdown any local filesystem monitoring
|
|
shutdownFilesystemMonitor();
|
|
// Shutdown the sync engine
|
|
if (scopeCaller == "SIGINT-SIGTERM-HANDLER") {
|
|
// Wait for all parallel jobs that depend on the database being available to complete
|
|
addLogEntry("Waiting for any existing upload|download process to complete");
|
|
}
|
|
shutdownSyncEngine();
|
|
// Release all CurlEngine instances
|
|
releaseAllCurlInstances();
|
|
// Shutdown the client side filtering objects
|
|
shutdownSelectiveSync();
|
|
// Shutdown the database
|
|
shutdownDatabase();
|
|
// Shutdown the application configuration objects - nothing should be active now
|
|
shutdownAppConfig();
|
|
// Shutdown application logging
|
|
shutdownApplicationLogging();
|
|
} catch (Exception e) {
|
|
addLogEntry("Error during performStandardExitProcess: " ~ e.toString(), ["error"]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void shutdownOneDriveWebhook() {
|
|
if (oneDriveWebhook !is null) {
|
|
if (debugLogging) {addLogEntry("Shutting down OneDrive Webhook instance", ["debug"]);}
|
|
oneDriveWebhook.stop();
|
|
object.destroy(oneDriveWebhook);
|
|
oneDriveWebhook = null;
|
|
if (debugLogging) {addLogEntry("Shutdown of OneDrive Webhook instance is complete", ["debug"]);}
|
|
}
|
|
}
|
|
|
|
void shutdownOneDriveSocketIo() {
|
|
if (oneDriveSocketIo !is null) {
|
|
if (debugLogging) addLogEntry("Shutting down OneDrive WebSocket instance", ["debug"]);
|
|
oneDriveSocketIo.stop();
|
|
object.destroy(oneDriveSocketIo);
|
|
oneDriveSocketIo = null;
|
|
if (debugLogging) addLogEntry("Shutdown of OneDrive WebSocket instance complete", ["debug"]);
|
|
}
|
|
}
|
|
|
|
void shutdownFilesystemMonitor() {
|
|
if (filesystemMonitor !is null) {
|
|
if (debugLogging) {addLogEntry("Shutting down Filesystem Monitoring instance", ["debug"]);}
|
|
filesystemMonitor.shutdown();
|
|
object.destroy(filesystemMonitor);
|
|
filesystemMonitor = null;
|
|
if (debugLogging) {addLogEntry("Shutdown of Filesystem Monitoring instance is complete", ["debug"]);}
|
|
}
|
|
}
|
|
|
|
void shutdownSelectiveSync() {
|
|
if (selectiveSync !is null) {
|
|
if (debugLogging) {addLogEntry("Shutting down Client Side Filtering instance", ["debug"]);}
|
|
selectiveSync.shutdown();
|
|
object.destroy(selectiveSync);
|
|
selectiveSync = null;
|
|
if (debugLogging) {addLogEntry("Shutdown of Client Side Filtering instance is complete", ["debug"]);}
|
|
}
|
|
}
|
|
|
|
void shutdownSyncEngine() {
|
|
if (syncEngineInstance !is null) {
|
|
if (debugLogging) {addLogEntry("Shutting down Sync Engine instance", ["debug"]);}
|
|
syncEngineInstance.shutdown(); // Make sure any running thread completes first
|
|
object.destroy(syncEngineInstance);
|
|
syncEngineInstance = null;
|
|
if (debugLogging) {addLogEntry("Shutdown Sync Engine instance is complete", ["debug"]);}
|
|
}
|
|
}
|
|
|
|
void shutdownDatabase() {
|
|
if (itemDB !is null && itemDB.isDatabaseInitialised()) {
|
|
if (debugLogging) {addLogEntry("Shutting down Database instance", ["debug"]);}
|
|
|
|
// Write WAL and SHM data to file
|
|
if (debugLogging) {addLogEntry("Merge contents of WAL and SHM files into main database file before shutting down database", ["debug"]);}
|
|
itemDB.performCheckpoint("TRUNCATE");
|
|
|
|
// Do we perform a database vacuum?
|
|
if (performDatabaseVacuum) {
|
|
// Logging to attempt this is denoted from performVacuum() - so no need to confirm here
|
|
itemDB.performVacuum();
|
|
// If this completes, it is denoted from performVacuum() - so no need to confirm here
|
|
}
|
|
|
|
// Close the DB File Handle
|
|
itemDB.closeDatabaseFile();
|
|
object.destroy(itemDB);
|
|
cleanupDatabaseFiles(runtimeDatabaseFile);
|
|
itemDB = null;
|
|
if (debugLogging) {addLogEntry("Shutdown of Database instance is complete", ["debug"]);}
|
|
}
|
|
}
|
|
|
|
void shutdownAppConfig() {
|
|
if (appConfig !is null) {
|
|
if (debugLogging) {addLogEntry("Shutting down Application Configuration instance", ["debug"]);}
|
|
object.destroy(appConfig);
|
|
appConfig = null;
|
|
if (debugLogging) {addLogEntry("Shutdown of Application Configuration instance is complete", ["debug"]);}
|
|
}
|
|
}
|
|
|
|
void shutdownApplicationLogging() {
|
|
// Log that we are exiting
|
|
if (loggingStillInitialised()) {
|
|
if (loggingActive()) {
|
|
// join all threads
|
|
thread_joinAll();
|
|
if (debugLogging) {addLogEntry("Application is exiting", ["debug"]);}
|
|
addLogEntry("#######################################################################################################################################", ["logFileOnly"]);
|
|
// Destroy the shared logging buffer which flushes any remaining logs
|
|
if (debugLogging) {addLogEntry("Shutting down Application Logging instance", ["debug"]);}
|
|
// Allow any logging complete before we exit
|
|
Thread.sleep(dur!("msecs")(500));
|
|
// Shutdown Logging which also sets logBuffer to null
|
|
shutdownLogging();
|
|
}
|
|
}
|
|
}
|
|
|
|
string compilerDetails() {
|
|
version(DigitalMars) enum compiler = "DMD";
|
|
else version(LDC) enum compiler = "LDC";
|
|
else version(GNU) enum compiler = "GDC";
|
|
else enum compiler = "Unknown compiler";
|
|
string compilerString = compiler ~ " " ~ to!string(__VERSION__);
|
|
return compilerString;
|
|
}
|
|
|
|
void attemptFileManagerIntegration() {
|
|
// Are we running under a Desktop Manager (GNOME or KDE)?
|
|
if (appConfig.isGuiSessionDetected()) {
|
|
// Generate desktop hints
|
|
auto hints = appConfig.detectDesktop();
|
|
|
|
// GNOME Desktop File Manager integration
|
|
if (hints.gnome) {
|
|
// Attempt integration
|
|
appConfig.addGnomeBookmark();
|
|
appConfig.setOneDriveFolderIcon();
|
|
return;
|
|
}
|
|
|
|
// KDE Desktop File Manager integration
|
|
if (hints.kde) {
|
|
// Attempt integration
|
|
appConfig.addKDEPlacesEntry();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void attemptFileManagerIntegrationRemoval() {
|
|
// Are we running under a Desktop Manager (GNOME or KDE)?
|
|
if (appConfig.isGuiSessionDetected()) {
|
|
// Generate desktop hints
|
|
auto hints = appConfig.detectDesktop();
|
|
|
|
// GNOME Desktop File Manager integration removal
|
|
if (hints.gnome) {
|
|
// Attempt integration removal
|
|
appConfig.removeGnomeBookmark();
|
|
appConfig.removeOneDriveFolderIcon();
|
|
return;
|
|
}
|
|
|
|
// KDE Desktop File Manager integration removal
|
|
if (hints.kde) {
|
|
// Attempt integration removal
|
|
appConfig.removeKDEPlacesEntry();
|
|
return;
|
|
}
|
|
}
|
|
}
|