Merge 'master' to PR

* Merge 'master' to PR
This commit is contained in:
abraunegg 2020-12-01 16:23:20 +11:00
commit 50137b5f3b
26 changed files with 1102 additions and 294 deletions

View file

@ -15,10 +15,13 @@ A clear and concise description of what the bug is.
* OneDrive Account Type
* Did you build from source or install from a package?
* If you installed from source, what is your DMD or LDC compiler version: `dmd --version` or `ldmd2 --version`
* OneDrive Application Version: Output of `onedrive --version`
* OneDrive Application Configuration: Output of `onedrive --display-config`
* Provide the version of curl you are using: Output of `curl --version`
* Is your configured 'sync_dir' a local directory or a network mount point?
* Provide all the mountpoints in your system: Output of: `mount`
* If *not* local, provide all the mountpoints in your system: Output of: `mount`
* What partition format type does your configured 'sync_dir' reside on? Output of: `lsblk -f`
* Explain your entire configuration setup - is the OneDrive folder shared with any other system, shared with any other platform at the same time, is the OneDrive account you use shared across multiple systems / platforms / Operating Systems and in use at the same time
**Note:** Please generate a full debug log whilst reproducing the issue as per [https://github.com/abraunegg/onedrive/wiki/Generate-debug-log-for-support](https://github.com/abraunegg/onedrive/wiki/Generate-debug-log-for-support) and email to support@mynas.com.au

View file

@ -1,7 +1,47 @@
# Changelog
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## 2.4.8 - 2020-11-30
### Fixed
* Fix to use config set option for 'remove_source_files' and 'skip_dir_strict_match' rather than ignore if set
* Fix download failure and crash due to incorrect local filesystem permissions when using mounted external devices
* Fix to not change permissions on pre-existing local directories
* Fix logging output when authentication authorisation fails to not say authorisation was successful
* Fix to check application_id before setting redirect URL when using specific Azure endpoints
* Fix application crash in --monitor mode due to 'Failed to stat file' when setgid is used on a directory and data cannot be read
### Added
* Added advanced-usage.md to document advaced client usage such as multi account configurations and Windows dual-boot
### Updated
* Updated --verbose logging output for config options when set
* Updated documentation (man page, USAGE.md, Office365.md, BusinessSharedFolders.md)
## 2.4.7 - 2020-11-09
### Fixed
* Fix debugging output for /delta changes available queries
* Fix logging output for modification comparison source data
* Fix Business Shared Folder handling to process only Shared Folders, not individually shared files
* Fix cleanup dryrun shm and wal files if they exist
* Fix --list-shared-folders to only show folders
* Fix to check for the presence of .nosync when processing DB entries
* Fix skip_dir matching when using --resync
* Fix uploading data to shared business folders when using --upload-only
* Fix to merge contents of SQLite WAL file into main database file on sync completion
* Fix to check if localModifiedTime is >= than item.mtime to avoid re-upload for equal modified time
* Fix to correctly set config directory permissions at first start
### Added
* Added environment variable to allow easy HTTPS debug in docker
* Added environment variable to allow download-only mode in Docker
* Implement Feature: Allow config to specify a tenant id for non-multi-tenant applications
* Implement Feature: Adding support for authentication with single tenant custom applications
* Implement Feature: Configure specific File and Folder Permissions
### Updated
* Updated documentation (readme.md, install.md, usage.md, bug_report.md)
## 2.4.6 - 2020-10-04
### Fixed
* Fix flagging of remaining free space when value is being restricted

View file

@ -55,7 +55,7 @@ endif
system_unit_files = contrib/systemd/onedrive@.service
user_unit_files = contrib/systemd/onedrive.service
DOCFILES = README.md config LICENSE CHANGELOG.md docs/Docker.md docs/INSTALL.md docs/Office365.md docs/USAGE.md docs/BusinessSharedFolders.md
DOCFILES = README.md config LICENSE CHANGELOG.md docs/Docker.md docs/INSTALL.md docs/Office365.md docs/USAGE.md docs/BusinessSharedFolders.md docs/advanced-usage.md
ifneq ("$(wildcard /etc/redhat-release)","")
RHEL = $(shell cat /etc/redhat-release | grep -E "(Red Hat Enterprise Linux Server|CentOS)" | wc -l)

2
config
View file

@ -40,3 +40,5 @@
# azure_ad_endpoint = ""
# azure_tenant_id = "common"
# sync_business_shared_folders = "false"
# sync_dir_permissions = "700"
# sync_file_permissions = "600"

20
configure vendored
View file

@ -1,6 +1,6 @@
#! /bin/sh
# Guess values for system-dependent variables and create Makefiles.
# Generated by GNU Autoconf 2.69 for onedrive v2.4.7-dev.
# Generated by GNU Autoconf 2.69 for onedrive v2.4.8.
#
# Report bugs to <https://github.com/abraunegg/onedrive>.
#
@ -579,8 +579,8 @@ MAKEFLAGS=
# Identity of this package.
PACKAGE_NAME='onedrive'
PACKAGE_TARNAME='onedrive'
PACKAGE_VERSION='v2.4.7-dev'
PACKAGE_STRING='onedrive v2.4.7-dev'
PACKAGE_VERSION='v2.4.8'
PACKAGE_STRING='onedrive v2.4.8'
PACKAGE_BUGREPORT='https://github.com/abraunegg/onedrive'
PACKAGE_URL=''
@ -1219,7 +1219,7 @@ if test "$ac_init_help" = "long"; then
# Omit some internal or obsolete options to make the list less imposing.
# This message is too long to be a string in the A/UX 3.1 sh.
cat <<_ACEOF
\`configure' configures onedrive v2.4.7-dev to adapt to many kinds of systems.
\`configure' configures onedrive v2.4.8 to adapt to many kinds of systems.
Usage: $0 [OPTION]... [VAR=VALUE]...
@ -1280,7 +1280,7 @@ fi
if test -n "$ac_init_help"; then
case $ac_init_help in
short | recursive ) echo "Configuration of onedrive v2.4.7-dev:";;
short | recursive ) echo "Configuration of onedrive v2.4.8:";;
esac
cat <<\_ACEOF
@ -1393,7 +1393,7 @@ fi
test -n "$ac_init_help" && exit $ac_status
if $ac_init_version; then
cat <<\_ACEOF
onedrive configure v2.4.7-dev
onedrive configure v2.4.8
generated by GNU Autoconf 2.69
Copyright (C) 2012 Free Software Foundation, Inc.
@ -1410,7 +1410,7 @@ cat >config.log <<_ACEOF
This file contains any messages produced by compilers while
running configure, to aid debugging if configure makes a mistake.
It was created by onedrive $as_me v2.4.7-dev, which was
It was created by onedrive $as_me v2.4.8, which was
generated by GNU Autoconf 2.69. Invocation command line was
$ $0 $@
@ -2162,7 +2162,7 @@ fi
PACKAGE_DATE="October 2020"
PACKAGE_DATE="November 2020"
@ -3159,7 +3159,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
# report actual input values of CONFIG_FILES etc. instead of their
# values after options handling.
ac_log="
This file was extended by onedrive $as_me v2.4.7-dev, which was
This file was extended by onedrive $as_me v2.4.8, which was
generated by GNU Autoconf 2.69. Invocation command line was
CONFIG_FILES = $CONFIG_FILES
@ -3212,7 +3212,7 @@ _ACEOF
cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`"
ac_cs_version="\\
onedrive config.status v2.4.7-dev
onedrive config.status v2.4.8
configured by $0, generated by GNU Autoconf 2.69,
with options \\"\$ac_cs_config\\"

View file

@ -9,7 +9,7 @@ dnl - commit the changed files (configure.ac, configure)
dnl - tag the release
AC_PREREQ([2.69])
AC_INIT([onedrive],[v2.4.7-dev], [https://github.com/abraunegg/onedrive], [onedrive])
AC_INIT([onedrive],[v2.4.8], [https://github.com/abraunegg/onedrive], [onedrive])
AC_CONFIG_SRCDIR([src/main.d])

View file

@ -40,12 +40,24 @@ if [ "${ONEDRIVE_DEBUG:=0}" == "1" ]; then
ARGS=(--verbose --verbose ${ARGS[@]})
fi
# Tell client to perform HTTPS debug output, based on an environment variable
if [ "${ONEDRIVE_DEBUG_HTTPS:=0}" == "1" ]; then
echo "# We are performing HTTPS debug output"
ARGS=(--debug-https ${ARGS[@]})
fi
# Tell client to perform a resync based on environment variable
if [ "${ONEDRIVE_RESYNC:=0}" == "1" ]; then
echo "# We are performing a --resync"
ARGS=(--resync ${ARGS[@]})
fi
# Tell client to sync in download-only mode based on environment variable
if [ "${ONEDRIVE_DOWNLOADONLY:=0}" == "1" ]; then
echo "# We are synchronizing in download-only mode"
ARGS=(--download-only ${ARGS[@]})
fi
if [ ${#} -gt 0 ]; then
ARGS=("${@}")
fi

View file

@ -6,7 +6,7 @@
%endif
Name: onedrive
Version: 2.4.6
Version: 2.4.8
Release: 1%{?dist}
Summary: Microsoft OneDrive Client
Group: System Environment/Network

View file

@ -2,7 +2,8 @@
Syncing OneDrive Business Shared Folders requires additional configuration for your 'onedrive' client:
1. List available shared folders to determine which folder you wish to sync & to validate that you have access to that folder
2. Create a new file called 'business_shared_folders' in your config directory which contains a list of the shared folders you wish to sync
3. Perform a sync
3. Test the configuration using '--dry-run'
4. Sync the OneDrive Business Shared folders as required
## Listing available OneDrive Business Shared Folders
List the available OneDrive Business Shared folders with the following command:

View file

@ -115,7 +115,7 @@ docker volume inspect onedrive_conf
Or you can map your own config folder to the config volume. Make sure to copy all files from the docker volume into your mapped folder first.
The detailed document for the config can be found here: [additional-configuration](https://github.com/abraunegg/onedrive#additional-configuration)
The detailed document for the config can be found here: [Configuration](https://github.com/abraunegg/onedrive/blob/master/docs/USAGE.md#configuration)
### 7. Sync multiple accounts
@ -152,7 +152,9 @@ docker run $firstRun --restart unless-stopped --name onedrive -v onedrive_conf:/
| <B>ONEDRIVE_GID</B> | GroupID (GID) to run as | 1000 |
| <B>ONEDRIVE_VERBOSE</B> | Controls "--verbose" switch on onedrive sync. Default is 0 | 1 |
| <B>ONEDRIVE_DEBUG</B> | Controls "--verbose --verbose" switch on onedrive sync. Default is 0 | 1 |
| <B>ONEDRIVE_DEBUG_HTTPS</B> | Controls "--debug-https" switch on onedrive sync. Default is 0 | 1 |
| <B>ONEDRIVE_RESYNC</B> | Controls "--resync" switch on onedrive sync. Default is 0 | 1 |
| <B>ONEDRIVE_DOWNLOADONLY</B> | Controls "--download-only" switch on onedrive sync. Default is 0 | 1 |
### Usage Examples
**Verbose Output:**

View file

@ -6,8 +6,9 @@ This project has been packaged for the following Linux distributions:
* Arch Linux, available from AUR as [onedrive-abraunegg](https://aur.archlinux.org/packages/onedrive-abraunegg/)
* Debian, available from the package repository as [onedrive](https://packages.debian.org/sid/net/onedrive)
* Fedora, available via package repositories as [onedrive](https://koji.fedoraproject.org/koji/packageinfo?packageID=26044)
* Gentoo, available via portage overlay as [onedrive](https://gpo.zugaina.org/net-misc/onedrive)
* NixOS, use package `onedrive` either by adding it to `configuration.nix` or by using the command `nix-env -iA <channel name>.onedrive`. This does not install a service. To install a service, use unstable channel (will stabilize in 20.09) and add `services.onedrive.enable=true` in `configuration.nix`. You can also add a custom package using the `services.onedrive.package` option (recommended since package lags upstream). Enabling the service installs a default package too (based on the channel). You can also add multiple onedrive accounts trivially, see [documentation](https://github.com/NixOS/nixpkgs/pull/77734#issuecomment-575874225)`.
* openSUSE, available for Tumbleweed as [onedrive](https://software.opensuse.org/package/onedrive)
* openSUSE, available for Tumbleweed, Leap 15.2, Leap 15.1 as [onedrive](https://software.opensuse.org/package/onedrive)
* Slackware, available from the slackbuilds.org repository as [onedrive](https://slackbuilds.org/repository/14.2/network/onedrive/)
* Solus, available from the package repository as [onedrive](https://dev.getsol.us/search/query/FB7PIf1jG9Z9/#R)
* Ubuntu, available as a package from the following PPA [onedrive](https://launchpad.net/~yann1ck/+archive/ubuntu/onedrive)
@ -233,6 +234,7 @@ sudo pacman -S libnotify
```
### Dependencies: Raspbian (ARMHF)
**Note:** Build environment must have at least 1GB of memory & 1GB swap space. Check with `swapon`.
```text
sudo apt-get install libcurl4-openssl-dev
sudo apt-get install libsqlite3-dev
@ -322,8 +324,8 @@ as far as possible automatically, but can be overridden by passing
`--with-fish-completion-dir=<DIR>` to `configure`.
### Building using a different compiler (for example [LDC](https://wiki.dlang.org/LDC))
#### ARMHF Architecture
**Note:** Build environment must have at least 1GB of memory & 1GB swap space. Check with `swapon -s`
#### ARMHF Architecture (Raspbian etc)
**Note:** Build environment must have at least 1GB of memory & 1GB swap space. Check with `swapon`.
```text
git clone https://github.com/abraunegg/onedrive.git
cd onedrive
@ -333,7 +335,7 @@ sudo make install
```
#### ARM64 Architecture
**Note:** Build environment must have at least 1GB of memory & 1GB swap space. Check with `swapon -s`
**Note:** Build environment must have at least 1GB of memory & 1GB swap space. Check with `swapon`
```text
git clone https://github.com/abraunegg/onedrive.git
cd onedrive

View file

@ -1,12 +1,24 @@
# Show how to access a Sharepoint group drive in Office 365 business or education
## Obtaining the Sharepoint Site Details
# How to configure OneDrive SharePoint Shared Library sync
Syncing a OneDrive SharePoint library requires additional configuration for your 'onedrive' client:
1. Login to OneDrive and under 'Shared Libraries' obtain the shared library name
2. Query that shared library name using the client to obtain the required configuration details
3. Configure the client's config file with the required 'drive_id'
4. Test the configuration using '--dry-run'
5. Sync the SharePoint Library as required
## Listing available OneDrive SharePoint Libraries
1. Login to the OneDrive web interface and determine which shared library you wish to configure the client for:
![shared_libraries](./images/SharedLibraries.jpg)
## Query that shared library name using the client to obtain the required configuration details
2. Run the following command using the 'onedrive' client
```text
onedrive --get-O365-drive-id '<your library name>'
```
3. This will return the following:
```text
Configuration file successfully loaded
Configuring Global Azure AD Endpoints
Initializing the Synchronization Engine ...
Office 365 Library Name Query: <your library name>
SiteName: <your library name>
@ -14,10 +26,21 @@ drive_id: b!6H_y8B...xU5
URL: <your site URL>
```
## Configuring the onedrive client
Once you have obtained the 'drive_id' above, add to your 'onedrive' configuration file (`~/.config/onedrive/config`) the following:
## Configure the client's config file with the required 'drive_id'
4. Once you have obtained the 'drive_id' above, add to your 'onedrive' configuration file (`~/.config/onedrive/config`) the following:
```text
drive_id = "insert the drive id from above here"
drive_id = "insert the drive_id value from above here"
```
The OneDrive client will now be configured to sync this SharePoint shared library to your local system.
The OneDrive client will now sync this SharePoint shared library to your local system.
**Note:** After changing `drive_id`, you must perform a full re-synchronization by adding `--resync` to your existing command line.
## Test the configuration using '--dry-run'
5. Test your new configuration using the `--dry-run` option to validate the the new configuration
## Sync the SharePoint Library as required
6. Sync the SharePoint Library to your system with either `--synchronize` or `--monitor` operations
# How to configure multiple OneDrive SharePoint Shared Library sync
Refer to [./advanced-usage.md](advanced-usage.md) for configuration assistance.

View file

@ -300,6 +300,8 @@ The default configuration file is listed below:
# azure_ad_endpoint = ""
# azure_tenant_id = "common"
# sync_business_shared_folders = "false"
# sync_dir_permissions = "700"
# sync_file_permissions = "600"
```
@ -327,6 +329,26 @@ The issue here is around how the client stores the sync_dir path in the database
**Important Note:** If your `sync_dir` is pointing to a network mount point (a network share via NFS, Windows Network Share, Samba Network Share) these types of network mount points do not support 'inotify', thus tracking real-time changes via inotify of local files is not possible. Local filesystem changes will be replicated between the local filesystem and OneDrive based on the `monitor_interval` value. This is not something (inotify support for NFS, Samba) that this client can fix.
#### sync_dir directory and file permissions
The following are directory and file default permissions for any new directory or file that is created:
* Directories: 700 - This provides the following permissions: `drwx------`
* Files: 600 - This provides the following permissions: `-rw-------`
To change the default permissions, update the following 2 configuration options with the required permissions. Utilise [Unix Permissions Calculator](http://permissions-calculator.org/) to assist in determining the required permissions.
```text
# When changing a config option below, remove the '#' from the start of the line
# For explanations of all config options below see docs/USAGE.md or the man page.
#
...
# sync_business_shared_folders = "false"
sync_dir_permissions = "700"
sync_file_permissions = "600"
```
**Important:** Special permission bits (setuid, setgid, sticky bit) are not supported. Valid permission values are from `000` to `777` only.
#### skip_dir
Example:
```text
@ -474,6 +496,33 @@ The following are supported for pattern matching and exclusion rules:
**Note:** after changing the sync_list, you must perform a full re-synchronization by adding `--resync` to your existing command line - for example: `onedrive --synchronize --resync`
### Configuring the client for 'single tenant application' use
In some instances when using OneDrive Business Accounts, depending on the Azure organisational configuration, it will be necessary to configure the client as a 'single tenant application'.
To configure this, after creating the application on your Azure tenant, update the 'config' file with the tenant name (not the GUID) and the newly created Application ID, then this will be used for the authentication process.
```text
# skip_dir_strict_match = "false"
application_id = "your.application.id.guid"
# resync = "false"
# bypass_data_preservation = "false"
# azure_ad_endpoint = "xxxxxx"
azure_tenant_id = "your.azure.tenant.name"
# sync_business_shared_folders = "false"
```
### Configuring the client to use older 'skilion' application identifier
In some instances it may be desirable to utilise the older 'skilion' application identifier to avoid authorising a new application ID within Microsoft Azure environments.
To configure this, update the 'config' file with the old Application ID, then this will be used for the authentication process.
```text
# skip_dir_strict_match = "false"
application_id = "22c49a0d-d21c-4792-aed1-8f163c982546"
# resync = "false"
# bypass_data_preservation = "false"
```
**Note:** The application will now use the older 'skilion' client identifier, however this may increase your chances of getting a OneDrive 429 error.
**Note:** After changing the 'application_id' you will need to restart any 'onedrive' process you have running, and potentially issue a `--logout` to re-auth the client with this updated application ID.
### How to 'skip' directories from syncing?
There are several mechanisms available to 'skip' a directory from the sync process:
* Utilise 'skip_dir'
@ -596,7 +645,7 @@ journalctl --unit=onedrive@<username> -f
In some cases you may wish to receive GUI notifications when using the client when logged in as a non-root user. In this case, follow the directions below:
1. Login via graphical UI as user you wish to enable the service for
2. Disable any `onedive@` service files for your username - eg:
2. Disable any `onedrive@` service files for your username - eg:
```text
sudo systemctl stop onedrive@alex.service
sudo systemctl disable onedrive@alex.service
@ -616,42 +665,7 @@ journalctl --user-unit=onedrive -f
## Additional Configuration
### Using multiple OneDrive accounts
You can run multiple instances of the application by specifying a different config directory in order to handle multiple OneDrive accounts. For example, if you have a work and a personal account, you can run the onedrive command using the --confdir parameter. Here is an example:
```text
onedrive --synchronize --verbose --confdir="~/.config/onedrivePersonal" &
onedrive --synchronize --verbose --confdir="~/.config/onedriveWork" &
```
or
```text
onedrive --monitor --verbose --confdir="~/.config/onedrivePersonal" &
onedrive --monitor --verbose --confdir="~/.config/onedriveWork" &
```
* `--synchronize` does a one-time sync
* `--monitor` keeps the application running and monitoring for changes both local and remote
* `&` puts the application in background and leaves the terminal interactive
**Important:** For each configuration, change the 'sync_dir' to a new value, unique for each specific configuration. Leaving this at the default of `sync_dir = "~/OneDrive"` will cause all data from both accounts to be synced to the same folder, then to each other.
### Automatic syncing of both OneDrive accounts
In order to automatically start syncing your OneDrive accounts, you will need to create a service file for each account. From the applicable 'user systemd folder':
* RHEL / CentOS: `/usr/lib/systemd/system`
* Others: `/usr/lib/systemd/user`
```text
cp onedrive.service onedrive-work.service
```
And edit the line beginning with `ExecStart` so that the confdir mirrors the one you used above:
```text
ExecStart=/usr/local/bin/onedrive --monitor --confdir="/path/to/config/dir"
```
Then you can safely run these commands:
```text
systemctl --user enable onedrive-work
systemctl --user start onedrive-work
```
Repeat these steps for each OneDrive account that you wish to use.
Refer to [./advanced-usage.md](advanced-usage.md) for configuration assistance.
### Access OneDrive service through a proxy
If you have a requirement to run the client through a proxy, there are a couple of ways to achieve this:
@ -690,7 +704,6 @@ sudo restorecon -R -v /path/to/onedriveSyncFolder
```
## All available commands
Output of `onedrive --help`
```text
OneDrive - a client for OneDrive Cloud Services

139
docs/advanced-usage.md Normal file
View file

@ -0,0 +1,139 @@
# Advanced Configuration of the OneDrive Free Client
This document covers the following scenarios:
* Configuring the client to use mutlitple OneDrive accounts / configurations
* Configuring the client for use in dual-boot (Windows / Linux) situations
## Configuring the client to use mutlitple OneDrive accounts / configurations
Essentially, each OneDrive account or SharePoint Shared Library which you require to be synced needs to have it's own and unique configuration, local sync directory and service files. To do this, the following steps are needed:
1. Create a unique configuration folder for each onedrive client configuration that you need
2. Copy to this folder a copy of the default configuration file
3. Update the default configuration file as required, changing the required minimum config options and any additional options as needed to support your multi-account configuration
4. Authenticate the client using the new configuration directory
5. Test the configuration using '--display-config' and '--dry-run'
6. Sync the OneDrive account data as required using `--synchronize` or `--monitor`
7. Configure a unique systemd service file for this account configuration
### 1. Create a unique configuration folder for each onedrive client configuration that you need
Make the configuration folder as required for this new configuration, for example:
```text
mkdir ~/.config/my-new-config
```
### 2. Copy to this folder a copy of the default configuration file
Copy to this folder a copy of the default configuration file by downloading this file from GitHub and saving this file in the directory created above:
```text
wget https://raw.githubusercontent.com/abraunegg/onedrive/master/config -O ~/.config/my-new-config/config
```
### 3. Update the default configuration file
The following config options *must* be updated to ensure that individual account data is not cross populated with other OneDrive accounts or other configurations:
* sync_dir
Other options that may require to be updated, depending on the OneDrive account that is being configured:
* drive_id
* application_id
* sync_business_shared_folders
* skip_dir
* skip_file
* Creation of a 'sync_list' file if required
* Creation of a 'business_shared_folders' file if required
### 4. Authenticate the client
Authenticate the client using the specific configuration file:
```text
onedrive --confdir="~/.config/my-new-config"
```
You will be asked to open a specific URL by using your web browser where you will have to login into your Microsoft Account and give the application the permission to access your files. After giving permission to the application, you will be redirected to a blank page. Copy the URI of the blank page into the application.
```text
[user@hostname ~]$ onedrive --confdir="~/.config/my-new-config"
Configuration file successfully loaded
Configuring Global Azure AD Endpoints
Authorize this app visiting:
https://.....
Enter the response uri:
```
### 5. Display and Test the configuration
Test the configuration using '--display-config' and '--dry-run'. By doing so, this allows you to test any configuration that you have currently made, enabling you to fix this configuration before using the configuration.
#### Display the configuration
```text
onedrive --confdir="~/.config/my-new-config --display-config"
```
#### Test the configuration by performing a dry-run
```text
onedrive --confdir="~/.config/my-new-config" --synchronize --verbose --dry-run
```
If both of these operate as per your expectation, the configuration of this client setup is complete and validated. If not, amend your configuration as required.
### 6. Sync the OneDrive account data as required
Sync the data for the new account configuration as required:
```text
onedrive --confdir="~/.config/my-new-config" --synchronize --verbose
```
or
```text
onedrive --confdir="~/.config/my-new-config" --monitor --verbose
```
* `--synchronize` does a one-time sync
* `--monitor` keeps the application running and monitoring for changes both local and remote
### 7. Automatic syncing of new OneDrive configuration
In order to automatically start syncing your OneDrive accounts, you will need to create a service file for each account. From the applicable 'systemd folder' where the applicable systemd service file exists:
* RHEL / CentOS: `/usr/lib/systemd/system`
* Others: `/usr/lib/systemd/user` and `/lib/systemd/system`
**Note:** The `onedrive.service` runs the service as the 'root' user, whereas the `onedrive@.service` runs the service as your user account.
Copy the required service file to a new name:
```text
cp onedrive.service onedrive-my-new-config.service
```
or
```text
cp onedrive@.service onedrive-my-new-config@.service
```
Edit the line beginning with `ExecStart` so that the confdir mirrors the one you used above:
```text
ExecStart=/usr/local/bin/onedrive --monitor --confdir="/full/path/to/config/dir"
```
Example:
```text
ExecStart=/usr/local/bin/onedrive --monitor --confdir="/home/myusername/.config/my-new-config"
```
Then you can safely run these commands:
```text
systemctl --user enable onedrive-my-new-config
systemctl --user start onedrive-my-new-config
```
or
```text
systemctl --user enable onedrive-my-new-config@myusername.service
systemctl --user start onedrive-my-new-config@myusername.service
```
Repeat these steps for each OneDrive new account that you wish to use.
## Configuring the client for use in dual-boot (Windows / Linux) situations
When dual booting Windows and Linux, depending on the Windows OneDrive account configuration, the 'Files On-Demand' option may be enabled when running OneDrive within your Windows environment.
When this option is enabled in Windows, if you are sharing this location between your Windows and Linux systems, all files will be a 0 byte link, and cannot be used under Linux.
To fix the problem of windows turning all files (that should be kept offline) into links, you have to uncheck a specific option in the onedrive settings window. The option in question is `Save space and download files as you use them`.
To find this setting, open the onedrive pop-up window from the taskbar, click "Help & Settings" > "Settings". This opens a new window. Go to the tab "Settings" and look for the section "Files On-Demand".
After unchecking the option and clicking "OK", the Windows OneDrive client should restart itself and start actually downloading your files so they will truely be available on your disk when offline. These files will then be fully accessible under Linux and the Linux OneDrive client.
| OneDrive Personal | Onedrive Business<br>SharePoint |
|---|---|
| ![Uncheck-Personal](./images/personal-files-on-demand.png) | ![Uncheck-Business](./images/business-files-on-demand.png) |

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -241,13 +241,23 @@ State caching
Real-Time file monitoring with Inotify
File upload / download validation to ensure data integrity
Resumable uploads
Support OneDrive for Business (part of Office 365)
Shared folders (OneDrive Personal)
Shared Folder support for OneDrive Personal and OneDrive Business accounts
SharePoint / Office 365 Group Drives (refer to README.Office365.md to configure)
SharePoint / Office365 Shared Libraries
Desktop notifications via libnotify
Dry-run capability to test configuration changes
Prevent major OneDrive accidental data deletion after configuration change
Support for National cloud deployments (Microsoft Cloud for US Government, Microsoft Cloud Germany, Azure and Office 365 operated by 21Vianet in China)
.SH CONFIGURATION
@ -339,4 +349,8 @@ for a user via the \fBonedrive@<username>\fP service.
Further examples and documentation is available in
\f[C]README.md\f[]
\f[C]README.Office365.md\f[]
\f[C]docs/USAGE.md\f[]
\f[C]docs/advanced-usage.md\f[]
\f[C]docs/BusinessSharedFolders.md\f[]
\f[C]docs/Office365.md\f[]
\f[C]docs/national-cloud-deployments.md\f[]

View file

@ -36,11 +36,17 @@ final class Config
private long[string] longValues;
// Compile time regex - this does not change
public auto configRegex = ctRegex!(`^(\w+)\s*=\s*"(.*)"\s*$`);
// Default directory permission mode
public long defaultDirectoryPermissionMode = 700;
public int configuredDirectoryPermissionMode;
// Default file permission mode
public long defaultFilePermissionMode = 600;
public int configuredFilePermissionMode;
this(string confdirOption)
{
// default configuration - entries in config file ~/.config/onedrive/config
// an entry here means it can be set via the config file if there is a coresponding read and set in update_from_args()
// an entry here means it can be set via the config file if there is a coresponding entry, read from config and set via update_from_args()
stringValues["sync_dir"] = defaultSyncDir;
stringValues["skip_file"] = defaultSkipFile;
stringValues["skip_dir"] = defaultSkipDir;
@ -106,6 +112,10 @@ final class Config
stringValues["azure_tenant_id"] = "common";
// Allow enable / disable of the syncing of OneDrive Business Shared Folders via configuration file
boolValues["sync_business_shared_folders"] = false;
// Configure the default folder permission attributes for newly created folders
longValues["sync_dir_permissions"] = defaultDirectoryPermissionMode;
// Configure the default file permission attributes for newly created file
longValues["sync_file_permissions"] = defaultFilePermissionMode;
// DEVELOPER OPTIONS
// display_memory = true | false
@ -181,7 +191,13 @@ final class Config
}
// Config directory options all determined
if (!exists(configDirName)) mkdirRecurse(configDirName);
if (!exists(configDirName)) {
// create the directory
mkdirRecurse(configDirName);
// Configure the applicable permissions for the folder
configDirName.setAttributes(returnRequiredDirectoryPermisions());
}
// configDirName has a trailing /
log.vlog("Using 'user' Config Dir: ", configDirName);
log.vlog("Using 'system' Config Dir: ", systemConfigDirName);
@ -266,8 +282,6 @@ final class Config
boolValues["monitor"] = false;
boolValues["synchronize"] = false;
boolValues["force"] = false;
boolValues["remove_source_files"] = false;
boolValues["skip_dir_strict_match"] = false;
boolValues["list_business_shared_folders"] = false;
// Application Startup option validation
@ -611,6 +625,59 @@ final class Config
}
return true;
}
void configureRequiredDirectoryPermisions() {
// return the directory permission mode required
// - return octal!defaultDirectoryPermissionMode; ... cant be used .. which is odd
// Error: variable defaultDirectoryPermissionMode cannot be read at compile time
if (getValueLong("sync_dir_permissions") != defaultDirectoryPermissionMode) {
// return user configured permissions as octal integer
string valueToConvert = to!string(getValueLong("sync_dir_permissions"));
auto convertedValue = parse!long(valueToConvert, 8);
configuredDirectoryPermissionMode = to!int(convertedValue);
} else {
// return default as octal integer
string valueToConvert = to!string(defaultDirectoryPermissionMode);
auto convertedValue = parse!long(valueToConvert, 8);
configuredDirectoryPermissionMode = to!int(convertedValue);
}
}
void configureRequiredFilePermisions() {
// return the file permission mode required
// - return octal!defaultFilePermissionMode; ... cant be used .. which is odd
// Error: variable defaultFilePermissionMode cannot be read at compile time
if (getValueLong("sync_file_permissions") != defaultFilePermissionMode) {
// return user configured permissions as octal integer
string valueToConvert = to!string(getValueLong("sync_file_permissions"));
auto convertedValue = parse!long(valueToConvert, 8);
configuredFilePermissionMode = to!int(convertedValue);
} else {
// return default as octal integer
string valueToConvert = to!string(defaultFilePermissionMode);
auto convertedValue = parse!long(valueToConvert, 8);
configuredFilePermissionMode = to!int(convertedValue);
}
}
int returnRequiredDirectoryPermisions() {
// read the configuredDirectoryPermissionMode and return
if (configuredDirectoryPermissionMode == 0) {
// the configured value is zero, this means that directories would get
// values of d---------
configureRequiredDirectoryPermisions();
}
return configuredDirectoryPermissionMode;
}
int returnRequiredFilePermisions() {
// read the configuredFilePermissionMode and return
if (configuredFilePermissionMode == 0) {
// the configured value is zero
configureRequiredFilePermisions();
}
return configuredFilePermissionMode;
}
}
void outputLongHelp(Option[] opt)

View file

@ -474,4 +474,11 @@ final class ItemDatabase
}
return items;
}
// Perform a vacuum on the database, commit WAL / SHM to file
void performVacuum()
{
auto stmt = db.prepare("VACUUM;");
stmt.exec();
}
}

View file

@ -62,10 +62,16 @@ int main(string[] args)
if (onedriveInitialised) {
oneDrive.shutdown();
}
// Make sure the .wal file is incorporated into the main db before we exit
destroy(itemDb);
// was itemDb initialised?
if (itemDb !is null) {
// Make sure the .wal file is incorporated into the main db before we exit
itemDb.performVacuum();
destroy(itemDb);
}
// free API instance
oneDrive = null;
if (oneDrive !is null) {
destroy(oneDrive);
}
// Perform Garbage Cleanup
GC.collect();
// Display memory details
@ -83,10 +89,16 @@ int main(string[] args)
if (onedriveInitialised) {
oneDrive.shutdown();
}
// Make sure the .wal file is incorporated into the main db before we exit
destroy(itemDb);
// was itemDb initialised?
if (itemDb !is null) {
// Make sure the .wal file is incorporated into the main db before we exit
itemDb.performVacuum();
destroy(itemDb);
}
// free API instance
oneDrive = null;
if (oneDrive !is null) {
destroy(oneDrive);
}
// Perform Garbage Cleanup
GC.collect();
// Display memory details
@ -380,12 +392,23 @@ int main(string[] args)
// dry-run notification and database setup
if (cfg.getValueBool("dry_run")) {
log.log("DRY-RUN Configured. Output below shows what 'would' have occurred.");
string dryRunShmFile = cfg.databaseFilePathDryRun ~ "-shm";
string dryRunWalFile = cfg.databaseFilePathDryRun ~ "-wal";
// If the dry run database exists, clean this up
if (exists(cfg.databaseFilePathDryRun)) {
// remove the existing file
log.vdebug("Removing items-dryrun.sqlite3 as it still exists for some reason");
safeRemove(cfg.databaseFilePathDryRun);
}
// silent cleanup of shm and wal files if they exist
if (exists(dryRunShmFile)) {
// remove items-dryrun.sqlite3-shm
safeRemove(dryRunShmFile);
}
if (exists(dryRunWalFile)) {
// remove items-dryrun.sqlite3-wal
safeRemove(dryRunWalFile);
}
// Make a copy of the original items.sqlite3 for use as the dry run copy if it exists
if (exists(cfg.databaseFilePath)) {
@ -486,6 +509,9 @@ int main(string[] args)
writeln("Config option 'min_notify_changes' = ", cfg.getValueLong("min_notify_changes"));
writeln("Config option 'log_dir' = ", cfg.getValueString("log_dir"));
writeln("Config option 'classify_as_big_delete' = ", cfg.getValueLong("classify_as_big_delete"));
writeln("Config option 'upload_only' = ", cfg.getValueBool("upload_only"));
writeln("Config option 'no_remote_delete' = ", cfg.getValueBool("no_remote_delete"));
writeln("Config option 'remove_source_files' = ", cfg.getValueBool("remove_source_files"));
// Is config option drive_id configured?
if (cfg.getValueString("drive_id") != ""){
@ -574,10 +600,17 @@ int main(string[] args)
// was the application just authorised?
if (cfg.applicationAuthorizeResponseUri) {
// Application was just authorised
log.log("\nApplication has been successfully authorised, however no additional command switches were provided.\n");
log.log("Please use --help for further assistance in regards to running this application.\n");
// Use exit scopes to shutdown API
return EXIT_SUCCESS;
if (exists(cfg.refreshTokenFilePath)) {
// OneDrive refresh token exists
log.log("\nApplication has been successfully authorised, however no additional command switches were provided.\n");
log.log("Please use --help for further assistance in regards to running this application.\n");
// Use exit scopes to shutdown API
return EXIT_SUCCESS;
} else {
// we just authorised, but refresh_token does not exist .. probably an auth error
log.log("\nApplication has not been successfully authorised. Please check your URI response entry and try again.\n");
return EXIT_FAILURE;
}
} else {
// Application was not just authorised
log.log("\n--synchronize or --monitor switches missing from your command line input. Please add one (not both) of these switches to your command line or use --help for further assistance.\n");
@ -607,6 +640,23 @@ int main(string[] args)
itemDb = new ItemDatabase(cfg.databaseFilePathDryRun);
}
// What are the permission that have been set for the application?
// These are relevant for:
// - The ~/OneDrive parent folder or 'sync_dir' configured item
// - Any new folder created under ~/OneDrive or 'sync_dir'
// - Any new file created under ~/OneDrive or 'sync_dir'
// valid permissions are 000 -> 777 - anything else is invalid
if ((cfg.getValueLong("sync_dir_permissions") < 0) || (cfg.getValueLong("sync_file_permissions") < 0) || (cfg.getValueLong("sync_dir_permissions") > 777) || (cfg.getValueLong("sync_file_permissions") > 777)) {
log.error("ERROR: Invalid 'User|Group|Other' permissions set within config file. Please check.");
return EXIT_FAILURE;
} else {
// debug log output what permissions are being set to
log.vdebug("Configuring default new folder permissions as: ", cfg.getValueLong("sync_dir_permissions"));
cfg.configureRequiredDirectoryPermisions();
log.vdebug("Configuring default new file permissions as: ", cfg.getValueLong("sync_file_permissions"));
cfg.configureRequiredFilePermisions();
}
// configure the sync direcory based on syncDir config option
log.vlog("All operations will be performed in: ", syncDir);
if (!exists(syncDir)) {
@ -614,6 +664,9 @@ int main(string[] args)
try {
// Attempt to create the sync dir we have been configured with
mkdirRecurse(syncDir);
// Configure the applicable permissions for the folder
log.vdebug("Setting directory permissions for: ", syncDir);
syncDir.setAttributes(cfg.returnRequiredDirectoryPermisions());
} catch (std.file.FileException e) {
// Creating the sync directory failed
log.error("ERROR: Unable to create local OneDrive syncDir - ", e.msg);
@ -621,6 +674,8 @@ int main(string[] args)
return EXIT_FAILURE;
}
}
// Change the working directory to the 'sync_dir' configured item
chdir(syncDir);
// Configure selective sync by parsing and getting a regex for skip_file config component
@ -721,14 +776,18 @@ int main(string[] args)
// Do we need to configure specific --upload-only options?
if (cfg.getValueBool("upload_only")) {
// --upload-only was passed in or configured
log.vdebug("Configuring uploadOnly flag to TRUE as --upload-only passed in or configured");
sync.setUploadOnly();
// was --no-remote-delete passed in or configured
if (cfg.getValueBool("no_remote_delete")) {
// Configure the noRemoteDelete flag
log.vdebug("Configuring noRemoteDelete flag to TRUE as --no-remote-delete passed in or configured");
sync.setNoRemoteDelete();
}
// was --remove-source-files passed in or configured
if (cfg.getValueBool("remove_source_files")) {
// Configure the localDeleteAfterUpload flag
log.vdebug("Configuring localDeleteAfterUpload flag to TRUE as --remove-source-files passed in or configured");
sync.setLocalDeleteAfterUpload();
}
}
@ -858,14 +917,22 @@ int main(string[] args)
if (!exists(cfg.getValueString("single_directory"))) {
// The requested path to use with --single-directory does not exist locally within the configured 'sync_dir'
log.logAndNotify("WARNING: The requested path for --single-directory does not exist locally. Creating requested path within ", syncDir);
// Make the required path locally
mkdirRecurse(cfg.getValueString("single_directory"));
// Make the required --single-directory path locally
string singleDirectoryPath = cfg.getValueString("single_directory");
mkdirRecurse(singleDirectoryPath);
// Configure the applicable permissions for the folder
log.vdebug("Setting directory permissions for: ", singleDirectoryPath);
singleDirectoryPath.setAttributes(cfg.returnRequiredDirectoryPermisions());
}
}
// perform a --synchronize sync
// fullScanRequired = false, for final true-up
// but if we have sync_list configured, use syncListConfigured which = true
performSync(sync, cfg.getValueString("single_directory"), cfg.getValueBool("download_only"), cfg.getValueBool("local_first"), cfg.getValueBool("upload_only"), LOG_NORMAL, false, syncListConfigured, displaySyncOptions, cfg.getValueBool("monitor"), m);
// Write WAL and SHM data to file for this sync
log.vdebug("Merge contents of WAL and SHM files into main database file");
itemDb.performVacuum();
}
}
@ -1077,8 +1144,15 @@ int main(string[] args)
if (displayMemoryUsage) {
log.displayMemoryUsagePostGC();
}
// Write WAL and SHM data to file for this loop
log.vdebug("Merge contents of WAL and SHM files into main database file");
itemDb.performVacuum();
// monitor loop complete
logOutputMessage = "################################################ LOOP COMPLETE ###############################################";
// Handle display options
if (displaySyncOptions) {
log.log(logOutputMessage);
} else {
@ -1100,11 +1174,23 @@ int main(string[] args)
// --dry-run temp database cleanup
if (cfg.getValueBool("dry_run")) {
string dryRunShmFile = cfg.databaseFilePathDryRun ~ "-shm";
string dryRunWalFile = cfg.databaseFilePathDryRun ~ "-wal";
if (exists(cfg.databaseFilePathDryRun)) {
// remove the file
log.vdebug("Removing items-dryrun.sqlite3 as dry run operations complete");
// remove items-dryrun.sqlite3
safeRemove(cfg.databaseFilePathDryRun);
}
// silent cleanup of shm and wal files if they exist
if (exists(dryRunShmFile)) {
// remove items-dryrun.sqlite3-shm
safeRemove(dryRunShmFile);
}
if (exists(dryRunWalFile)) {
// remove items-dryrun.sqlite3-wal
safeRemove(dryRunWalFile);
}
}
// Exit application
@ -1372,8 +1458,12 @@ 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);
// was itemDb initialised?
if (itemDb !is null) {
// Make sure the .wal file is incorporated into the main db before we exit
itemDb.performVacuum();
destroy(itemDb);
}
// Use exit scopes to shutdown OneDrive API
})();
} catch(Exception e) {}

View file

@ -1,7 +1,8 @@
import core.sys.linux.sys.inotify;
import core.stdc.errno;
import core.sys.posix.poll, core.sys.posix.unistd;
import std.exception, std.file, std.path, std.regex, std.stdio, std.string, std.algorithm.mutation;
import std.exception, std.file, std.path, std.regex, std.stdio, std.string, std.algorithm;
import core.stdc.stdlib;
import config;
import selective;
import util;
@ -132,17 +133,43 @@ final class Monitor
}
// passed all potential exclusions
// add inotify watch for this path / directory / file
log.vdebug("Calling add() for this dirname: ", dirname);
add(dirname);
try {
auto pathList = dirEntries(dirname, SpanMode.shallow, false);
foreach(DirEntry entry; pathList) {
if (entry.isDir) {
addRecursive(entry.name);
// if this is a directory, recursivly add this path
if (isDir(dirname)) {
// try and get all the directory entities for this path
try {
auto pathList = dirEntries(dirname, SpanMode.shallow, false);
foreach(DirEntry entry; pathList) {
if (entry.isDir) {
log.vdebug("Calling addRecursive() for this directory: ", entry.name);
addRecursive(entry.name);
}
}
// catch any error which is generated
} catch (std.file.FileException e) {
// Standard filesystem error
displayFileSystemErrorMessage(e.msg);
return;
} catch (Exception e) {
// Issue #1154 handling
// Need to check for: Failed to stat file in error message
if (canFind(e.msg, "Failed to stat file")) {
// File system access issue
log.error("ERROR: The local file system returned an error with the following message:");
log.error(" Error Message: ", e.msg);
log.error("ACCESS ERROR: Please check your UID and GID access to this file, as the permissions on this file is preventing this application to read it");
log.error("\nFATAL: Exiting application to avoid deleting data due to local file system access issues\n");
// Must exit here
exit(-1);
} else {
// some other error
displayFileSystemErrorMessage(e.msg);
return;
}
}
} catch (std.file.FileException e) {
log.vdebug("ERROR: ", e.msg);
return;
}
}
@ -173,6 +200,7 @@ final class Monitor
// Add path to inotify watch - required regardless if a '.folder' or 'folder'
wdToDirName[wd] = buildNormalizedPath(pathname) ~ "/";
log.vdebug("inotify_add_watch successfully added for: ", pathname);
// Do we log that we are monitoring this directory?
if (isDir(pathname)) {
@ -360,4 +388,12 @@ final class Monitor
}
}
}
// Parse and display error message received from the local file system
private void displayFileSystemErrorMessage(string message)
{
log.error("ERROR: The local file system returned an error with the following message:");
auto errorArray = splitLines(message);
log.error(" Error Message: ", errorArray[0]);
}
}

View file

@ -15,6 +15,9 @@ private bool simulateNoRefreshTokenFile = false;
private ulong retryAfterValue = 0;
private immutable {
// Client ID / Application ID (abraunegg)
string clientIdDefault = "d50ca740-c83f-4d1b-b616-12c519384f0c";
// Azure Active Directory & Graph Explorer Endpoints
// Global & Defaults
string globalAuthEndpoint = "https://login.microsoftonline.com";
@ -38,8 +41,8 @@ private immutable {
}
private {
// Client ID / Application ID (abraunegg)
string clientId = "d50ca740-c83f-4d1b-b616-12c519384f0c";
// Client ID / Application ID
string clientId = clientIdDefault;
// Default User Agent configuration
string isvTag = "ISV";
@ -149,6 +152,14 @@ final class OneDriveApi
.debugResponse = true;
}
// Update clientId if application_id is set in config file
if (cfg.getValueString("application_id") != "") {
// an application_id is set in config file
log.vdebug("Setting custom application_id to: " , cfg.getValueString("application_id"));
clientId = cfg.getValueString("application_id");
companyName = "custom_application";
}
// Configure tenant id value, if 'azure_tenant_id' is configured,
// otherwise use the "common" multiplexer
string tenantId = "common";
@ -156,19 +167,35 @@ final class OneDriveApi
// Use the value entered by the user
tenantId = cfg.getValueString("azure_tenant_id");
}
// Configure Azure AD endpoints if 'azure_ad_endpoint' is configured
string azureConfigValue = cfg.getValueString("azure_ad_endpoint");
switch(azureConfigValue) {
case "":
log.log("Configuring Global Azure AD Endpoints");
if (tenantId == "common") {
log.log("Configuring Global Azure AD Endpoints");
} else {
log.log("Configuring Global Azure AD Endpoints - Single Tenant Application");
}
// Authentication
authUrl = globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize";
redirectUrl = globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient";
tokenUrl = globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token";
break;
case "USL4":
log.log("Configuring Azure AD for US Government Endpoints");
// Authentication
authUrl = usl4AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize";
redirectUrl = usl4AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient";
tokenUrl = usl4AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token";
if (clientId == clientIdDefault) {
// application_id == default
log.vdebug("USL4 AD Endpoint but default application_id, redirectUrl needs to be aligned to globalAuthEndpoint");
redirectUrl = globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient";
} else {
// custom application_id
redirectUrl = usl4AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient";
}
// Drive Queries
driveUrl = usl4GraphEndpoint ~ "/v1.0/me/drive";
driveByIdUrl = usl4GraphEndpoint ~ "/v1.0/drives/";
@ -186,8 +213,16 @@ final class OneDriveApi
log.log("Configuring Azure AD for US Government Endpoints (DOD)");
// Authentication
authUrl = usl5AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize";
redirectUrl = usl5AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient";
tokenUrl = usl5AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token";
if (clientId == clientIdDefault) {
// application_id == default
log.vdebug("USL5 AD Endpoint but default application_id, redirectUrl needs to be aligned to globalAuthEndpoint");
redirectUrl = globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient";
} else {
// custom application_id
redirectUrl = usl5AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient";
}
// Drive Queries
driveUrl = usl5GraphEndpoint ~ "/v1.0/me/drive";
driveByIdUrl = usl5GraphEndpoint ~ "/v1.0/drives/";
@ -205,8 +240,16 @@ final class OneDriveApi
log.log("Configuring Azure AD Germany");
// Authentication
authUrl = deAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize";
redirectUrl = deAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient";
tokenUrl = deAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token";
if (clientId == clientIdDefault) {
// application_id == default
log.vdebug("DE AD Endpoint but default application_id, redirectUrl needs to be aligned to globalAuthEndpoint");
redirectUrl = globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient";
} else {
// custom application_id
redirectUrl = deAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient";
}
// Drive Queries
driveUrl = deGraphEndpoint ~ "/v1.0/me/drive";
driveByIdUrl = deGraphEndpoint ~ "/v1.0/drives/";
@ -224,8 +267,16 @@ final class OneDriveApi
log.log("Configuring AD China operated by 21Vianet");
// Authentication
authUrl = cnAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize";
redirectUrl = cnAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient";
tokenUrl = cnAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/token";
if (clientId == clientIdDefault) {
// application_id == default
log.vdebug("CN AD Endpoint but default application_id, redirectUrl needs to be aligned to globalAuthEndpoint");
redirectUrl = globalAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient";
} else {
// custom application_id
redirectUrl = cnAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/nativeclient";
}
// Drive Queries
driveUrl = cnGraphEndpoint ~ "/v1.0/me/drive";
driveByIdUrl = cnGraphEndpoint ~ "/v1.0/drives/";
@ -296,13 +347,6 @@ final class OneDriveApi
bool init()
{
// Update clientId if application_id is set in config file
if (cfg.getValueString("application_id") != "") {
// an application_id is set in config file
clientId = cfg.getValueString("application_id");
companyName = "custom_application";
}
// detail what we are using for applicaion identification
log.vdebug("clientId = ", clientId);
log.vdebug("companyName = ", companyName);
@ -556,11 +600,43 @@ final class OneDriveApi
{
checkAccessTokenExpired();
scope(failure) {
if (exists(saveToPath)) remove(saveToPath);
if (exists(saveToPath)) {
// try and remove the file, catch error
try {
remove(saveToPath);
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg);
}
}
}
mkdirRecurse(dirName(saveToPath));
// Create the required local directory
string newPath = dirName(saveToPath);
// Does the path exist locally?
if (!exists(newPath)) {
try {
log.vdebug("Requested path does not exist, creating directory structure: ", newPath);
mkdirRecurse(newPath);
// Configure the applicable permissions for the folder
log.vdebug("Setting directory permissions for: ", newPath);
newPath.setAttributes(cfg.returnRequiredDirectoryPermisions());
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg);
}
}
const(char)[] url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/content?AVOverride=1";
// Download file
download(url, saveToPath, fileSize);
// Does path exist?
if (exists(saveToPath)) {
// File was downloaded sucessfully - configure the applicable permissions for the file
log.vdebug("Setting file permissions for: ", saveToPath);
saveToPath.setAttributes(cfg.returnRequiredFilePermisions());
}
}
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content
@ -792,7 +868,16 @@ final class OneDriveApi
refreshToken = response["refresh_token"].str();
accessTokenExpiration = Clock.currTime() + dur!"seconds"(response["expires_in"].integer());
if (!.dryRun) {
std.file.write(cfg.refreshTokenFilePath, refreshToken);
try {
// try and update the refresh_token file
log.vdebug("Updating refresh_token file with new token from OneDrive");
std.file.write(cfg.refreshTokenFilePath, refreshToken);
log.vdebug("Setting file permissions for: ", cfg.refreshTokenFilePath);
cfg.refreshTokenFilePath.setAttributes(cfg.returnRequiredFilePermisions());
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg);
}
}
if (printAccessToken) writeln("New access token: ", accessToken);
} else {
@ -803,7 +888,9 @@ final class OneDriveApi
} else {
// External tenant token handling
if ("access_token" in response){
log.vdebug("Updating access token with new token received from External OneDrive 3rd Party");
externalAccessToken = "bearer " ~ response["access_token"].str();
log.vdebug("Updating refresh_token with new token received from External OneDrive 3rd Party");
externalRefreshToken = response["refresh_token"].str();
externalAccessTokenExpiration = Clock.currTime() + dur!"seconds"(response["expires_in"].integer());
if (printAccessToken) writeln("New external access token: ", externalAccessToken);
@ -1399,6 +1486,14 @@ final class OneDriveApi
log.error(" Error Reason: ", errorMessage["error_description"].str);
}
}
// Parse and display error message received from the local file system
private void displayFileSystemErrorMessage(string message)
{
log.error("ERROR: The local file system returned an error with the following message:");
auto errorArray = splitLines(message);
log.error(" Error Message: ", errorArray[0]);
}
}
unittest

View file

@ -89,15 +89,30 @@ final class SelectiveSync
// Try full path match first
if (!name.matchFirst(dirmask).empty) {
log.vdebug("'!name.matchFirst(dirmask).empty' returned true = matched");
return true;
} else {
// Do we check the base name as well?
if (!skipDirStrictMatch) {
// check just the basename in the path
string parent = baseName(name);
if(!parent.matchFirst(dirmask).empty) {
return true;
log.vdebug("No Strict Matching Enforced");
// Test the entire path working backwards from child
string path = buildNormalizedPath(name);
string checkPath;
auto paths = pathSplitter(path);
foreach_reverse(directory; paths) {
if (directory != "/") {
// This will add a leading '/' but that needs to be stripped to check
checkPath = "/" ~ directory ~ checkPath;
if(!checkPath.strip('/').matchFirst(dirmask).empty) {
log.vdebug("'!checkPath.matchFirst(dirmask).empty' returned true = matched");
return true;
}
}
}
} else {
log.vdebug("Strict Matching Enforced - No Match");
}
}
// no match

View file

@ -130,10 +130,14 @@ private Item makeItem(const ref JSONValue driveItem)
// Resolve 'Key not found: fileSystemInfo' when then item is a remote item
// https://github.com/abraunegg/onedrive/issues/11
if (isItemRemote(driveItem)) {
// remoteItem is a OneDrive object that exists on a 'different' OneDrive drive id, when compared to account default
item.mtime = SysTime.fromISOExtString(driveItem["remoteItem"]["fileSystemInfo"]["lastModifiedDateTime"].str);
} else {
// item exists on account default drive id
item.mtime = SysTime.fromISOExtString(driveItem["fileSystemInfo"]["lastModifiedDateTime"].str);
}
// debug output of what the OneDrive item modified time is
log.vdebug("lastModifiedDateTime (OneDrive item): ", item.mtime);
}
if (isItemFile(driveItem)) {
@ -603,133 +607,158 @@ final class SyncEngine
// Log that an invalid JSON object was returned
log.error("ERROR: onedrive.getTenantID call returned an invalid JSON Object");
}
// query OneDrive Business Shared Folders shared with me
// Query OneDrive Business Shared Folders shared with me
log.vlog("Attempting to sync OneDrive Business Shared Folders");
JSONValue graphQuery = onedrive.getSharedWithMe();
if (graphQuery.type() == JSONType.object) {
string sharedFolderName;
bool isExternalTenant = false;
foreach (searchResult; graphQuery["value"].array) {
sharedFolderName = searchResult["name"].str;
// Compare this to values in business_shared_folders
if(selectiveSync.isSharedFolderMatched(sharedFolderName)){
// Folder name matches what we are looking for
// Flags for matching
bool itemInDatabase = false;
bool itemLocalDirExists = false;
bool itemPathIsLocal = false;
isExternalTenant = false;
// "what if" there are 2 or more folders shared with me have the "same" name?
// The folder name will be the same, but driveId will be different
// This will then cause these 'shared folders' to cross populate data, which may not be desirable
log.vdebug("Shared Folder Name: ", sharedFolderName);
log.vdebug("Parent Drive Id: ", searchResult["remoteItem"]["parentReference"]["driveId"].str);
log.vdebug("Shared Item Id: ", searchResult["remoteItem"]["id"].str);
// Is this OneDrive Shared Folder on an external tenant?
if (searchResult["remoteItem"]["sharepointIds"]["tenantId"].str != myTenantID) {
isExternalTenant = true;
log.vdebug("This shared folder is shared from an external organisation as the tenant is different");
// Have to configure the access to this tenant, which requires separate tokenUrl for that tenant
onedrive.setExternalTenant(searchResult["remoteItem"]["sharepointIds"]["tenantId"].str);
// Configure additional logging items for this array element
string sharedByName;
string sharedByEmail;
// Extra details for verbose logging
if ("sharedBy" in searchResult["remoteItem"]["shared"]) {
if ("displayName" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) {
sharedByName = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["displayName"].str;
}
// for each driveid in the existing driveIDsArray
Item databaseItem;
foreach (searchDriveId; driveIDsArray) {
log.vdebug("Searching database for: ", searchDriveId, ", ", sharedFolderName);
if (itemdb.selectByPath(sharedFolderName, searchDriveId, databaseItem)) {
log.vdebug("Found shared folder name in database");
itemInDatabase = true;
log.vdebug("databaseItem: ", databaseItem);
// Does the databaseItem.driveId == defaultDriveId?
if (databaseItem.driveId == defaultDriveId) {
itemPathIsLocal = true;
}
} else {
log.vdebug("Shared folder name not found in database");
// "what if" there is 'already' a local folder with this name
// Check if in the database
// If NOT in the database, but resides on disk, this could be a new local folder created after last sync but before this one
// However we sync 'shared folders' before checking for local changes
string localpath = expandTilde(cfg.getValueString("sync_dir")) ~ "/" ~ sharedFolderName;
if (exists(localpath)) {
// local path exists
log.vdebug("Found shared folder name in local OneDrive sync_dir");
itemLocalDirExists = true;
if ("email" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) {
sharedByEmail = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["email"].str;
}
}
// is the shared item with us a 'folder' ?
if (isItemFolder(searchResult)) {
// item returned is a shared folder, not a shared file
sharedFolderName = searchResult["name"].str;
// Compare this to values in business_shared_folders
if(selectiveSync.isSharedFolderMatched(sharedFolderName)){
// Folder name matches what we are looking for
// Flags for matching
bool itemInDatabase = false;
bool itemLocalDirExists = false;
bool itemPathIsLocal = false;
isExternalTenant = false;
// "what if" there are 2 or more folders shared with me have the "same" name?
// The folder name will be the same, but driveId will be different
// This will then cause these 'shared folders' to cross populate data, which may not be desirable
log.vdebug("Shared Folder Name: ", sharedFolderName);
log.vdebug("Parent Drive Id: ", searchResult["remoteItem"]["parentReference"]["driveId"].str);
log.vdebug("Shared Item Id: ", searchResult["remoteItem"]["id"].str);
// Is this OneDrive Shared Folder on an external tenant?
if (searchResult["remoteItem"]["sharepointIds"]["tenantId"].str != myTenantID) {
isExternalTenant = true;
log.vdebug("This shared folder is shared from an external organisation as the tenant is different");
// Have to configure the access to this tenant, which requires separate tokenUrl for that tenant
onedrive.setExternalTenant(searchResult["remoteItem"]["sharepointIds"]["tenantId"].str);
}
// for each driveid in the existing driveIDsArray
Item databaseItem;
foreach (searchDriveId; driveIDsArray) {
log.vdebug("searching database for: ", searchDriveId, " ", sharedFolderName);
if (itemdb.selectByPath(sharedFolderName, searchDriveId, databaseItem)) {
log.vdebug("Found shared folder name in database");
itemInDatabase = true;
log.vdebug("databaseItem: ", databaseItem);
// Does the databaseItem.driveId == defaultDriveId?
if (databaseItem.driveId == defaultDriveId) {
itemPathIsLocal = true;
}
} else {
log.vdebug("Shared folder name not found in database");
// "what if" there is 'already' a local folder with this name
// Check if in the database
// If NOT in the database, but resides on disk, this could be a new local folder created after last sync but before this one
// However we sync 'shared folders' before checking for local changes
string localpath = expandTilde(cfg.getValueString("sync_dir")) ~ "/" ~ sharedFolderName;
if (exists(localpath)) {
// local path exists
log.vdebug("Found shared folder name in local OneDrive sync_dir");
itemLocalDirExists = true;
}
}
}
}
// Shared Folder Evaluation Debugging
log.vdebug("item in database: ", itemInDatabase);
log.vdebug("path exists on disk: ", itemLocalDirExists);
log.vdebug("database drive id matches defaultDriveId: ", itemPathIsLocal);
log.vdebug("database data matches search data: ", ((databaseItem.driveId == searchResult["remoteItem"]["parentReference"]["driveId"].str) && (databaseItem.id == searchResult["remoteItem"]["id"].str)));
// Additional logging
string sharedByName;
string sharedByEmail;
// Extra details for verbose logging
if ("sharedBy" in searchResult["remoteItem"]["shared"]) {
if ("displayName" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) {
sharedByName = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["displayName"].str;
}
if ("email" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) {
sharedByEmail = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["email"].str;
}
}
if ( ((!itemInDatabase) || (!itemLocalDirExists)) || (((databaseItem.driveId == searchResult["remoteItem"]["parentReference"]["driveId"].str) && (databaseItem.id == searchResult["remoteItem"]["id"].str)) && (!itemPathIsLocal)) ) {
// This shared folder does not exist in the database
if (!cfg.getValueBool("monitor")) {
log.log("Syncing this OneDrive Business Shared Folder: ", sharedFolderName);
// Shared Folder Evaluation Debugging
log.vdebug("item in database: ", itemInDatabase);
log.vdebug("path exists on disk: ", itemLocalDirExists);
log.vdebug("database drive id matches defaultDriveId: ", itemPathIsLocal);
log.vdebug("database data matches search data: ", ((databaseItem.driveId == searchResult["remoteItem"]["parentReference"]["driveId"].str) && (databaseItem.id == searchResult["remoteItem"]["id"].str)));
if ( ((!itemInDatabase) || (!itemLocalDirExists)) || (((databaseItem.driveId == searchResult["remoteItem"]["parentReference"]["driveId"].str) && (databaseItem.id == searchResult["remoteItem"]["id"].str)) && (!itemPathIsLocal)) ) {
// This shared folder does not exist in the database
if (!cfg.getValueBool("monitor")) {
log.log("Syncing this OneDrive Business Shared Folder: ", sharedFolderName);
} else {
log.vlog("Syncing this OneDrive Business Shared Folder: ", sharedFolderName);
}
Item businessSharedFolder = makeItem(searchResult);
// Log who shared this to assist with sync data correlation
if ((sharedByName != "") && (sharedByEmail != "")) {
log.vlog("OneDrive Business Shared Folder - Shared By: ", sharedByName, " (", sharedByEmail, ")");
} else {
if (sharedByName != "") {
log.vlog("OneDrive Business Shared Folder - Shared By: ", sharedByName);
}
}
// Do the actual sync
applyDifferences(businessSharedFolder.remoteDriveId, businessSharedFolder.remoteId, performFullItemScan);
// add this parent drive id to the array to search for, ready for next use
string newDriveID = searchResult["remoteItem"]["parentReference"]["driveId"].str;
// Keep the driveIDsArray with unique entries only
if (!canFind(driveIDsArray, newDriveID)) {
// Add this drive id to the array to search with
driveIDsArray ~= newDriveID;
}
} else {
log.vlog("Syncing this OneDrive Business Shared Folder: ", sharedFolderName);
}
Item businessSharedFolder = makeItem(searchResult);
// Shared Folder Name Conflict ...
log.log("WARNING: Skipping shared folder due to existing name conflict: ", sharedFolderName);
log.log("WARNING: Skipping changes of Path ID: ", searchResult["remoteItem"]["id"].str);
log.log("WARNING: To sync this shared folder, this shared folder needs to be renamed");
// Log who shared this to assist with conflict resolution
if ((sharedByName != "") && (sharedByEmail != "")) {
log.vlog("WARNING: Conflict Shared By: ", sharedByName, " (", sharedByEmail, ")");
} else {
if (sharedByName != "") {
log.vlog("WARNING: Conflict Shared By: ", sharedByName);
}
}
}
}
} else {
// not a folder, is this a file?
if (isItemFile(searchResult)) {
// shared item is a file
string sharedFileName = searchResult["name"].str;
// log that this is not supported
log.vlog("WARNING: Not syncing this OneDrive Business Shared File: ", sharedFileName);
// Log who shared this to assist with sync data correlation
if ((sharedByName != "") && (sharedByEmail != "")) {
log.vlog("OneDrive Business Shared Folder - Shared By: ", sharedByName, " (", sharedByEmail, ")");
log.vlog("OneDrive Business Shared File - Shared By: ", sharedByName, " (", sharedByEmail, ")");
} else {
if (sharedByName != "") {
log.vlog("OneDrive Business Shared Folder - Shared By: ", sharedByName);
log.vlog("OneDrive Business Shared File - Shared By: ", sharedByName);
}
}
// Do the actual sync
applyDifferences(businessSharedFolder.remoteDriveId, businessSharedFolder.remoteId, performFullItemScan);
// add this parent drive id to the array to search for, ready for next use
string newDriveID = searchResult["remoteItem"]["parentReference"]["driveId"].str;
// Keep the driveIDsArray with unique entries only
if (!canFind(driveIDsArray, newDriveID)) {
// Add this drive id to the array to search with
driveIDsArray ~= newDriveID;
}
} else {
// Shared Folder Name Conflict ...
log.log("WARNING: Skipping shared folder due to existing name conflict: ", sharedFolderName);
log.log("WARNING: Skipping changes of Path ID: ", searchResult["remoteItem"]["id"].str);
log.log("WARNING: To sync this shared folder, this shared folder needs to be renamed");
// Log who shared this to assist with conflict resolution
if ((sharedByName != "") && (sharedByEmail != "")) {
log.vlog("WARNING: Conflict Shared By: ", sharedByName, " (", sharedByEmail, ")");
} else {
if (sharedByName != "") {
log.vlog("WARNING: Conflict Shared By: ", sharedByName);
}
}
// something else entirely
log.log("WARNING: Not syncing this OneDrive Business Shared item: ", searchResult["name"].str);
}
}
// Was this shared folder on an external tenant?
if (isExternalTenant) {
// clear external tenant
onedrive.clearExternalTenant();
}
}
}
} else {
// Log that an invalid JSON object was returned
@ -2005,25 +2034,31 @@ final class SyncEngine
// - full path + combination of any above two - /path/name*.txt
// - full path to file - /path/to/file.txt
// need to compute the full path for this file
path = itemdb.computePath(item.driveId, item.parentId) ~ "/" ~ item.name;
// The path that needs to be checked needs to include the '/'
// This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched
if (!startsWith(path, "/")){
// Add '/' to the path
path = '/' ~ path;
// is the parent id in the database?
if (itemdb.idInLocalDatabase(item.driveId, item.parentId)){
// need to compute the full path for this file
path = itemdb.computePath(item.driveId, item.parentId) ~ "/" ~ item.name;
// The path that needs to be checked needs to include the '/'
// This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched
if (!startsWith(path, "/")){
// Add '/' to the path
path = '/' ~ path;
}
log.vdebug("skip_file item to check: ", path);
unwanted = selectiveSync.isFileNameExcluded(path);
log.vdebug("Result: ", unwanted);
if (unwanted) log.vlog("Skipping item - excluded by skip_file config: ", item.name);
} else {
// parent id is not in the database
unwanted = true;
log.vlog("Skipping file - parent path not present in local database");
}
log.vdebug("skip_file item to check: ", path);
unwanted = selectiveSync.isFileNameExcluded(path);
log.vdebug("Result: ", unwanted);
if (unwanted) log.vlog("Skipping item - excluded by skip_file config: ", item.name);
}
}
// check the item type
if (!unwanted) {
if (isItemFile(driveItem)) {
log.vdebug("The item we are syncing is a file");
@ -2149,25 +2184,37 @@ final class SyncEngine
if (cached && item.eTag != oldItem.eTag) {
// Is the item in the local database
if (itemdb.idInLocalDatabase(item.driveId, item.id)){
log.vdebug("OneDrive item ID is present in local database");
oldPath = itemdb.computePath(item.driveId, item.id);
if (!isItemSynced(oldItem, oldPath)) {
// Query DB for existing local item in specified path
string itemSource = "database";
if (!isItemSynced(oldItem, oldPath, itemSource)) {
if (exists(oldPath)) {
// Is the local file technically 'newer' based on UTC timestamp?
SysTime localModifiedTime = timeLastModified(oldPath).toUTC();
localModifiedTime.fracSecs = Duration.zero;
item.mtime.fracSecs = Duration.zero;
if (localModifiedTime > item.mtime) {
// local file is newer than item on OneDrive
// debug the output of time comparison
log.vdebug("localModifiedTime (local file): ", localModifiedTime);
log.vdebug("item.mtime (OneDrive item): ", item.mtime);
// Compare file on disk modified time with modified time provided by OneDrive API
if (localModifiedTime >= item.mtime) {
// local file is newer or has the same time than the item on OneDrive
log.vdebug("Skipping OneDrive change as this is determined to be unwanted due to local item modified time being newer or equal to item modified time from OneDrive");
// no local rename
// no download needed
log.vlog("Local item modified time is newer based on UTC time conversion - keeping local item");
log.vdebug("Skipping OneDrive change as this is determined to be unwanted due to local item modified time being newer than OneDrive item");
if (localModifiedTime == item.mtime) {
log.vlog("Local item modified time is equal to OneDrive item modified time based on UTC time conversion - keeping local item");
} else {
log.vlog("Local item modified time is newer than OneDrive item modified time based on UTC time conversion - keeping local item");
}
skippedItems ~= item.id;
return;
} else {
// remote file is newer than local item
log.vlog("Remote item modified time is newer based on UTC time conversion");
log.vlog("Remote item modified time is newer based on UTC time conversion"); // correct message, remote item is newer
auto ext = extension(oldPath);
auto newPath = path.chomp(ext) ~ "-" ~ deviceName ~ ext;
@ -2225,6 +2272,14 @@ final class SyncEngine
}
// What was the item that was saved
log.vdebug("item details: ", item);
} else {
// flag was tripped, which was it
if (downloadFailed) {
log.vdebug("Download or creation of local directory failed");
}
if (malwareDetected) {
log.vdebug("OneDrive reported that file contained malware");
}
}
}
@ -2233,7 +2288,9 @@ final class SyncEngine
{
if (exists(path)) {
// path exists locally
if (isItemSynced(item, path)) {
// Query DB for new remote item in specified path
string itemSource = "remote";
if (isItemSynced(item, path, itemSource)) {
// file details from OneDrive and local file details in database are in-sync
log.vdebug("The item to sync is already present on the local file system and is in-sync with the local database");
return;
@ -2243,8 +2300,8 @@ final class SyncEngine
SysTime localModifiedTime = timeLastModified(path).toUTC();
SysTime itemModifiedTime = item.mtime;
// HACK: reduce time resolution to seconds before comparing
itemModifiedTime.fracSecs = Duration.zero;
localModifiedTime.fracSecs = Duration.zero;
itemModifiedTime.fracSecs = Duration.zero;
// is the local modified time greater than that from OneDrive?
if (localModifiedTime > itemModifiedTime) {
@ -2306,7 +2363,10 @@ final class SyncEngine
}
} else {
// remote file is newer than local item
log.vlog("Remote item modified time is newer based on UTC time conversion");
log.vlog("Remote item modified time is newer based on UTC time conversion"); // correct message, remote item is newer
log.vdebug("localModifiedTime (local file): ", localModifiedTime);
log.vdebug("itemModifiedTime (OneDrive item): ", itemModifiedTime);
auto ext = extension(path);
auto newPath = path.chomp(ext) ~ "-" ~ deviceName ~ ext;
@ -2405,7 +2465,23 @@ final class SyncEngine
}
if (!dryRun) {
mkdirRecurse(path);
try {
// Does the path exist locally?
if (!exists(path)) {
// Create the new directory
log.vdebug("Requested path does not exist, creating directory structure: ", path);
mkdirRecurse(path);
// Configure the applicable permissions for the folder
log.vdebug("Setting directory permissions for: ", path);
path.setAttributes(cfg.returnRequiredDirectoryPermisions());
}
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg);
// flag that this failed
downloadFailed = true;
return;
}
} else {
// we dont create the directory, but we need to track that we 'faked it'
idsFaked ~= [item.driveId, item.id];
@ -2431,7 +2507,9 @@ final class SyncEngine
if (exists(newPath)) {
Item localNewItem;
if (itemdb.selectByPath(newPath, defaultDriveId, localNewItem)) {
if (isItemSynced(localNewItem, newPath)) {
// Query DB for new local item in specified path
string itemSource = "database";
if (isItemSynced(localNewItem, newPath, itemSource)) {
log.vlog("Destination is in sync and will be overwritten");
} else {
// TODO: force remote sync by deleting local item
@ -2457,6 +2535,7 @@ final class SyncEngine
// handle changed time
if (newItem.type == ItemType.file && oldItem.mtime != newItem.mtime) {
try {
log.vdebug("Calling setTimes() for this file: ", newPath);
setTimes(newPath, newItem.mtime, newItem.mtime);
} catch (FileException e) {
// display the error message
@ -2542,7 +2621,6 @@ final class SyncEngine
log.vdebug("onedrive.downloadById(item.driveId, item.id, path, fileSize); generated a OneDriveException");
// 408 = Request Time Out
// 429 = Too Many Requests - need to delay
if (e.httpStatusCode == 408) {
// 408 error handling - request time out
// https://github.com/abraunegg/onedrive/issues/694
@ -2614,6 +2692,12 @@ final class SyncEngine
}
}
}
} catch (FileException e) {
// There was a file system error
// display the error message
displayFileSystemErrorMessage(e.msg);
downloadFailed = true;
return;
} catch (std.exception.ErrnoException e) {
// There was a file system error
// display the error message
@ -2632,6 +2716,7 @@ final class SyncEngine
// downloaded matches either size or hash
log.vdebug("Downloaded file matches reported size and or reported file hash");
try {
log.vdebug("Calling setTimes() for this file: ", path);
setTimes(path, item.mtime, item.mtime);
} catch (FileException e) {
// display the error message
@ -2667,7 +2752,7 @@ final class SyncEngine
}
// returns true if the given item corresponds to the local one
private bool isItemSynced(const ref Item item, const(string) path)
private bool isItemSynced(const ref Item item, const(string) path, string itemSource)
{
if (!exists(path)) return false;
final switch (item.type) {
@ -2676,17 +2761,17 @@ final class SyncEngine
SysTime localModifiedTime = timeLastModified(path).toUTC();
SysTime itemModifiedTime = item.mtime;
// HACK: reduce time resolution to seconds before comparing
itemModifiedTime.fracSecs = Duration.zero;
localModifiedTime.fracSecs = Duration.zero;
itemModifiedTime.fracSecs = Duration.zero;
if (localModifiedTime == itemModifiedTime) {
return true;
} else {
log.vlog("The local item has a different modified time ", localModifiedTime, " remote is ", itemModifiedTime);
log.vlog("The local item has a different modified time ", localModifiedTime, " when compared to ", itemSource, " modified time ", itemModifiedTime);
}
if (testFileHash(path, item)) {
return true;
} else {
log.vlog("The local item has a different hash");
log.vlog("The local item has a different hash when compared to ", itemSource, " item hash");
}
} else {
log.vlog("The local item is a directory but should be a file");
@ -2778,6 +2863,16 @@ final class SyncEngine
logPath = path;
}
// If we are using --upload-only & --sync-shared-folders there is a possability that a 'new' local folder might
// be misinterpreted that it needs to be uploaded to the users default OneDrive DriveID rather than the requested / configured
// Shared Business Folder. In --resync scenarios, the DB information that tells that this Business Shared Folder does not exist,
// and in a --upload-only scenario will never exist, so the correct lookups are unable to be performed.
if ((exists(cfg.businessSharedFolderFilePath)) && (syncBusinessFolders) && (cfg.getValueBool("upload_only"))){
// business_shared_folders file exists, --sync-shared-folders is enabled, --upload-only is enabled
log.vdebug("OneDrive Business --upload-only & --sync-shared-folders edge case triggered");
handleUploadOnlyBusinessSharedFoldersEdgeCase();
}
// Are we configured to use a National Cloud Deployment
// Any entry in the DB than is flagged as out-of-sync needs to be cleaned up locally first before we scan the entire DB
// Normally, this is done at the end of processing all /delta queries, but National Cloud Deployments (US and DE) do not support /delta as a query
@ -2846,6 +2941,16 @@ final class SyncEngine
logPath = path;
}
// If we are using --upload-only & --sync-shared-folders there is a possability that a 'new' local folder might
// be misinterpreted that it needs to be uploaded to the users default OneDrive DriveID rather than the requested / configured
// Shared Business Folder. In --resync scenarios, the DB information that tells that this Business Shared Folder does not exist,
// and in a --upload-only scenario will never exist, so the correct lookups are unable to be performed.
if ((exists(cfg.businessSharedFolderFilePath)) && (syncBusinessFolders) && (cfg.getValueBool("upload_only"))){
// business_shared_folders file exists, --sync-shared-folders is enabled, --upload-only is enabled
log.vdebug("OneDrive Business --upload-only & --sync-shared-folders edge case triggered");
handleUploadOnlyBusinessSharedFoldersEdgeCase();
}
// Are we configured to use a National Cloud Deployment
// Any entry in the DB than is flagged as out-of-sync needs to be cleaned up locally first before we scan the entire DB
// Normally, this is done at the end of processing all /delta queries, but National Cloud Deployments (US and DE) do not support /delta as a query
@ -2892,6 +2997,87 @@ final class SyncEngine
}
}
void handleUploadOnlyBusinessSharedFoldersEdgeCase() {
// read in the business_shared_folders file contents
string[] businessSharedFoldersList;
// open file as read only
auto file = File(cfg.businessSharedFolderFilePath, "r");
auto range = file.byLine();
foreach (line; range) {
// Skip comments in file
if (line.length == 0 || line[0] == ';' || line[0] == '#') continue;
businessSharedFoldersList ~= buildNormalizedPath(line);
}
file.close();
// Query the GET /me/drive/sharedWithMe API
JSONValue graphQuery = onedrive.getSharedWithMe();
if (graphQuery.type() == JSONType.object) {
if (count(graphQuery["value"].array) != 0) {
// Shared items returned
log.vdebug("onedrive.getSharedWithMe API Response: ", graphQuery);
foreach (searchResult; graphQuery["value"].array) {
// loop variables
string sharedFolderName;
string remoteParentDriveId;
string remoteParentItemId;
Item remoteItemRoot;
Item remoteItem;
// is the shared item with us a 'folder' ?
// we only handle folders, not files or other items
if (isItemFolder(searchResult)) {
// Debug response output
log.vdebug("shared folder entry: ", searchResult);
sharedFolderName = searchResult["name"].str;
remoteParentDriveId = searchResult["remoteItem"]["parentReference"]["driveId"].str;
remoteParentItemId = searchResult["remoteItem"]["parentReference"]["id"].str;
if (canFind(businessSharedFoldersList, sharedFolderName)) {
// Shared Folder matches what is in the shared folder list
log.vdebug("shared folder name matches business_shared_folders list item: ", sharedFolderName);
// Actions:
// 1. Add this remote item to the DB so that it can be queried
// 2. Add remoteParentDriveId to driveIDsArray so we have a record of it
// Make JSON item DB compatible
remoteItem = makeItem(searchResult);
// Fix up entries, as we are manipulating the data
remoteItem.driveId = remoteParentDriveId;
remoteItem.eTag = "";
remoteItem.cTag = "";
remoteItem.parentId = defaultRootId;
remoteItem.remoteDriveId = "";
remoteItem.remoteId = "";
// Build the remote root DB item
remoteItemRoot.driveId = remoteParentDriveId;
remoteItemRoot.id = defaultRootId;
remoteItemRoot.name = "root";
remoteItemRoot.type = ItemType.dir;
remoteItemRoot.mtime = remoteItem.mtime;
remoteItemRoot.syncStatus = "Y";
// Add root remote item to the local database
log.vdebug("Adding remote folder root to database: ", remoteItemRoot);
itemdb.upsert(remoteItemRoot);
// Add shared folder item to the local database
log.vdebug("Adding remote folder to database: ", remoteItem);
itemdb.upsert(remoteItem);
// Keep the driveIDsArray with unique entries only
if (!canFind(driveIDsArray, remoteParentDriveId)) {
// Add this drive id to the array to search with
driveIDsArray ~= remoteParentDriveId;
}
}
}
}
}
}
}
// scan the given directory for new items - for use with --monitor
void scanForDifferencesFilesystemScan(const(string) path)
{
@ -2933,9 +3119,18 @@ final class SyncEngine
// Is this item excluded by user configuration of skip_dir or skip_file?
// Is this item a directory or 'remote' type? A 'remote' type is a folder DB tie so should be compared as directory for exclusion
if ((item.type == ItemType.dir)||(item.type == ItemType.remote)) {
// Do we need to check for .nosync? Only if --check-for-nosync was passed in
if (cfg.getValueBool("check_nosync")) {
if (exists(path ~ "/.nosync")) {
log.vlog("Skipping item - .nosync found & --check-for-nosync enabled: ", path);
return;
}
}
// Is the path excluded?
unwanted = selectiveSync.isDirNameExcluded(item.name);
}
// Is this item a file?
if (item.type == ItemType.file) {
// Is the filename excluded?
@ -3912,13 +4107,36 @@ final class SyncEngine
} else {
// parent is in database
log.vlog("The parent for this path is in the local database - adding requested path (", path ,") to database");
// are we in a --dry-run scenario?
if (!dryRun) {
// get the live data
auto res = onedrive.getPathDetails(path);
JSONValue pathDetails;
try {
pathDetails = onedrive.getPathDetailsByDriveId(parent.driveId, path);
} catch (OneDriveException e) {
log.vdebug("pathDetails = onedrive.getPathDetailsByDriveId(parent.driveId, path) generated a OneDriveException");
if (e.httpStatusCode == 404) {
// The directory was not found
log.error("ERROR: The requested single directory to sync was not found on OneDrive");
return;
}
if (e.httpStatusCode == 429) {
// HTTP request returned status code 429 (Too Many Requests). We need to leverage the response Retry-After HTTP header to ensure minimum delay until the throttle is removed.
handleOneDriveThrottleRequest();
// Retry original request by calling function again to avoid replicating any further error handling
log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - calling onedrive.getPathDetailsByDriveId(parent.driveId, path);");
pathDetails = onedrive.getPathDetailsByDriveId(parent.driveId, path);
}
if (e.httpStatusCode >= 500) {
// OneDrive returned a 'HTTP 5xx Server Side Error' - gracefully handling error - error message already logged
return;
}
}
// Is the response a valid JSON object - validation checking done in saveItem
saveItem(res);
saveItem(pathDetails);
} else {
// need to fake this data
auto fakeResponse = createFakeResponse(path);
@ -4019,9 +4237,10 @@ final class SyncEngine
// Does this 'file' already exist on OneDrive?
try {
// test if the local path exists on OneDrive
fileDetailsFromOneDrive = onedrive.getPathDetails(path);
fileDetailsFromOneDrive = onedrive.getPathDetailsByDriveId(parent.driveId, path);
} catch (OneDriveException e) {
log.vdebug("fileDetailsFromOneDrive = onedrive.getPathDetails(path); generated a OneDriveException");
// A 404 is the expected response if the file was not present
log.vdebug("fileDetailsFromOneDrive = onedrive.getPathDetailsByDriveId(parent.driveId, path); generated a OneDriveException");
if (e.httpStatusCode == 401) {
// OneDrive returned a 'HTTP/1.1 401 Unauthorized Error'
log.vlog("Skipping item - OneDrive returned a 'HTTP 401 - Unauthorized' when attempting to query if file exists");
@ -5507,8 +5726,31 @@ final class SyncEngine
// 6. name
// 7. parent reference
string fakeDriveId = defaultDriveId;
string fakeRootId = defaultRootId;
SysTime mtime = timeLastModified(path).toUTC();
// If the account type is Business, and if Shared Business Folders are being used
// Need to update the 'fakeDriveId' & 'fakeRootId' with elements from the database
// Otherwise some calls to validate objects fail as the actual driveId being used is invalid
if (accountType == "business") {
string parentPath = dirName(path);
Item databaseItem;
if (parentPath != ".") {
// Not a 'root' parent
// For each driveid in the existing driveIDsArray
foreach (searchDriveId; driveIDsArray) {
log.vdebug("FakeResponse: searching database for: ", searchDriveId, " ", parentPath);
if (itemdb.selectByPath(parentPath, searchDriveId, databaseItem)) {
log.vdebug("FakeResponse: Found Database Item: ", databaseItem);
fakeDriveId = databaseItem.driveId;
fakeRootId = databaseItem.id;
}
}
}
}
// real id / eTag / cTag are different format for personal / business account
auto sha1 = new SHA1Digest();
ubyte[] hash1 = sha1.digest(path);
@ -5527,9 +5769,9 @@ final class SyncEngine
]),
"name": JSONValue(baseName(path)),
"parentReference": JSONValue([
"driveId": JSONValue(defaultDriveId),
"driveId": JSONValue(fakeDriveId),
"driveType": JSONValue(accountType),
"id": JSONValue(defaultRootId)
"id": JSONValue(fakeRootId)
]),
"folder": JSONValue("")
];
@ -5548,9 +5790,9 @@ final class SyncEngine
]),
"name": JSONValue(baseName(path)),
"parentReference": JSONValue([
"driveId": JSONValue(defaultDriveId),
"driveId": JSONValue(fakeDriveId),
"driveType": JSONValue(accountType),
"id": JSONValue(defaultRootId)
"id": JSONValue(fakeRootId)
]),
"file": JSONValue([
"hashes":JSONValue([
@ -5920,44 +6162,49 @@ final class SyncEngine
string sharedFolderName;
string sharedByName;
string sharedByEmail;
// Debug response output
log.vdebug("shared folder entry: ", searchResult);
sharedFolderName = searchResult["name"].str;
if ("sharedBy" in searchResult["remoteItem"]["shared"]) {
// we have shared by details we can use
if ("displayName" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) {
sharedByName = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["displayName"].str;
// is the shared item with us a 'folder' ?
// we only handle folders, not files or other items
if (isItemFolder(searchResult)) {
// Debug response output
log.vdebug("shared folder entry: ", searchResult);
sharedFolderName = searchResult["name"].str;
// configure who this was shared by
if ("sharedBy" in searchResult["remoteItem"]["shared"]) {
// we have shared by details we can use
if ("displayName" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) {
sharedByName = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["displayName"].str;
}
if ("email" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) {
sharedByEmail = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["email"].str;
}
}
if ("email" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) {
sharedByEmail = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["email"].str;
// Output query result
log.log("---------------------------------------");
// Default output
log.log("Shared Folder: ", sharedFolderName);
if ((sharedByName != "") && (sharedByEmail != "")) {
log.log("Shared By: ", sharedByName, " (", sharedByEmail, ")");
} else {
if (sharedByName != "") {
log.log("Shared By: ", sharedByName);
}
}
}
// Output query result
log.log("---------------------------------------");
// Default output
log.log("Shared Folder: ", sharedFolderName);
if ((sharedByName != "") && (sharedByEmail != "")) {
log.log("Shared By: ", sharedByName, " (", sharedByEmail, ")");
} else {
if (sharedByName != "") {
log.log("Shared By: ", sharedByName);
// Tenant details
if (searchResult["remoteItem"]["sharepointIds"]["tenantId"].str == myTenantID) {
log.log("External Organisation: no");
} else {
log.log("External Organisation: yes");
}
// Extra verbose output
log.vlog("Item Id: ", searchResult["remoteItem"]["id"].str);
log.vlog("Parent Drive Id: ", searchResult["remoteItem"]["parentReference"]["driveId"].str);
if ("id" in searchResult["remoteItem"]["parentReference"]) {
log.vlog("Parent Item Id: ", searchResult["remoteItem"]["parentReference"]["id"].str);
}
log.vlog("Tenant ID: ", searchResult["remoteItem"]["sharepointIds"]["tenantId"].str);
}
if (searchResult["remoteItem"]["sharepointIds"]["tenantId"].str == myTenantID) {
log.log("External Organisation: no");
} else {
log.log("External Organisation: yes");
}
// Extra verbose output
log.vlog("Item Id: ", searchResult["remoteItem"]["id"].str);
log.vlog("Parent Drive Id: ", searchResult["remoteItem"]["parentReference"]["driveId"].str);
if ("id" in searchResult["remoteItem"]["parentReference"]) {
log.vlog("Parent Item Id: ", searchResult["remoteItem"]["parentReference"]["id"].str);
}
log.vlog("Tenant ID: ", searchResult["remoteItem"]["sharepointIds"]["tenantId"].str);
}
}
write("\n");