abraunegg-onedrive/src/config.d

441 lines
16 KiB
D
Raw Normal View History

import std.file, std.string, std.regex, std.stdio;
import selective;
static import log;
2015-09-01 20:45:34 +02:00
final class Config
2015-09-01 20:45:34 +02:00
{
public string refreshTokenFilePath;
public string deltaLinkFilePath;
public string databaseFilePath;
public string uploadStateFilePath;
public string syncListFilePath;
2019-01-26 01:03:00 +01:00
public string homePath;
private string userConfigFilePath;
// hashmap for the values found in the user config file
2015-09-01 20:45:34 +02:00
private string[string] values;
this(string configDirName)
{
2019-01-26 01:03:00 +01:00
// Determine the users home directory.
// Need to avoid using ~ here as expandTilde() below does not interpret correctly when running under init.d or systemd scripts
// Check for HOME environment variable
if (environment.get("HOME") != ""){
// Use HOME environment variable
log.vdebug("homePath: HOME environment variable set");
homePath = environment.get("HOME");
} else {
if ((environment.get("SHELL") == "") && (environment.get("USER") == "")){
// No shell is set or username - observed case when running as systemd service under CentOS 7.x
log.vdebug("homePath: WARNING - no HOME environment variable set");
log.vdebug("homePath: WARNING - no SHELL environment variable set");
log.vdebug("homePath: WARNING - no USER environment variable set");
homePath = "/root";
} else {
// A shell & valid user is set, but no HOME is set, use ~ which can be expanded
log.vdebug("homePath: WARNING - no HOME environment variable set");
homePath = "~";
}
}
// Output homePath calculation
log.vdebug("homePath: ", homePath);
// Determine the correct configuration directory to use
if (configDirName != "") {
// A CLI 'confdir' was passed in
log.vdebug("configDirName: CLI override to set configDirName to: ", configDirName);
if (canFind(configDirName,"~")) {
// A ~ was found
log.vdebug("configDirName: A '~' was found in configDirName, using the calculated 'homePath' to replace '~'");
configDirName = homePath ~ strip(configDirName,"~","~");
}
} else {
// Determine the base directory relative to which user specific configuration files should be stored.
if (environment.get("XDG_CONFIG_HOME") != ""){
log.vdebug("configDirBase: XDG_CONFIG_HOME environment variable set");
configDirBase = environment.get("XDG_CONFIG_HOME");
} else {
// XDG_CONFIG_HOME does not exist on systems where X11 is not present - ie - headless systems / servers
log.vdebug("configDirBase: WARNING - no XDG_CONFIG_HOME environment variable set");
configDirBase = homePath ~ "/.config";
}
// Output configDirBase calculation
log.vdebug("configDirBase: ", configDirBase);
// Set the default application configuration directory
log.vdebug("configDirName: Configuring application to use default config path");
// configDirBase contains the correct path so we do not need to check for presence of '~'
configDirName = configDirBase ~ "/onedrive";
}
log.vlog("Using Config Dir: ", configDirName);
if (!exists(configDirName)) mkdirRecurse(configDirName);
refreshTokenFilePath = configDirName ~ "/refresh_token";
deltaLinkFilePath = configDirName ~ "/delta_link";
2016-12-25 19:23:33 +01:00
databaseFilePath = configDirName ~ "/items.sqlite3";
uploadStateFilePath = configDirName ~ "/resume_upload";
userConfigFilePath = configDirName ~ "/config";
syncListFilePath = configDirName ~ "/sync_list";
}
bool init()
2015-09-01 20:45:34 +02:00
{
// Default configuration directory
setValue("sync_dir", "~/OneDrive");
// Configure to skip ONLY temp files (~*.doc etc) by default
// Prior configuration was: .*|~*
setValue("skip_file", "~*");
// By default symlinks are not skipped (using string type
// instead of boolean because hashmap only stores string types)
setValue("skip_symlinks", "false");
// Configure the monitor mode loop - the number of seconds by which
// each sync operation is undertaken when idle under monitor mode
setValue("monitor_interval", "45");
// Configure the default logging directory to be /var/log/onedrive/
setValue("log_dir", "/var/log/onedrive/");
// Configure a default empty value for drive_id
setValue("drive_id", "");
// Minimal changes that trigger a log and notification on sync
setValue("min_notif_changes", "5");
if (!load(userConfigFilePath)) {
// What was the reason for failure?
if (!exists(userConfigFilePath)) {
log.vlog("No config file found, using application defaults");
return true;
} else {
log.log("Configuration file has errors - please check your configuration");
return false;
}
}
return true;
2015-09-01 20:45:34 +02:00
}
string getValue(string key)
2015-09-01 20:45:34 +02:00
{
auto p = key in values;
if (p) {
return *p;
} else {
2015-09-14 19:21:06 +02:00
throw new Exception("Missing config value: " ~ key);
}
2015-09-01 20:45:34 +02:00
}
2016-09-18 11:37:41 +02:00
string getValue(string key, string value)
{
auto p = key in values;
if (p) {
return *p;
} else {
return value;
}
}
void setValue(string key, string value)
{
values[key] = value;
}
private bool load(string filename)
2015-09-01 20:45:34 +02:00
{
scope(failure) return false;
2015-09-24 18:59:17 +02:00
auto file = File(filename, "r");
auto r = regex(`^(\w+)\s*=\s*"(.*)"\s*$`);
2015-09-24 18:59:17 +02:00
foreach (line; file.byLine()) {
line = stripLeft(line);
if (line.length == 0 || line[0] == ';' || line[0] == '#') continue;
2015-09-24 18:59:17 +02:00
auto c = line.matchFirst(r);
if (!c.empty) {
c.popFront(); // skip the whole match
string key = c.front.dup;
auto p = key in values;
if (p) {
c.popFront();
setValue(key, c.front.dup);
} else {
log.log("Unknown key in config file: ", key);
return false;
}
2015-09-24 18:59:17 +02:00
} else {
log.log("Malformed config line: ", line);
return false;
2015-09-22 14:48:18 +02:00
}
2015-09-01 20:45:34 +02:00
}
return true;
2015-09-01 20:45:34 +02:00
}
2019-01-26 01:03:00 +01:00
bool updateConfigFromCmdLine(string[] args) {
auto cfg2settingBool = [
"upload_only": &uploadOnly,
"check_for_nomount": &checkMount,
"download_only": &downloadOnly,
"disable_notifications": &disableNotifications,
"disable_upload_validation": &disableUploadValidation,
"enable_logging": &enableLogFile,
"force_http_11": &forceHTTP11,
"local_first": &localFirst,
"no_remote_delete": &noRemoteDelete,
"skip_symlinks": &skipSymlinks,
"verbose": &verbose
];
auto cfg2settingString = [
"single_directory": &singleDirectory,
"syncdir": &syncDirName
];
void boolHandler(string option) {
switch (option) {
case "upload_only":
case "check_for_nomount":
case "download_only":
case "disable_notifications":
case "disable_upload_validation":
case "enable_logging":
case "force_http_11":
case "local_first":
case "no_remote_delete":
case "skip_symlinks":
case "verbose":
setValue(option, "true");
case "quiet": verbosityLevel = 0; break;
case "verbose": verbosityLevel = 2; break;
case "shouting": verbosityLevel = verbosityLevel.max; break;
default:
stderr.writeln("Unknown verbosity level ", value);
handlerFailed = true;
break;
}
}
// Application Option Variables
// Add a check mounts option to resolve https://github.com/abraunegg/onedrive/issues/8
bool checkMount = false;
// Create a single root directory on OneDrive
string createDirectory;
// The destination directory if we are using the OneDrive client to rename a directory
string destinationDirectory;
// Debug the HTTPS submit operations if required
bool debugHttp = false;
// Do not use notifications in monitor mode
bool disableNotifications = false;
// Display application configuration but do not sync
bool displayConfiguration = false;
// Display sync status
bool displaySyncStatus = false;
// only download remote changes
bool downloadOnly = false;
// Does the user want to disable upload validation - https://github.com/abraunegg/onedrive/issues/205
// SharePoint will associate some metadata from the library the file is uploaded to directly in the file - thus change file size & checksums
bool disableUploadValidation = false;
// Do we enable a log file
bool enableLogFile = false;
// Force the use of HTTP 1.1 to overcome curl => 7.62.0 where some operations are now sent via HTTP/2
// Whilst HTTP/2 operations are handled, in some cases the handling of this outside of the client is not being done correctly (router, other) thus the client breaks
// This flag then allows the user to downgrade all HTTP operations to HTTP 1.1 for maximum network path compatibility
bool forceHTTP11 = false;
// SharePoint / Office 365 Shared Library name to query
string o365SharedLibraryName;
// Local sync - Upload local changes first before downloading changes from OneDrive
bool localFirst = false;
// remove the current user and sync state
bool logout = false;
// enable monitor mode
bool monitor = false;
// Add option for no remote delete
bool noRemoteDelete = false;
// print the access token
bool printAccessToken = false;
// force a full resync
bool resync = false;
// Remove a single directory on OneDrive
string removeDirectory;
// This allows for selective directory syncing instead of everything under ~/OneDrive/
string singleDirectory;
// Add option to skip symlinks
bool skipSymlinks = false;
// The source directory if we are using the OneDrive client to rename a directory
string sourceDirectory;
// override the sync directory
string syncDirName;
// Configure a flag to perform a sync
// This is beneficial so that if just running the client itself - without any options, or sync check, the client does not perform a sync
bool synchronize = false;
// Upload Only
bool uploadOnly = false;
// enable verbose logging
bool verbose = false;
//
// IDEA TODO TODO
// first run of getopt that leaves options (passThrough etc) only for the conffile
// then load config files
// then second getopt run with all other options overwriting config files options
// Application Startup option validation
try {
auto opt = getopt(
args,
std.getopt.config.bundling,
std.getopt.config.caseSensitive,
"check-for-nomount", "Check for the presence of .nosync in the syncdir root. If found, do not perform sync.", &checkMount,
"confdir", "Set the directory used to store the configuration files", &configDirName,
"create-directory", "Create a directory on OneDrive - no sync will be performed.", &createDirectory,
"destination-directory", "Destination directory for renamed or move on OneDrive - no sync will be performed.", &destinationDirectory,
"debug-https", "Debug OneDrive HTTPS communication.", &debugHttp,
"disable-notifications", "Do not use desktop notifications in monitor mode.", &disableNotifications,
"display-config", "Display what options the client will use as currently configured - no sync will be performed.", &displayConfiguration,
"display-sync-status", "Display the sync status of the client - no sync will be performed.", &displaySyncStatus,
"download-only|d", "Only download remote changes", &downloadOnly,
"disable-upload-validation", "Disable upload validation when uploading to OneDrive", &disableUploadValidation,
"enable-logging", "Enable client activity to a separate log file", &enableLogFile,
"force-http-1.1", "Force the use of HTTP 1.1 for all operations", &forceHTTP11,
"get-O365-drive-id", "Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library", &o365SharedLibraryName,
"local-first", "Synchronize from the local directory source first, before downloading changes from OneDrive.", &localFirst,
"logout", "Logout the current user", &logout,
"monitor|m", "Keep monitoring for local and remote changes", &monitor,
"no-remote-delete", "Do not delete local file 'deletes' from OneDrive when using --upload-only", &noRemoteDelete,
"print-token", "Print the access token, useful for debugging", &printAccessToken,
"resync", "Forget the last saved state, perform a full sync", &resync,
"remove-directory", "Remove a directory on OneDrive - no sync will be performed.", &removeDirectory,
"single-directory", "Specify a single local directory within the OneDrive root to sync.", &singleDirectory,
"skip-symlinks", "Skip syncing of symlinks", &skipSymlinks,
"source-directory", "Source directory to rename or move on OneDrive - no sync will be performed.", &sourceDirectory,
"syncdir", "Specify the local directory used for synchronization to OneDrive", &syncDirName,
"synchronize", "Perform a synchronization", &synchronize,
"upload-only", "Only upload to OneDrive, do not sync changes from OneDrive locally", &uploadOnly,
"verbose|v+", "Print more details, useful for debugging (repeat for extra debugging)", &log.verbose,
);
if (opt.helpWanted) {
outputLongHelp(opt.options);
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;
}
// Main function variables
string homePath = "";
string configDirBase = "";
// Debug the HTTPS response operations if required
bool debugHttpSubmit;
// Are we able to reach the OneDrive Service
bool online = false;
auto cfg = new config.Config(configDirName);
if(!cfg.init()){
// There was an error loading the configuration
// Error message already printed
return EXIT_FAILURE;
}
config.updateConfigFromCmdLine(args);
foreach (cfgKey, p; cfg2settingBool) {
if (*p) {
// the user passed in an alternate setting via cmd line
log.vdebug("CLI override to set", cfgKey, "to true");
cfg.setValue(cfgKey, "true");
}
}
foreach (cfgKey, p; cfg2settingString) {
if (*p) {
// the user passed in an alternate setting via cmd line
log.vdebug("CLI override to set", cfgKey, "to", *p);
cfg.setValue(cfgKey, *p);
}
}
// command line parameters to override default 'config' & take precedence
// Set the client to skip symbolic links if --skip-symlinks was passed in
if (skipSymlinks) {
// The user passed in an alternate skip_symlinks as to what was either in 'config' file or application default
log.vdebug("CLI override to set skip_symlinks to: true");
cfg.setValue("skip_symlinks", "true");
}
// Set the OneDrive Local Sync Directory if was passed in via --syncdir
if (syncDirName) {
// The user passed in an alternate sync_dir as to what was either in 'config' file or application default
// Do not expandTilde here as we do not know if we reliably can
log.vdebug("CLI override to set sync_dir to: ", syncDirName);
cfg.setValue("sync_dir", syncDirName);
}
// 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.getValue("sync_dir"),"~")) {
// A ~ was found
log.vdebug("sync_dir: A '~' was found in sync_dir, using the calculated 'homePath' to replace '~'");
syncDir = homePath ~ strip(cfg.getValue("sync_dir"),"~","~");
} else {
// No ~ found in sync_dir, use as is
log.vdebug("sync_dir: Getting syncDir from config value sync_dir");
syncDir = cfg.getValue("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.getValue("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.getValue("sync_dir"));
} else {
syncDir = cfg.getValue("sync_dir");
}
}
// vdebug syncDir as set and calculated
log.vdebug("syncDir: ", syncDir);
}
2015-09-01 20:45:34 +02:00
}
unittest
{
auto cfg = new Config("");
cfg.load("config");
assert(cfg.getValue("sync_dir") == "~/OneDrive");
2016-09-18 11:37:41 +02:00
assert(cfg.getValue("empty", "default") == "default");
2015-09-24 18:59:17 +02:00
}