2017-03-12 16:07:45 +01:00
import std.file , std . string , std . regex , std . stdio ;
2017-03-24 22:30:03 +01:00
import selective ;
2016-08-04 23:35:58 +02:00
static import log ;
2015-09-01 20:45:34 +02:00
2016-08-04 23:35:58 +02:00
final class Config
2015-09-01 20:45:34 +02:00
{
2016-08-04 23:35:58 +02:00
public string refreshTokenFilePath ;
2017-05-28 20:54:57 +02:00
public string deltaLinkFilePath ;
2016-08-04 23:35:58 +02:00
public string databaseFilePath ;
public string uploadStateFilePath ;
2017-03-12 16:07:45 +01:00
public string syncListFilePath ;
2019-01-26 01:03:00 +01:00
public string homePath ;
2016-08-04 23:35:58 +02:00
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 ;
2016-08-04 23:35:58 +02:00
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 ) ;
2016-08-04 23:35:58 +02:00
refreshTokenFilePath = configDirName ~ "/refresh_token" ;
2017-05-28 20:54:57 +02:00
deltaLinkFilePath = configDirName ~ "/delta_link" ;
2016-12-25 19:23:33 +01:00
databaseFilePath = configDirName ~ "/items.sqlite3" ;
2016-08-04 23:35:58 +02:00
uploadStateFilePath = configDirName ~ "/resume_upload" ;
userConfigFilePath = configDirName ~ "/config" ;
2017-03-12 16:07:45 +01:00
syncListFilePath = configDirName ~ "/sync_list" ;
2016-08-04 23:35:58 +02:00
}
2018-12-20 00:51:21 +01:00
bool init ( )
2015-09-01 20:45:34 +02:00
{
2018-09-13 00:46:27 +02:00
// Default configuration directory
2017-03-12 16:07:45 +01:00
setValue ( "sync_dir" , "~/OneDrive" ) ;
2018-04-16 13:39:45 +02:00
// Configure to skip ONLY temp files (~*.doc etc) by default
// Prior configuration was: .*|~*
setValue ( "skip_file" , "~*" ) ;
2018-08-02 22:02:10 +02:00
// By default symlinks are not skipped (using string type
// instead of boolean because hashmap only stores string types)
setValue ( "skip_symlinks" , "false" ) ;
2018-08-07 21:35:18 +02:00
// 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" ) ;
2018-11-23 21:13:16 +01:00
// Configure the default logging directory to be /var/log/onedrive/
setValue ( "log_dir" , "/var/log/onedrive/" ) ;
2018-12-20 00:51:21 +01:00
// Configure a default empty value for drive_id
setValue ( "drive_id" , "" ) ;
2019-01-28 18:54:03 +01:00
// Minimal changes that trigger a log and notification on sync
setValue ( "min_notif_changes" , "5" ) ;
2018-11-23 21:13:16 +01:00
2017-03-12 16:07:45 +01:00
if ( ! load ( userConfigFilePath ) ) {
2018-12-20 00:51:21 +01:00
// 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 ;
}
2017-03-12 16:07:45 +01:00
}
2018-12-20 00:51:21 +01:00
return true ;
2015-09-01 20:45:34 +02:00
}
2016-08-04 23:35:58 +02:00
string getValue ( string key )
2015-09-01 20:45:34 +02:00
{
2015-09-24 19:12:19 +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 ;
}
}
2017-03-12 16:07:45 +01:00
void setValue ( string key , string value )
{
values [ key ] = value ;
}
2016-08-04 23:35:58 +02:00
private bool load ( string filename )
2015-09-01 20:45:34 +02:00
{
2016-08-04 23:35:58 +02:00
scope ( failure ) return false ;
2015-09-24 18:59:17 +02:00
auto file = File ( filename , "r" ) ;
2017-03-12 16:07:45 +01:00
auto r = regex ( `^(\w+)\s*=\s*"(.*)"\s*$` ) ;
2015-09-24 18:59:17 +02:00
foreach ( line ; file . byLine ( ) ) {
2017-03-12 16:07:45 +01:00
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 ;
2018-12-20 00:51:21 +01:00
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 {
2016-08-04 23:35:58 +02:00
log . log ( "Malformed config line: " , line ) ;
2018-12-20 00:51:21 +01:00
return false ;
2015-09-22 14:48:18 +02:00
}
2015-09-01 20:45:34 +02:00
}
2016-08-04 23:35:58 +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
{
2016-08-04 23:35:58 +02:00
auto cfg = new Config ( "" ) ;
2017-03-12 16:07:45 +01:00
cfg . load ( "config" ) ;
2016-08-04 23:35:58 +02:00
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
}