abraunegg-onedrive/src/main.d
abraunegg 12947d160f
Fix unhandled monitor initialisation exception (Issue #704) (#705)
* Catch MonitorException when initialisation failure occurs, print error and exit ... cant enter monitor loop if we cant initialise correctly.
* Cleanup and add documentation update
2019-10-30 17:46:02 +11:00

905 lines
34 KiB
D

import core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit;
import core.memory, core.time, core.thread;
import std.getopt, std.file, std.path, std.process, std.stdio, std.conv, std.algorithm.searching, std.string, std.regex;
import config, itemdb, monitor, onedrive, selective, sync, util;
import std.net.curl: CurlException;
import core.stdc.signal;
import std.traits;
static import log;
OneDriveApi oneDrive;
ItemDatabase itemDb;
const int EXIT_UNAUTHORIZED = 3;
enum MONITOR_LOG_SILENT = 2;
enum MONITOR_LOG_QUIET = 1;
enum LOG_NORMAL = 0;
int main(string[] args)
{
// Disable buffering on stdout
stdout.setvbuf(0, _IONBF);
// configuration directory
string confdirOption;
try {
// print the version and exit
bool printVersion = false;
auto opt = getopt(
args,
std.getopt.config.passThrough,
std.getopt.config.bundling,
std.getopt.config.caseSensitive,
"confdir", "Set the directory used to store the configuration files", &confdirOption,
"verbose|v+", "Print more details, useful for debugging (repeat for extra debugging)", &log.verbose,
"version", "Print the version and exit", &printVersion
);
if (opt.helpWanted) {
args ~= "--help";
}
if (printVersion) {
std.stdio.write("onedrive ", import("version"));
return EXIT_SUCCESS;
}
} catch (GetOptException e) {
log.error(e.msg);
log.error("Try 'onedrive -h' for more information");
return EXIT_FAILURE;
} catch (Exception e) {
// error
log.error(e.msg);
log.error("Try 'onedrive -h' for more information");
return EXIT_FAILURE;
}
// load configuration file if available
auto cfg = new config.Config(confdirOption);
if (!cfg.initialize()) {
// There was an error loading the configuration
// Error message already printed
return EXIT_FAILURE;
}
// update configuration from command line args
cfg.update_from_args(args);
// Has any of our configuration that would require a --resync been changed?
// 1. sync_list file modification
// 2. config file modification - but only if sync_dir, skip_dir, skip_file or drive_id was modified
// 3. CLI input overriding configured config file option
string currentConfigHash;
string currentSyncListHash;
string previousConfigHash;
string previousSyncListHash;
string configHashFile = cfg.configDirName ~ "/.config.hash";
string syncListHashFile = cfg.configDirName ~ "/.sync_list.hash";
string configBackupFile = cfg.configDirName ~ "/.config.backup";
bool configOptionsDifferent = false;
bool syncListConfigured = false;
bool syncListDifferent = false;
bool syncDirDifferent = false;
bool skipFileDifferent = false;
bool skipDirDifferent = false;
if ((exists(cfg.configDirName ~ "/config")) && (!exists(configHashFile))) {
// Hash of config file needs to be created
std.file.write(configHashFile, computeQuickXorHash(cfg.configDirName ~ "/config"));
}
if ((exists(cfg.configDirName ~ "/sync_list")) && (!exists(syncListHashFile))) {
// Hash of sync_list file needs to be created
std.file.write(syncListHashFile, computeQuickXorHash(cfg.configDirName ~ "/sync_list"));
}
// If hash files exist, but config files do not ... remove the hash, but only if --resync was issued as now the application will use 'defaults' which 'may' be different
if ((!exists(cfg.configDirName ~ "/config")) && (exists(configHashFile))) {
// if --resync safe remove config.hash and config.backup
if (cfg.getValueBool("resync")) {
safeRemove(configHashFile);
safeRemove(configBackupFile);
}
}
if ((!exists(cfg.configDirName ~ "/sync_list")) && (exists(syncListHashFile))) {
// if --resync safe remove sync_list.hash
if (cfg.getValueBool("resync")) safeRemove(syncListHashFile);
}
// Read config hashes if they exist
if (exists(cfg.configDirName ~ "/config")) currentConfigHash = computeQuickXorHash(cfg.configDirName ~ "/config");
if (exists(cfg.configDirName ~ "/sync_list")) currentSyncListHash = computeQuickXorHash(cfg.configDirName ~ "/sync_list");
if (exists(configHashFile)) previousConfigHash = readText(configHashFile);
if (exists(syncListHashFile)) previousSyncListHash = readText(syncListHashFile);
// Was sync_list updated?
if (currentSyncListHash != previousSyncListHash) {
// Debugging output to assist what changed
log.vdebug("sync_list file has been updated, --resync needed");
syncListDifferent = true;
}
// Was config updated?
if (currentConfigHash != previousConfigHash) {
// config file was updated, however we only want to trigger a --resync requirement if sync_dir, skip_dir, skip_file or drive_id was modified
log.vdebug("config file has been updated, checking if --resync needed");
if (exists(configBackupFile)) {
// check backup config what has changed for these configuration options if anything
// # sync_dir = "~/OneDrive"
// # skip_file = "~*|.~*|*.tmp"
// # skip_dir = ""
// # drive_id = ""
string[string] stringValues;
stringValues["sync_dir"] = "";
stringValues["skip_file"] = "";
stringValues["skip_dir"] = "";
stringValues["drive_id"] = "";
auto file = File(configBackupFile, "r");
auto r = regex(`^(\w+)\s*=\s*"(.*)"\s*$`);
foreach (line; file.byLine()) {
line = stripLeft(line);
if (line.length == 0 || line[0] == ';' || line[0] == '#') continue;
auto c = line.matchFirst(r);
if (!c.empty) {
c.popFront(); // skip the whole match
string key = c.front.dup;
auto p = key in stringValues;
if (p) {
c.popFront();
// compare this key
if ((key == "sync_dir") && (c.front.dup != cfg.getValueString("sync_dir"))) {
log.vdebug(key, " was modified since the last time the application was successfully run, --resync needed");
configOptionsDifferent = true;
}
if ((key == "skip_file") && (c.front.dup != cfg.getValueString("skip_file"))){
log.vdebug(key, " was modified since the last time the application was successfully run, --resync needed");
configOptionsDifferent = true;
}
if ((key == "skip_dir") && (c.front.dup != cfg.getValueString("skip_dir"))){
log.vdebug(key, " was modified since the last time the application was successfully run, --resync needed");
configOptionsDifferent = true;
}
if ((key == "drive_id") && (c.front.dup != cfg.getValueString("drive_id"))){
log.vdebug(key, " was modified since the last time the application was successfully run, --resync needed");
configOptionsDifferent = true;
}
}
}
}
} else {
// no backup to check
log.vdebug("WARNING: no backup config file was found, unable to validate if any changes made");
}
// If there was a backup, any modified values we need to worry about would been detected
if (!cfg.getValueBool("display_config")) {
// we are not testing the configuration
if (!configOptionsDifferent) {
// no options are different
if (!cfg.getValueBool("dry_run")) {
// we are not in a dry-run scenario
// update config hash
log.vdebug("updating config hash as it is out of date");
std.file.write(configHashFile, computeQuickXorHash(cfg.configDirName ~ "/config"));
// create backup copy of current config file
log.vdebug("making backup of config file as it is out of date");
std.file.copy(cfg.configDirName ~ "/config", configBackupFile);
}
}
}
}
// Is there a backup of the config file if the config file exists?
if ((exists(cfg.configDirName ~ "/config")) && (!exists(configBackupFile))) {
// create backup copy of current config file
std.file.copy(cfg.configDirName ~ "/config", configBackupFile);
}
// config file set options can be changed via CLI input, specifically these will impact sync and --resync will be needed:
// --syncdir ARG
// --skip-file ARG
// --skip-dir ARG
if (exists(cfg.configDirName ~ "/config")) {
// config file exists
// was the sync_dir updated by CLI?
if (cfg.configFileSyncDir != "") {
// sync_dir was set in config file
if (cfg.configFileSyncDir != cfg.getValueString("sync_dir")) {
// config file was set and CLI input changed this
log.vdebug("sync_dir: CLI override of config file option, --resync needed");
syncDirDifferent = true;
}
}
// was the skip_file updated by CLI?
if (cfg.configFileSkipFile != "") {
// skip_file was set in config file
if (cfg.configFileSkipFile != cfg.getValueString("skip_file")) {
// config file was set and CLI input changed this
log.vdebug("skip_file: CLI override of config file option, --resync needed");
skipFileDifferent = true;
}
}
// was the skip_dir updated by CLI?
if (cfg.configFileSkipDir != "") {
// skip_dir was set in config file
if (cfg.configFileSkipDir != cfg.getValueString("skip_dir")) {
// config file was set and CLI input changed this
log.vdebug("skip_dir: CLI override of config file option, --resync needed");
skipDirDifferent = true;
}
}
}
// Has anything triggered a --resync requirement?
if (configOptionsDifferent || syncListDifferent || syncDirDifferent || skipFileDifferent || skipDirDifferent) {
// --resync needed, is the user just testing configuration changes?
if (!cfg.getValueBool("display_config")){
// not testing configuration changes
if (!cfg.getValueBool("resync")) {
// --resync not issued, fail fast
log.error("An application configuration change has been detected where a --resync is required");
return EXIT_FAILURE;
} else {
// --resync issued, update hashes of config files if they exist
if (!cfg.getValueBool("dry_run")) {
// not doing a dry run, update hash files if config & sync_list exist
if (exists(cfg.configDirName ~ "/config")) {
// update hash
log.vdebug("updating config hash as --resync issued");
std.file.write(configHashFile, computeQuickXorHash(cfg.configDirName ~ "/config"));
// create backup copy of current config file
log.vdebug("making backup of config file as --resync issued");
std.file.copy(cfg.configDirName ~ "/config", configBackupFile);
}
if (exists(cfg.configDirName ~ "/sync_list")) {
// update sync_list hash
log.vdebug("updating sync_list hash as --resync issued");
std.file.write(syncListHashFile, computeQuickXorHash(cfg.configDirName ~ "/sync_list"));
}
}
}
}
}
// dry-run notification
if (cfg.getValueBool("dry_run")) {
log.log("DRY-RUN Configured. Output below shows what 'would' have occurred.");
}
// Are we able to reach the OneDrive Service
bool online = false;
// dry-run database setup
if (cfg.getValueBool("dry_run")) {
// Make a copy of the original items.sqlite3 for use as the dry run copy if it exists
if (exists(cfg.databaseFilePath)) {
// copy the file
log.vdebug("Copying items.sqlite3 to items-dryrun.sqlite3 to use for dry run operations");
copy(cfg.databaseFilePath,cfg.databaseFilePathDryRun);
}
}
// sync_dir environment handling to handle ~ expansion properly
string syncDir;
if ((environment.get("SHELL") == "") && (environment.get("USER") == "")){
log.vdebug("sync_dir: No SHELL or USER environment variable configuration detected");
// No shell or user set, so expandTilde() will fail - usually headless system running under init.d / systemd or potentially Docker
// Does the 'currently configured' sync_dir include a ~
if (canFind(cfg.getValueString("sync_dir"), "~")) {
// A ~ was found
log.vdebug("sync_dir: A '~' was found in sync_dir, using the calculated 'homePath' to replace '~'");
syncDir = cfg.homePath ~ strip(cfg.getValueString("sync_dir"), "~");
} else {
// No ~ found in sync_dir, use as is
log.vdebug("sync_dir: Getting syncDir from config value sync_dir");
syncDir = cfg.getValueString("sync_dir");
}
} else {
// A shell and user is set, expand any ~ as this will be expanded correctly if present
log.vdebug("sync_dir: Getting syncDir from config value sync_dir");
if (canFind(cfg.getValueString("sync_dir"), "~")) {
log.vdebug("sync_dir: A '~' was found in configured sync_dir, automatically expanding as SHELL and USER environment variable is set");
syncDir = expandTilde(cfg.getValueString("sync_dir"));
} else {
syncDir = cfg.getValueString("sync_dir");
}
}
// vdebug syncDir as set and calculated
log.vdebug("syncDir: ", syncDir);
// Configure logging if enabled
if (cfg.getValueBool("enable_logging")){
// Read in a user defined log directory or use the default
string logDir = cfg.getValueString("log_dir");
log.vlog("Using logfile dir: ", logDir);
log.init(logDir);
}
// Configure whether notifications are used
log.setNotifications(cfg.getValueBool("monitor") && !cfg.getValueBool("disable_notifications"));
// upgrades
if (exists(cfg.configDirName ~ "/items.db")) {
if (!cfg.getValueBool("dry_run")) {
safeRemove(cfg.configDirName ~ "/items.db");
}
log.logAndNotify("Database schema changed, resync needed");
cfg.setValueBool("resync", true);
}
if (cfg.getValueBool("resync") || cfg.getValueBool("logout")) {
if (cfg.getValueBool("resync")) log.vdebug("--resync requested");
log.vlog("Deleting the saved status ...");
if (!cfg.getValueBool("dry_run")) {
safeRemove(cfg.databaseFilePath);
safeRemove(cfg.deltaLinkFilePath);
safeRemove(cfg.uploadStateFilePath);
}
if (cfg.getValueBool("logout")) {
log.vdebug("--logout requested");
if (!cfg.getValueBool("dry_run")) {
safeRemove(cfg.refreshTokenFilePath);
}
}
}
// Display current application configuration, no application initialisation
if (cfg.getValueBool("display_config")){
string userConfigFilePath = cfg.configDirName ~ "/config";
string userSyncList = cfg.configDirName ~ "/sync_list";
// Display application version
std.stdio.write("onedrive version = ", import("version"));
// Display all of the pertinent configuration options
writeln("Config path = ", cfg.configDirName);
// Does a config file exist or are we using application defaults
if (exists(userConfigFilePath)){
writeln("Config file found in config path = true");
} else {
writeln("Config file found in config path = false");
}
// Config Options
writeln("Config option 'check_nosync' = ", cfg.getValueBool("check_nosync"));
writeln("Config option 'sync_dir' = ", syncDir);
writeln("Config option 'skip_dir' = ", cfg.getValueString("skip_dir"));
writeln("Config option 'skip_file' = ", cfg.getValueString("skip_file"));
writeln("Config option 'skip_dotfiles' = ", cfg.getValueBool("skip_dotfiles"));
writeln("Config option 'skip_symlinks' = ", cfg.getValueBool("skip_symlinks"));
writeln("Config option 'monitor_interval' = ", cfg.getValueLong("monitor_interval"));
writeln("Config option 'min_notify_changes' = ", cfg.getValueLong("min_notify_changes"));
writeln("Config option 'log_dir' = ", cfg.getValueString("log_dir"));
// Is config option drive_id configured?
if (cfg.getValueString("drive_id") != ""){
writeln("Config option 'drive_id' = ", cfg.getValueString("drive_id"));
}
// Is sync_list configured?
if (exists(userSyncList)){
writeln("Config option 'sync_root_files' = ", cfg.getValueBool("sync_root_files"));
writeln("Selective sync configured = true");
writeln("sync_list contents:");
// Output the sync_list contents
auto syncListFile = File(userSyncList);
auto range = syncListFile.byLine();
foreach (line; range)
{
writeln(line);
}
} else {
writeln("Config option 'sync_root_files' = ", cfg.getValueBool("sync_root_files"));
writeln("Selective sync configured = false");
}
// exit
return EXIT_SUCCESS;
}
if (cfg.getValueBool("force_http_11")) {
log.log("NOTE: The use of --force-http-1.1 is depreciated");
}
log.vlog("Initializing the OneDrive API ...");
try {
online = testNetwork();
} catch (CurlException e) {
// No network connection to OneDrive Service
log.error("Cannot connect to Microsoft OneDrive Service");
log.error("Reason: ", e.msg);
if (!cfg.getValueBool("monitor")) {
return EXIT_FAILURE;
}
}
// Initialize OneDrive, check for authorization
oneDrive = new OneDriveApi(cfg);
oneDrive.printAccessToken = cfg.getValueBool("print_token");
if (!oneDrive.init()) {
log.error("Could not initialize the OneDrive API");
// workaround for segfault in std.net.curl.Curl.shutdown() on exit
oneDrive.http.shutdown();
return EXIT_UNAUTHORIZED;
}
// if --synchronize or --monitor not passed in, exit & display help
auto performSyncOK = false;
if (cfg.getValueBool("synchronize") || cfg.getValueBool("monitor")) {
performSyncOK = true;
}
// create-directory, remove-directory, source-directory, destination-directory
// are activities that dont perform a sync no error message for these items either
if (((cfg.getValueString("create_directory") != "") || (cfg.getValueString("remove_directory") != "")) || ((cfg.getValueString("source_directory") != "") && (cfg.getValueString("destination_directory") != "")) || (cfg.getValueString("get_file_link") != "") || (cfg.getValueString("get_o365_drive_id") != "") || cfg.getValueBool("display_sync_status")) {
performSyncOK = true;
}
if (!performSyncOK) {
writeln("\n--synchronize or --monitor missing from your command options or use --help for further assistance\n");
writeln("No OneDrive sync will be performed without either of these two arguments being present\n");
oneDrive.http.shutdown();
return EXIT_FAILURE;
}
// if --synchronize && --monitor passed in, exit & display help as these conflict with each other
if (cfg.getValueBool("synchronize") && cfg.getValueBool("monitor")) {
writeln("\nERROR: --synchronize and --monitor cannot be used together\n");
writeln("Refer to --help to determine which command option you should use.\n");
oneDrive.http.shutdown();
return EXIT_FAILURE;
}
// Initialize the item database
log.vlog("Opening the item database ...");
if (!cfg.getValueBool("dry_run")) {
// Load the items.sqlite3 file as the database
log.vdebug("Using database file: ", asNormalizedPath(cfg.databaseFilePath));
itemDb = new ItemDatabase(cfg.databaseFilePath);
} else {
// Load the items-dryrun.sqlite3 file as the database
log.vdebug("Using database file: ", asNormalizedPath(cfg.databaseFilePathDryRun));
itemDb = new ItemDatabase(cfg.databaseFilePathDryRun);
}
log.vlog("All operations will be performed in: ", syncDir);
if (!exists(syncDir)) {
log.vdebug("syncDir: Configured syncDir is missing. Creating: ", syncDir);
try {
// Attempt to create the sync dir we have been configured with
mkdirRecurse(syncDir);
} catch (std.file.FileException e) {
// Creating the sync directory failed
log.error("ERROR: Unable to create local OneDrive syncDir - ", e.msg);
oneDrive.http.shutdown();
return EXIT_FAILURE;
}
}
chdir(syncDir);
// Configure selective sync by parsing and getting a regex for skip_file config component
auto selectiveSync = new SelectiveSync();
if (exists(cfg.syncListFilePath)){
log.vdebug("Loading user configured sync_list file ...");
syncListConfigured = true;
// list what will be synced
auto syncListFile = File(cfg.syncListFilePath);
auto range = syncListFile.byLine();
foreach (line; range)
{
log.vdebug("sync_list: ", line);
}
}
selectiveSync.load(cfg.syncListFilePath);
// Configure skip_dir & skip_file from config entries
log.vdebug("Configuring skip_dir ...");
log.vdebug("skip_dir: ", cfg.getValueString("skip_dir"));
selectiveSync.setDirMask(cfg.getValueString("skip_dir"));
log.vdebug("Configuring skip_file ...");
// Validate skip_file to ensure that this does not contain an invalid configuration
// Do not use a skip_file entry of .* as this will prevent correct searching of local changes to process.
foreach(entry; cfg.getValueString("skip_file").split("|")){
if (entry == ".*") {
// invalid entry element detected
log.logAndNotify("ERROR: Invalid skip_file entry '.*' detected");
return EXIT_FAILURE;
}
}
// valid entry
log.vdebug("skip_file: ", cfg.getValueString("skip_file"));
selectiveSync.setFileMask(cfg.getValueString("skip_file"));
// Initialize the sync engine
auto sync = new SyncEngine(cfg, oneDrive, itemDb, selectiveSync);
try {
if (!initSyncEngine(sync)) {
oneDrive.http.shutdown();
return EXIT_FAILURE;
} else {
if (cfg.getValueString("get_file_link") == "") {
// Print out that we are initializing the engine only if we are not grabbing the file link
log.logAndNotify("Initializing the Synchronization Engine ...");
}
}
} catch (CurlException e) {
if (!cfg.getValueBool("monitor")) {
log.log("\nNo Internet connection.");
oneDrive.http.shutdown();
return EXIT_FAILURE;
}
}
// We should only set noRemoteDelete in an upload-only scenario
if ((cfg.getValueBool("upload_only"))&&(cfg.getValueBool("no_remote_delete"))) sync.setNoRemoteDelete();
// Do we configure to disable the upload validation routine
if (cfg.getValueBool("disable_upload_validation")) sync.setDisableUploadValidation();
// Do we need to validate the syncDir to check for the presence of a '.nosync' file
if (cfg.getValueBool("check_nomount")) {
// we were asked to check the mounts
if (exists(syncDir ~ "/.nosync")) {
log.logAndNotify("ERROR: .nosync file found. Aborting synchronization process to safeguard data.");
oneDrive.http.shutdown();
return EXIT_FAILURE;
}
}
// Do we need to create or remove a directory?
if ((cfg.getValueString("create_directory") != "") || (cfg.getValueString("remove_directory") != "")) {
if (cfg.getValueString("create_directory") != "") {
// create a directory on OneDrive
sync.createDirectoryNoSync(cfg.getValueString("create_directory"));
}
if (cfg.getValueString("remove_directory") != "") {
// remove a directory on OneDrive
sync.deleteDirectoryNoSync(cfg.getValueString("remove_directory"));
}
}
// Are we renaming or moving a directory?
if ((cfg.getValueString("source_directory") != "") && (cfg.getValueString("destination_directory") != "")) {
// We are renaming or moving a directory
sync.renameDirectoryNoSync(cfg.getValueString("source_directory"), cfg.getValueString("destination_directory"));
}
// Are we obtaining the Office 365 Drive ID for a given Office 365 SharePoint Shared Library?
if (cfg.getValueString("get_o365_drive_id") != "") {
sync.querySiteCollectionForDriveID(cfg.getValueString("get_o365_drive_id"));
}
// Are we obtaining the URL path for a synced file?
if (cfg.getValueString("get_file_link") != "") {
sync.queryOneDriveForFileURL(cfg.getValueString("get_file_link"), syncDir);
}
// Are we displaying the sync status of the client?
if (cfg.getValueBool("display_sync_status")) {
string remotePath = "/";
string localPath = ".";
// Are we doing a single directory check?
if (cfg.getValueString("single_directory") != ""){
// Need two different path strings here
remotePath = cfg.getValueString("single_directory");
localPath = cfg.getValueString("single_directory");
}
sync.queryDriveForChanges(remotePath);
}
// Are we performing a sync, resync or monitor operation?
if ((cfg.getValueBool("synchronize")) || (cfg.getValueBool("resync")) || (cfg.getValueBool("monitor"))) {
if ((cfg.getValueBool("synchronize")) || (cfg.getValueBool("resync"))) {
if (online) {
// Check user entry for local path - the above chdir means we are already in ~/OneDrive/ thus singleDirectory is local to this path
if (cfg.getValueString("single_directory") != ""){
// Does the directory we want to sync actually exist?
if (!exists(cfg.getValueString("single_directory"))){
// the requested directory does not exist ..
log.logAndNotify("ERROR: The requested local directory does not exist. Please check ~/OneDrive/ for requested path");
oneDrive.http.shutdown();
return EXIT_FAILURE;
}
}
// perform a --synchronize sync
performSync(sync, cfg.getValueString("single_directory"), cfg.getValueBool("download_only"), cfg.getValueBool("local_first"), cfg.getValueBool("upload_only"), LOG_NORMAL, true, syncListConfigured);
}
}
if (cfg.getValueBool("monitor")) {
log.logAndNotify("Initializing monitor ...");
log.log("OneDrive monitor interval (seconds): ", cfg.getValueLong("monitor_interval"));
Monitor m = new Monitor(selectiveSync);
m.onDirCreated = delegate(string path) {
log.vlog("[M] Directory created: ", path);
try {
sync.scanForDifferences(path);
} catch (CurlException e) {
log.vlog("Offline, cannot create remote dir!");
} catch(Exception e) {
log.logAndNotify("Cannot create remote directory: ", e.msg);
}
};
m.onFileChanged = delegate(string path) {
log.vlog("[M] File changed: ", path);
try {
sync.scanForDifferences(path);
} catch (CurlException e) {
log.vlog("Offline, cannot upload changed item!");
} catch(Exception e) {
log.logAndNotify("Cannot upload file changes/creation: ", e.msg);
}
};
m.onDelete = delegate(string path) {
log.vlog("[M] Item deleted: ", path);
try {
sync.deleteByPath(path);
} catch (CurlException e) {
log.vlog("Offline, cannot delete item!");
} catch(SyncException e) {
if (e.msg == "The item to delete is not in the local database") {
log.vlog("Item cannot be deleted because not found in database");
} else {
log.logAndNotify("Cannot delete remote item: ", e.msg);
}
} catch(Exception e) {
log.logAndNotify("Cannot delete remote item: ", e.msg);
}
};
m.onMove = delegate(string from, string to) {
log.vlog("[M] Item moved: ", from, " -> ", to);
try {
sync.uploadMoveItem(from, to);
} catch (CurlException e) {
log.vlog("Offline, cannot move item!");
} catch(Exception e) {
log.logAndNotify("Cannot move item:, ", e.msg);
}
};
signal(SIGINT, &exitHandler);
signal(SIGTERM, &exitHandler);
// attempt to initialise monitor class
try {
m.init(cfg, cfg.getValueLong("verbose") > 0, cfg.getValueBool("skip_symlinks"), cfg.getValueBool("check_nosync"));
} catch (MonitorException e) {
// monitor initialisation failed
log.error("ERROR: ", e.msg);
exit(-1);
}
if (!cfg.getValueBool("download_only")) {
// monitor loop
immutable auto checkInterval = dur!"seconds"(cfg.getValueLong("monitor_interval"));
immutable auto logInterval = cfg.getValueLong("monitor_log_frequency");
immutable auto fullScanFrequency = cfg.getValueLong("monitor_fullscan_frequency");
auto lastCheckTime = MonoTime.currTime();
auto logMonitorCounter = 0;
auto fullScanCounter = 0;
bool fullScanRequired = true;
bool syncListConfiguredOverride = false;
while (true) {
if (!cfg.getValueBool("download_only")) m.update(online);
auto currTime = MonoTime.currTime();
if (currTime - lastCheckTime > checkInterval) {
// log monitor output suppression
logMonitorCounter += 1;
if (logMonitorCounter > logInterval)
logMonitorCounter = 1;
// full scan of sync_dir
fullScanCounter += 1;
if (fullScanCounter > fullScanFrequency){
fullScanCounter = 1;
fullScanRequired = true;
if (syncListConfigured) {
syncListConfiguredOverride = true;
}
}
// log.logAndNotify("DEBUG trying to create checkpoint");
// auto res = itemdb.db_checkpoint();
// log.logAndNotify("Checkpoint return: ", res);
// itemdb.dump_open_statements();
try {
if (!initSyncEngine(sync)) {
oneDrive.http.shutdown();
return EXIT_FAILURE;
}
try {
// perform a --monitor sync
performSync(sync, cfg.getValueString("single_directory"), cfg.getValueBool("download_only"), cfg.getValueBool("local_first"), cfg.getValueBool("upload_only"), (logMonitorCounter == logInterval ? MONITOR_LOG_QUIET : MONITOR_LOG_SILENT), fullScanRequired, syncListConfiguredOverride);
if (!cfg.getValueBool("download_only")) {
// discard all events that may have been generated by the sync
m.update(false);
}
} catch (CurlException e) {
// we already tried three times in the performSync routine
// if we still have problems, then the sync handle might have
// gone stale and we need to re-initialize the sync engine
log.log("Persistent connection errors, reinitializing connection");
sync.reset();
}
} catch (CurlException e) {
log.log("Cannot initialize connection to OneDrive");
}
// performSync complete, set lastCheckTime to current time
fullScanRequired = false;
if (syncListConfigured) {
syncListConfiguredOverride = false;
}
lastCheckTime = MonoTime.currTime();
GC.collect();
}
Thread.sleep(dur!"msecs"(500));
}
}
}
}
// Workaround for segfault in std.net.curl.Curl.shutdown() on exit
oneDrive.http.shutdown();
// Make sure the .wal file is incorporated into the main db before we exit
destroy(itemDb);
// --dry-run temp database cleanup
if (cfg.getValueBool("dry_run")) {
if (exists(cfg.databaseFilePathDryRun)) {
// remove the file
log.vdebug("Removing items-dryrun.sqlite3 as dry run operations complete");
safeRemove(cfg.databaseFilePathDryRun);
}
}
return EXIT_SUCCESS;
}
bool initSyncEngine(SyncEngine sync)
{
try {
sync.init();
} catch (OneDriveException e) {
if (e.httpStatusCode == 400 || e.httpStatusCode == 401) {
// Authorization is invalid
log.log("\nAuthorization token invalid, use --logout to authorize the client again\n");
return false;
}
if (e.httpStatusCode >= 500) {
// There was a HTTP 5xx Server Side Error, message already printed
return false;
}
}
return true;
}
// try to synchronize the folder three times
void performSync(SyncEngine sync, string singleDirectory, bool downloadOnly, bool localFirst, bool uploadOnly, long logLevel, bool fullScanRequired, bool syncListConfigured)
{
int count;
string remotePath = "/";
string localPath = ".";
// Are we doing a single directory sync?
if (singleDirectory != ""){
// Need two different path strings here
remotePath = singleDirectory;
localPath = singleDirectory;
// Set flag for singleDirectoryScope for change handling
sync.setSingleDirectoryScope();
}
// Due to Microsoft Sharepoint 'enrichment' of files, we try to download the Microsoft modified file automatically
// Set flag if we are in upload only state to handle this differently
// See: https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details
if (uploadOnly) sync.setUploadOnly();
do {
try {
if (singleDirectory != ""){
// we were requested to sync a single directory
log.vlog("Syncing changes from this selected path: ", singleDirectory);
if (uploadOnly){
// Upload Only of selected single directory
if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from selected local path only - NOT syncing data changes from OneDrive ...");
sync.scanForDifferences(localPath);
} else {
// No upload only
if (localFirst) {
// Local First
if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from selected local path first before downloading changes from OneDrive ...");
sync.scanForDifferences(localPath);
sync.applyDifferencesSingleDirectory(remotePath);
} else {
// OneDrive First
if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from selected OneDrive path ...");
sync.applyDifferencesSingleDirectory(remotePath);
// is this a download only request?
if (!downloadOnly) {
// process local changes
sync.scanForDifferences(localPath);
// ensure that the current remote state is updated locally
sync.applyDifferencesSingleDirectory(remotePath);
}
}
}
} else {
// no single directory sync
if (uploadOnly){
// Upload Only of entire sync_dir
if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from local path only - NOT syncing data changes from OneDrive ...");
sync.scanForDifferences(localPath);
} else {
// No upload only
if (localFirst) {
// sync local files first before downloading from OneDrive
if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from local path first before downloading changes from OneDrive ...");
sync.scanForDifferences(localPath);
sync.applyDifferences(syncListConfigured);
} else {
// sync from OneDrive first before uploading files to OneDrive
if (logLevel < MONITOR_LOG_SILENT) log.log("Syncing changes from OneDrive ...");
// if sync_list is configured, syncListConfigured = true, thus a FULL walk of all OneDrive objects will be performed
sync.applyDifferences(syncListConfigured);
// Is a full scan of the entire sync_dir required?
if (fullScanRequired) {
// is this a download only request?
if (!downloadOnly) {
// process local changes walking the entire path checking for changes
// in monitor mode all local changes are captured via inotify
// thus scanning every 'monitor_interval' (default 45 seconds) for local changes is excessive and not required
sync.scanForDifferences(localPath);
// ensure that the current remote state is updated locally
// for the 'true-up' set this to false so that we use the delta link for second operation
sync.applyDifferences(false);
}
}
}
}
}
count = -1;
} catch (Exception e) {
if (++count == 3) {
log.log("Giving up on sync after three attempts: ", e.msg);
throw e;
} else
log.log("Retry sync count: ", count, ": ", e.msg);
}
} while (count != -1);
}
// getting around the @nogc problem
// https://p0nce.github.io/d-idioms/#Bypassing-@nogc
auto assumeNoGC(T) (T t) if (isFunctionPointer!T || isDelegate!T)
{
enum attrs = functionAttributes!T | FunctionAttribute.nogc;
return cast(SetFunctionAttributes!(T, functionLinkage!T, attrs)) t;
}
extern(C) nothrow @nogc @system void exitHandler(int value) {
try {
assumeNoGC ( () {
log.log("Got termination signal, shutting down db connection");
// make sure the .wal file is incorporated into the main db
destroy(itemDb);
// workaround for segfault in std.net.curl.Curl.shutdown() on exit
oneDrive.http.shutdown();
})();
} catch(Exception e) {}
exit(0);
}