From e3d360bd0b8533e86c5e54be8155c4e566b1b209 Mon Sep 17 00:00:00 2001 From: abraunegg Date: Tue, 9 Jan 2024 08:14:11 +1100 Subject: [PATCH] Repository Cleanup * OneDrive Client Version v2.5.0-alpha-5 Cleanup --- CHANGELOG.md | 1034 -- LICENSE | 674 - Makefile.in | 160 - README.md | 92 - aclocal.m4 | 290 - config | 61 - configure | 3785 ----- configure.ac | 288 - contrib/completions/complete.bash | 49 - contrib/completions/complete.fish | 38 - contrib/completions/complete.zsh | 48 - contrib/docker/Dockerfile | 41 - contrib/docker/Dockerfile-alpine | 38 - contrib/docker/Dockerfile-debian | 36 - contrib/docker/entrypoint.sh | 132 - contrib/docker/hooks/post_push | 6 - contrib/gentoo/onedrive-2.4.25.ebuild | 31 - contrib/init.d/onedrive.init | 82 - contrib/init.d/onedrive_service.sh | 5 - contrib/logrotate/onedrive.logrotate | 26 - contrib/pacman/PKGBUILD.in | 30 - contrib/spec/onedrive.spec.in | 80 - contrib/systemd/onedrive.service.in | 27 - contrib/systemd/onedrive@.service.in | 27 - docs/BusinessSharedFolders.md | 192 - docs/Docker.md | 396 - docs/INSTALL.md | 277 - docs/Podman.md | 360 - docs/SharePoint-Shared-Libraries.md | 228 - docs/USAGE.md | 1469 -- docs/advanced-usage.md | 302 - docs/application-security.md | 97 - docs/build-rpm-howto.md | 379 - docs/images/SharedLibraries.jpg | Bin 17382 -> 0 bytes docs/images/application_registration.jpg | Bin 66392 -> 0 bytes docs/images/application_registration_done.jpg | Bin 73999 -> 0 bytes docs/images/authentication_response_uri.jpg | Bin 90909 -> 0 bytes docs/images/authentication_scopes.jpg | Bin 87986 -> 0 bytes docs/images/business-files-on-demand.png | Bin 15128 -> 0 bytes docs/images/confirmed_verified_publisher.jpg | Bin 45819 -> 0 bytes docs/images/default_authentication_scopes.jpg | Bin 88294 -> 0 bytes docs/images/personal-files-on-demand.png | Bin 17670 -> 0 bytes docs/images/shared_with_me.JPG | Bin 46012 -> 0 bytes docs/known-issues.md | 54 - docs/national-cloud-deployments.md | 145 - docs/privacy-policy.md | 65 - docs/terms-of-service.md | 54 - docs/ubuntu-package-install.md | 414 - install-sh | 518 - onedrive.1.in | 391 - src/arsd/README.md | 8 - src/arsd/cgi.d | 11810 ---------------- src/config.d | 901 -- src/itemdb.d | 525 - src/log.d | 239 - src/main.d | 2094 --- src/monitor.d | 391 - src/notifications/README | 10 - src/notifications/dnotify.d | 323 - src/notifications/notify.d | 195 - src/onedrive.d | 1887 --- src/progress.d | 156 - src/qxor.d | 88 - src/selective.d | 422 - src/sqlite.d | 256 - src/sync.d | 7302 ---------- src/upload.d | 302 - src/util.d | 609 - tests/bad-file-name.tar.xz | Bin 499880 -> 0 bytes tests/makefiles.sh | 61 - 70 files changed, 40000 deletions(-) delete mode 100644 CHANGELOG.md delete mode 100644 LICENSE delete mode 100644 Makefile.in delete mode 100644 README.md delete mode 100644 aclocal.m4 delete mode 100644 config delete mode 100755 configure delete mode 100644 configure.ac delete mode 100644 contrib/completions/complete.bash delete mode 100644 contrib/completions/complete.fish delete mode 100644 contrib/completions/complete.zsh delete mode 100644 contrib/docker/Dockerfile delete mode 100644 contrib/docker/Dockerfile-alpine delete mode 100644 contrib/docker/Dockerfile-debian delete mode 100755 contrib/docker/entrypoint.sh delete mode 100755 contrib/docker/hooks/post_push delete mode 100644 contrib/gentoo/onedrive-2.4.25.ebuild delete mode 100644 contrib/init.d/onedrive.init delete mode 100644 contrib/init.d/onedrive_service.sh delete mode 100644 contrib/logrotate/onedrive.logrotate delete mode 100644 contrib/pacman/PKGBUILD.in delete mode 100644 contrib/spec/onedrive.spec.in delete mode 100644 contrib/systemd/onedrive.service.in delete mode 100644 contrib/systemd/onedrive@.service.in delete mode 100644 docs/BusinessSharedFolders.md delete mode 100644 docs/Docker.md delete mode 100644 docs/INSTALL.md delete mode 100644 docs/Podman.md delete mode 100644 docs/SharePoint-Shared-Libraries.md delete mode 100644 docs/USAGE.md delete mode 100644 docs/advanced-usage.md delete mode 100644 docs/application-security.md delete mode 100644 docs/build-rpm-howto.md delete mode 100644 docs/images/SharedLibraries.jpg delete mode 100644 docs/images/application_registration.jpg delete mode 100644 docs/images/application_registration_done.jpg delete mode 100644 docs/images/authentication_response_uri.jpg delete mode 100644 docs/images/authentication_scopes.jpg delete mode 100644 docs/images/business-files-on-demand.png delete mode 100644 docs/images/confirmed_verified_publisher.jpg delete mode 100644 docs/images/default_authentication_scopes.jpg delete mode 100644 docs/images/personal-files-on-demand.png delete mode 100644 docs/images/shared_with_me.JPG delete mode 100644 docs/known-issues.md delete mode 100644 docs/national-cloud-deployments.md delete mode 100644 docs/privacy-policy.md delete mode 100644 docs/terms-of-service.md delete mode 100644 docs/ubuntu-package-install.md delete mode 100755 install-sh delete mode 100644 onedrive.1.in delete mode 100644 src/arsd/README.md delete mode 100644 src/arsd/cgi.d delete mode 100644 src/config.d delete mode 100644 src/itemdb.d delete mode 100644 src/log.d delete mode 100644 src/main.d delete mode 100644 src/monitor.d delete mode 100644 src/notifications/README delete mode 100644 src/notifications/dnotify.d delete mode 100644 src/notifications/notify.d delete mode 100644 src/onedrive.d delete mode 100644 src/progress.d delete mode 100644 src/qxor.d delete mode 100644 src/selective.d delete mode 100644 src/sqlite.d delete mode 100644 src/sync.d delete mode 100644 src/upload.d delete mode 100644 src/util.d delete mode 100644 tests/bad-file-name.tar.xz delete mode 100644 tests/makefiles.sh diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index a6d2d3f1..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,1034 +0,0 @@ -# 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.25 - 2023-06-21 -### Fixed -* Fixed that the application was reporting as v2.2.24 when in fact it was v2.4.24 (release tagging issue) -* Fixed that the running version obsolete flag (due to above issue) was causing a false flag as being obsolete -* Fixed that zero-byte files do not have a hash as reported by the OneDrive API thus should not generate an error message - -### Updated -* Update to Debian Docker file to resolve Docker image Operating System reported vulnerabilities -* Update to Alpine Docker file to resolve Docker image Operating System reported vulnerabilities -* Update to Fedora Docker file to resolve Docker image Operating System reported vulnerabilities -* Updated documentation (various) - -## 2.4.24 - 2023-06-20 -### Fixed -* Fix for extra encoded quotation marks surrounding Docker environment variables -* Fix webhook subscription creation for SharePoint Libraries -* Fix that a HTTP 504 - Gateway Timeout causes local files to be deleted when using --download-only & --cleanup-local-files mode -* Fix that folders are renamed despite using --dry-run -* Fix deprecation warnings with dmd 2.103.0 -* Fix error that the application is unable to perform a database vacuum: out of memory when exiting - -### Removed -* Remove sha1 from being used by the client as this is being depreciated by Microsoft in July 2023 -* Complete the removal of crc32 elements - -### Added -* Added ONEDRIVE_SINGLE_DIRECTORY configuration capability to Docker -* Added --get-file-link shell completion -* Added configuration to allow HTTP session timeout(s) tuning via config (taken from v2.5.x) - -### Updated -* Update to Debian Docker file to resolve Docker image Operating System reported vulnerabilities -* Update to Alpine Docker file to resolve Docker image Operating System reported vulnerabilities -* Update to Fedora Docker file to resolve Docker image Operating System reported vulnerabilities -* Updated cgi.d to commit 680003a - last upstream change before requiring `core.d` dependency requirement -* Updated documentation (various) - -## 2.4.23 - 2023-01-06 -### Fixed -* Fixed RHEL7, RHEL8 and RHEL9 Makefile and SPEC file compatibility - -### Removed -* Disable systemd 'PrivateUsers' due to issues with systemd running processes when option is enabled, causes local file deletes on RHEL based systems - -### Updated -* Update --get-O365-drive-id error handling to display a more a more appropriate error message if the API cannot be found -* Update the GitHub version check to utilise the date a release was done, to allow 1 month grace period before generating obsolete version message -* Update Alpine Dockerfile to use Alpine 3.17 and Golang 1.19 -* Update handling of --source-directory and --destination-directory if one is empty or missing and if used with --synchronize or --monitor -* Updated documentation (various) - -## 2.4.22 - 2022-12-06 -### Fixed -* Fix application crash when local file is changed to a symbolic link with non-existent target -* Fix build error with dmd-2.101.0 -* Fix build error with LDC 1.28.1 on Alpine -* Fix issue of silent exit when unable to delete local files when using --cleanup-local-files -* Fix application crash due to access permissions on configured path for sync_dir -* Fix potential application crash when exiting due to failure state and unable to cleanly shutdown the database -* Fix creation of parent empty directories when parent is excluded by sync_list - -### Added -* Added performance output details for key functions - -### Changed -* Switch Docker 'latest' to point at Debian builds rather than Fedora due to ongoing Fedora build failures -* Align application logging events to actual application defaults for --monitor operations -* Performance Improvement: Avoid duplicate costly path calculations and DB operations if not required -* Disable non-working remaining sandboxing options within systemd service files -* Performance Improvement: Only check 'sync_list' if this has been enabled and configured -* Display 'Sync with OneDrive is complete' when using --synchronize -* Change the order of processing between Microsoft OneDrive restrictions and limitations check and skip_file|skip_dir check - -### Removed -* Remove building Fedora ARMv7 builds due to ongoing build failures - -### Updated -* Update config change detection handling -* Updated documentation (various) - -## 2.4.21 - 2022-09-27 -### Fixed -* Fix that the download progress bar doesn't always reach 100% when rate_limit is set -* Fix --resync handling of database file removal -* Fix Makefile to be consistent with permissions that are being used -* Fix that logging output for skipped uploaded files is missing -* Fix to allow non-sync tasks while sync is running -* Fix where --resync is enforced for non-sync operations -* Fix to resolve segfault when running 'onedrive --display-sync-status' when run as 2nd process -* Fix DMD 2.100.2 depreciation warning - -### Added -* Add GitHub Action Test Build Workflow (replacing Travis CI) -* Add option --display-running-config to display the running configuration as used at application startup -* Add 'config' option to request readonly access in oauth authorization step -* Add option --cleanup-local-files to cleanup local files regardless of sync state when using --download-only -* Add option --with-editing-perms to create a read-write shareable link when used with --create-share-link - -### Changed -* Change the exit code of the application to 126 when a --resync is required - -### Updated -* Updated --get-O365-drive-id implementation for data access -* Update what application options require an argument -* Update application logging output for error messages to remove certain \n prefix when logging to a file -* Update onedrive.spec.in to fix error building RPM -* Update GUI notification handling for specific skipped scenarios -* Updated documentation (various) - -## 2.4.20 - 2022-07-20 -### Fixed -* Fix 'foreign key constraint failed' when using OneDrive Business Shared Folders due to change to using /delta query -* Fix various little spelling fixes (check with lintian during Debian packaging) -* Fix handling of a custom configuration directory when using --confdir -* Fix to ensure that any active http instance is shutdown before any application exit -* Fix to enforce that --confdir must be a directory - -### Added -* Added 'force_http_11' configuration option to allow forcing HTTP/1.1 operations - -### Changed -* Increased thread sleep for better process I/O wait handling -* Removed 'force_http_2' configuration option - -### Updated -* Update OneDrive API response handling for National Cloud Deployments -* Updated to switch to using curl defaults for HTTP/2 operations -* Updated documentation (various) - -## 2.4.19 - 2022-06-15 -### Fixed -* Update Business Shared Folders to use a /delta query -* Update when DB is updated by OneDrive API data and update when file hash is required to be generated - -### Added -* Added ONEDRIVE_UPLOADONLY flag for Docker - -### Updated -* Updated GitHub workflows -* Updated documentation (various) - -## 2.4.18 - 2022-06-02 -### Fixed -* Fixed various database related access issues steming from running multiple instances of the application at the same time using the same configuration data -* Fixed --display-config being impacted by --resync flag -* Fixed installation permissions for onedrive man-pages file -* Fixed that in some situations that users try --upload-only and --download-only together which is not possible -* Fixed application crash if unable to read required hash files - -### Added -* Added Feature Request to add an override for skip_dir|skip_file through flag to force sync -* Added a check to validate local filesystem available space before attempting file download -* Added GitHub Actions to build Docker containers and push to DockerHub - -### Updated -* Updated all Docker build files to current distributions, using updated distribution LDC version -* Updated logging output to logfiles when an actual sync process is occuring -* Updated output of --display-config to be more relevant -* Updated manpage to align with application configuration -* Updated documentation and Docker files based on minimum compiler versions to dmd-2.088.0 and ldc-1.18.0 -* Updated documentation (various) - -## 2.4.17 - 2022-04-30 -### Fixed -* Fix docker build, by add missing git package for Fedora builds -* Fix application crash when attempting to sync a broken symbolic link -* Fix Internet connect disruption retry handling and logging output -* Fix local folder creation timestamp with timestamp from OneDrive -* Fix logging output when download failed - -### Added -* Add additional logging specifically for delete event to denote in log output the source of a deletion event when running in --monitor mode - -### Changed -* Improve when the local database integrity check is performed and on what frequency the database integrity check is performed - -### Updated -* Remove application output ambiguity on how to access 'help' for the client -* Update logging output when running in --monitor --verbose mode in regards to the inotify events -* Updated documentation (various) - -## 2.4.16 - 2022-03-10 -### Fixed -* Update application file logging error handling -* Explicitly set libcurl options -* Fix that when a sync_list exclusion is matched, the item needs to be excluded when using --resync -* Fix so that application can be compiled correctly on Android hosts -* Fix the handling of 429 and 5xx responses when they are generated by OneDrive in a self-referencing circular pattern -* Fix applying permissions to volume directories when running in rootless podman -* Fix unhandled errors from OneDrive when initialising subscriptions fail - -### Added -* Enable GitHub Sponsors -* Implement --resync-auth to enable CLI passing in of --rsync approval -* Add function to check client version vs latest GitHub release -* Add --reauth to allow easy re-authentication of the client -* Implement --modified-by to display who last modified a file and when the modification was done -* Implement feature request to mark partially-downloaded files as .partial during download -* Add documentation for Podman support - -### Changed -* Document risk regarding using --resync and force user acceptance of usage risk to proceed -* Use YAML for Bug Reports and Feature Requests -* Update Dockerfiles to use more modern base Linux distribution - -### Updated -* Updated documentation (various) - -## 2.4.15 - 2021-12-31 -### Fixed -* Fix unable to upload to OneDrive Business Shared Folders due to OneDrive API restricting quota information -* Update fixing edge case with OneDrive Personal Shared Folders and --resync --upload-only - -### Added -* Add SystemD hardening -* Add --operation-timeout argument - -### Changed -* Updated minimum compiler versions to dmd-2.087.0 and ldc-1.17.0 - -### Updated -* Updated Dockerfile-alpine to use Apline 3.14 -* Updated documentation (various) - -## 2.4.14 - 2021-11-24 -### Fixed -* Support DMD 2.097.0 as compiler for Docker Builds -* Fix getPathDetailsByDriveId query when using --dry-run and a nested path with --single-directory -* Fix edge case when syncing OneDrive Personal Shared Folders -* Catch unhandled API response errors when querying OneDrive Business Shared Folders -* Catch unhandled API response errors when listing OneDrive Business Shared Folders -* Fix error 'Key not found: remaining' with Business Shared Folders (OneDrive API change) -* Fix overwriting local files with older versions from OneDrive when items.sqlite3 does not exist and --resync is not used - -### Added -* Added operation_timeout as a new configuration to assist in cases where operations take longer that 1h to complete -* Add Real-Time syncing of remote updates via webhooks -* Add --auth-response option and expose through entrypoint.sh for Docker -* Add --disable-download-validation - -### Changed -* Always prompt for credentials for authentication rather than re-using cached browser details -* Do not re-auth on --logout - -### Updated -* Updated documentation (various) - -## 2.4.13 - 2021-7-14 -### Fixed -* Support DMD 2.097.0 as compiler -* Fix to handle OneDrive API Bad Request response when querying if file exists -* Fix application crash and incorrect handling of --single-directory when syncing a OneDrive Business Shared Folder due to using 'Add Shortcut to My Files' -* Fix application crash due to invalid UTF-8 sequence in the pathname for the application configuration -* Fix error message when deleting a large number of files -* Fix Docker build process to source GOSU keys from updated GPG key location -* Fix application crash due to a conversion overflow when calculating file offset for session uploads -* Fix Docker Alpine build failing due to filesystem permissions issue due to Docker build system and Alpine Linux 3.14 incompatibility -* Fix that Business Shared Folders with parentheses are ignored - -### Updated -* Updated Lock Bot to run daily -* Updated documentation (various) - -## 2.4.12 - 2021-5-28 -### Fixed -* Fix an unhandled Error 412 when uploading modified files to OneDrive Business Accounts -* Fix 'sync_list' handling of inclusions when name is included in another folders name -* Fix that options --upload-only & --remove-source-files are ignored on an upload session restore -* Fix to add file check when adding item to database if using --upload-only --remove-source-files -* Fix application crash when SharePoint displayName is being withheld - -### Updated -* Updated Lock Bot to use GitHub Actions -* Updated documentation (various) - -## 2.4.11 - 2021-4-07 -### Fixed -* Fix support for '/*' regardless of location within sync_list file -* Fix 429 response handling correctly check for 'retry-after' response header and use set value -* Fix 'sync_list' path handling for sub item matching, so that items in parent are not implicitly matched when there is no wildcard present -* Fix --get-O365-drive-id to use 'nextLink' value if present when searching for specific SharePoint site names -* Fix OneDrive Business Shared Folder existing name conflict check -* Fix incorrect error message 'Item cannot be deleted from OneDrive because it was not found in the local database' when item is actually present -* Fix application crash when unable to rename folder structure due to unhandled file-system issue -* Fix uploading documents to Shared Business Folders when the shared folder exists on a SharePoint site due to Microsoft Sharepoint 'enrichment' of files -* Fix that a file record is kept in database when using --no-remote-delete & --remove-source-files - -### Added -* Added support in --get-O365-drive-id to provide the 'drive_id' for multiple 'document libraries' within a single Shared Library Site - -### Removed -* Removed the depreciated config option 'force_http_11' which was flagged as depreciated by PR #549 in v2.3.6 (June 2019) - -### Updated -* Updated error output of --get-O365-drive-id to provide more details why an error occurred if a SharePoint site lacks the details we need to perform the match -* Updated Docker build files for Raspberry Pi to dedicated armhf & aarch64 Dockerfiles -* Updated logging output when in --monitor mode, avoid outputting misleading logging when the new or modified item is a file, not a directory -* Updated documentation (various) - -## 2.4.10 - 2021-2-19 -### Fixed -* Catch database assertion when item path cannot be calculated -* Fix alpine Docker build so it uses the same golang alpine version -* Search all distinct drive id's rather than just default drive id for --get-file-link -* Use correct driveId value to query for changes when using --single-directory -* Improve upload handling of files for SharePoint sites and detecting when SharePoint modifies the file post upload -* Correctly handle '~' when present in 'log_dir' configuration option -* Fix logging output when handing downloaded new files -* Fix to use correct path offset for sync_list exclusion matching - -### Added -* Add upload speed metrics when files are uploaded and clarify that 'data to transfer' is what is needed to be downloaded from OneDrive -* Add new config option to rate limit connection to OneDrive -* Support new file maximum upload size of 250GB -* Support sync_list matching full path root wildcard with exclusions to simplify sync_list configuration - -### Updated -* Rename Office365.md --> SharePoint-Shared-Libraries.md which better describes this document -* Updated Dockerfile config for arm64 -* Updated documentation (various) - -## 2.4.9 - 2020-12-27 -### Fixed -* Fix to handle case where API provided deltaLink generates a further API error -* Fix application crash when unable to read a local file due to local file permissions -* Fix application crash when calculating the path length due to invalid UTF characters in local path -* Fix Docker build on Alpine due missing symbols due to using the edge version of ldc and ldc-runtime -* Fix application crash with --get-O365-drive-id when API response is restricted - -### Added -* Add debug log output of the configured URL's which will be used throughout the application to remove any ambiguity as to using incorrect URL's when making API calls -* Improve application startup when using --monitor when there is no network connection to the OneDrive API and only initialise application once OneDrive API is reachable -* Add Docker environment variable to allow --logout for re-authentication - -### Updated -* Remove duplicate code for error output functions and enhance error logging output -* Updated documentation - -## 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 -* Fix --single-directory path handling when path does not exist locally -* Fix checking for 'Icon' path as no longer listed by Microsoft as an invalid file or folder name -* Fix removing child items on OneDrive when parent item responds with access denied -* Fix to handle deletion events for files when inotify events are missing -* Fix uninitialised value error as reported by valgrind -* Fix to handle deletion events for directories when inotify events are missing - -### Added -* Implement Feature: Create shareable link -* Implement Feature: Support wildcard within sync_list entries -* Implement Feature: Support negative patterns in sync_list for fine grained exclusions -* Implement Feature: Multiple skip_dir & skip_file configuration rules -* Add GUI notification to advise users when the client needs to be reauthenticated - -### Updated -* Updated documentation (readme.md, install.md, usage.md, bug_report.md) - -## 2.4.5 - 2020-08-13 -### Fixed -* Fixed fish auto completions installation destination - -## 2.4.4 - 2020-08-11 -### Fixed -* Fix 'skip_dir' & 'skip_file' pattern matching to ensure correct matching is performed -* Fix 'skip_dir' & 'skip_file' so that each directive is only used against directories or files as requried in --monitor -* Fix client hand when attempting to sync a Unix pipe file -* Fix --single-directory & 'sync_list' performance -* Fix erroneous 'return' statements which could prematurely end processing all changes returned from OneDrive -* Fix segfault when attempting to perform a comparison on an inotify event when determining if event path is directory or file -* Fix handling of Shared Folders to ensure these are checked against 'skip_dir' entries -* Fix 'Skipping uploading this new file as parent path is not in the database' when uploading to a Personal Shared Folder -* Fix how available free space is tracked when uploading files to OneDrive and Shared Folders -* Fix --single-directory handling of parent path matching if path is being seen for first time - -### Added -* Added Fish auto completions - -### Updated -* Increase maximum individual file size to 100GB due to Microsoft file limit increase -* Update Docker build files and align version of compiler across all Docker builds -* Update Docker documentation -* Update NixOS build information -* Update the 'Processing XXXX' output to display the full path -* Update logging output when a sync starts and completes when using --monitor -* Update Office 365 / SharePoint site search query and response if query return zero match - -## 2.4.3 - 2020-06-29 -### Fixed -* Check if symbolic link is relative to location path -* When using output logfile, fix inconsistent output spacing -* Perform initial sync at startup in monitor mode -* Handle a 'race' condition to process inotify events generated whilst performing DB or filesystem walk -* Fix segfault when moving folder outside the sync directory when using --monitor on Arch Linux - -### Added -* Added additional inotify event debugging -* Added support for loading system configs if there's no user config -* Added Ubuntu installation details to include installing the client from a PPA -* Added openSUSE installation details to include installing the client from a package -* Added support for comments in sync_list file -* Implement recursive deletion when Retention Policy is enabled on OneDrive Business Accounts -* Implement support for National cloud deployments -* Implement OneDrive Business Shared Folders Support - -### Updated -* Updated documentation files (various) -* Updated log output messaging when a full scan has been set or triggered -* Updated buildNormalizedPath complexity to simplify code -* Updated to only process OneDrive Personal Shared Folders only if account type is 'personal' - -## 2.4.2 - 2020-05-27 -### Fixed -* Fixed the catching of an unhandled exception when inotify throws an error -* Fixed an uncaught '100 Continue' response when files are being uploaded -* Fixed progress bar for uploads to be more accurate regarding percentage complete -* Fixed handling of database query enforcement if item is from a shared folder -* Fixed compiler depreciation of std.digest.digest -* Fixed checking & loading of configuration file sequence -* Fixed multiple issues reported by Valgrind -* Fixed double scan at application startup when using --monitor & --resync together -* Fixed when renaming a file locally, ensure that the target filename is valid before attempting to upload to OneDrive -* Fixed so that if a file is modified locally and --resync is used, rename the local file for data preservation to prevent local data loss - -### Added -* Implement 'bypass_data_preservation' enhancement - -### Changed -* Changed the monitor interval default to 300 seconds - -### Updated -* Updated the handling of out-of-space message when OneDrive is out of space -* Updated debug logging for retry wait times - -## 2.4.1 - 2020-05-02 -### Fixed -* Fixed the handling of renaming files to a name starting with a dot when skip_dotfiles = true -* Fixed the handling of parentheses from path or file names, when doing comparison with regex -* Fixed the handling of renaming dotfiles to another dotfile when skip_dotfile=true in monitor mode -* Fixed the handling of --dry-run and --resync together correctly as current database may be corrupt -* Fixed building on Alpine Linux under Docker -* Fixed the handling of --single-directory for --dry-run and --resync scenarios -* Fixed the handling of .nosync directive when downloading new files into existing directories that is (was) in sync -* Fixed the handling of zero-byte modified files for OneDrive Business -* Fixed skip_dotfiles handling of .folders when in monitor mode to prevent monitoring -* Fixed the handling of '.folder' -> 'folder' move when skip_dotfiles is enabled -* Fixed the handling of folders that cannot be read (permission error) if parent should be skipped -* Fixed the handling of moving folders from skipped directory to non-skipped directory via OneDrive web interface -* Fixed building on CentOS Linux under Docker -* Fixed Codacy reported issues: double quote to prevent globbing and word splitting -* Fixed an assertion when attempting to compute complex path comparison from shared folders -* Fixed the handling of .folders when being skipped via skip_dir - -### Added -* Implement Feature: Implement the ability to set --resync as a config option, default is false - -### Updated -* Update error logging to be consistent when initialising fails -* Update error logging output to handle HTML error response reasoning if present -* Update link to new Microsoft documentation -* Update logging output to differentiate between OneNote objects and other unsupported objects -* Update RHEL/CentOS spec file example -* Update known-issues.md regarding 'SSL_ERROR_SYSCALL, errno 104' -* Update progress bar to be more accurate when downloading large files -* Updated #658 and #865 handling of when to trigger a directory walk when changes occur on OneDrive -* Updated handling of when a full scan is requried due to utilising sync_list -* Updated handling of when OneDrive service throws a 429 or 504 response to retry original request after a delay - -## 2.4.0 - 2020-03-22 -### Fixed -* Fixed how the application handles 429 response codes from OneDrive (critical update) -* Fixed building on Alpine Linux under Docker -* Fixed how the 'username' is determined from the running process for logfile naming -* Fixed file handling when a failed download has occured due to exiting via CTRL-C -* Fixed an unhandled exception when OneDrive throws an error response on initialising -* Fixed the handling of moving files into a skipped .folder when skip_dotfiles = true -* Fixed the regex parsing of response URI to avoid potentially generating a bad request to OneDrive, leading to a 'AADSTS9002313: Invalid request. Request is malformed or invalid.' response. - -### Added -* Added a Dockerfile for building on Rasberry Pi / ARM platforms -* Implement Feature: warning on big deletes to safeguard data on OneDrive -* Implement Feature: delete local files after sync -* Implement Feature: perform skip_dir explicit match only -* Implement Feature: provide config file option for specifying the Client Identifier - -### Changed -* Updated the 'Client Identifier' to a new Application ID - -### Updated -* Updated relevant documentation (README.md, USAGE.md) to add new feature details and clarify existing information -* Update completions to include the --force-http-2 option -* Update to always log when a file is skipped due to the item being invalid -* Update application output when just authorising application to make information clearer -* Update logging output when using sync_list to be clearer as to what is actually being processed and why - -## 2.3.13 - 2019-12-31 -### Fixed -* Change the sync list override flag to false as default when not using sync_list -* Fix --dry-run output when using --upload-only & --no-remote-delete and deleting local files - -### Added -* Add a verbose log entry when a monitor sync loop with OneDrive starts & completes - -### Changed -* Remove logAndNotify for 'processing X changes' as it is excessive for each change bundle to inform the desktop of the number of changes the client is processing - -### Updated -* Updated INSTALL.md with Ubuntu 16.x i386 build instructions to reflect working configuration on legacy hardware -* Updated INSTALL.md with details of Linux packages -* Updated INSTALL.md build instructions for CentOS platforms - -## 2.3.12 - 2019-12-04 -### Fixed -* Retry session upload fragment when transient errors occur to prevent silent upload failure -* Update Microsoft restriction and limitations about windows naming files to include '~' for folder names -* Docker guide fixes, add multiple account setup instructions -* Check database for excluded sync_list items previously in scope -* Catch DNS resolution error -* Fix where an item now out of scope should be flagged for local delete -* Fix rebuilding of onedrive, but ensure version is properly updated -* Update Ubuntu i386 build instructions to use DMD using preferred method - -### Added -* Add debug message to when a message is sent to dbus or notification daemon -* Add i386 instructions for legacy low memory platforms using LDC - -## 2.3.11 - 2019-11-05 -### Fixed -* Fix typo in the documentation regarding invalid config when upgrading from 'skilion' codebase -* Fix handling of skip_dir, skip_file & sync_list config options -* Fix typo in the documentation regarding sync_list -* Fix log output to be consistent with sync_list exclusion -* Fix 'Processing X changes' output to be more reflective of actual activity when using sync_list -* Remove unused and unexported SED variable in Makefile.in -* Handle curl exceptions and timeouts better with backoff/retry logic -* Update skip_dir pattern matching when using wildcards -* Fix when a full rescan is performed when using sync_list -* Fix 'Key not found: name' when computing skip_dir path -* Fix call from --monitor to observe --no-remote-delete -* Fix unhandled exception when monitor initialisation failure occurs due to too many open local files -* Fix unhandled 412 error response from OneDrive API when moving files right after upload -* Fix --monitor when used with --download-only. This fixes a regression introduced in 12947d1. -* Fix if --single-directory is being used, and we are using --monitor, only set inotify watches on the single directory - -### Changed -* Move JSON logging output from error messages to debug output - -## 2.3.10 - 2019-10-01 -### Fixed -* Fix searching for 'name' when deleting a synced item, if the OneDrive API does not return the expected details in the API call -* Fix abnormal termination when no Internet connection -* Fix downloading of files from OneDrive Personal Shared Folders when the OneDrive API responds with unexpected additional path data -* Fix logging of 'initialisation' of client to actually when the attempt to initialise is performed -* Fix when using a sync_list file, using deltaLink will actually 'miss' changes (moves & deletes) on OneDrive as using sync_list discards changes -* Fix OneDrive API status code 500 handling when uploading files as error message is not correct -* Fix crash when resume_upload file is not a valid JSON -* Fix crash when a file system exception is generated when attempting to update the file date & time and this fails - -### Added -* If there is a case-insensitive match error, also return the remote name from the response -* Make user-agent string a configuration option & add to config file -* Set default User-Agent to 'OneDrive Client for Linux v{version}' - -### Changed -* Make verbose logging output optional on Docker -* Enable --resync & debug client output via environment variables on Docker - -## 2.3.9 - 2019-09-01 -### Fixed -* Catch a 403 Forbidden exception when querying Sharepoint Library Names -* Fix unhandled error exceptions that cause application to exit / crash when uploading files -* Fix JSON object validation for queries made against OneDrive where a JSON response is expected and where that response is to be used and expected to be valid -* Fix handling of 5xx responses from OneDrive when uploading via a session - -### Added -* Detect the need for --resync when config changes either via config file or cli override - -### Changed -* Change minimum required version of LDC to v1.12.0 - -### Removed -* Remove redundant logging output due to change in how errors are reported from OneDrive - -## 2.3.8 - 2019-08-04 -### Fixed -* Fix unable to download all files when OneDrive fails to return file level details used to validate file integrity -* Included the flag "-m" to create the home directory when creating the user -* Fix entrypoint.sh to work with "sudo docker run" -* Fix docker build error on stretch -* Fix hidden directories in 'root' from having prefix removed -* Fix Sharepoint Document Library handling for .txt & .csv files -* Fix logging for init.d service -* Fix OneDrive response missing required 'id' element when uploading images -* Fix 'Unexpected character '<'. (Line 1:1)' when OneDrive has an exception error -* Fix error when creating the sync dir fails when there is no permission to create the sync dir - -### Added -* Add explicit check for hashes to be returned in cases where OneDrive API fails to provide them despite requested to do so -* Add comparison with sha1 if OneDrive provides that rather than quickXor -* Add selinux configuration details for a sync folder outside of the home folder -* Add date tag on docker.hub -* Add back CentOS 6 install & uninstall to Makefile -* Add a check to handle moving items out of sync_list sync scope & delete locally if true -* Implement --get-file-link which will return the weburl of a file which has been synced to OneDrive - -### Changed -* Change unauthorized-api exit code to 3 -* Update LDC to v1.16.0 for Travis CI testing -* Use replace function for modified Sharepoint Document Library files rather than delete and upload as new file, preserving file history -* Update Sharepoint modified file handling for files > 4Mb in size - -### Removed -* Remove -d shorthand for --download-only to avoid confusion with other GNU applications where -d stands for 'debug' - -## 2.3.7 - 2019-07-03 -### Fixed -* Fix not all files being downloaded due to OneDrive query failure -* False DB update which potentially could had lead to false data loss on OneDrive - -## 2.3.6 - 2019-07-03 (DO NOT USE) -### Fixed -* Fix JSONValue object validation -* Fix building without git being available -* Fix some spelling/grammatical errors -* Fix OneDrive error response on creating upload session - -### Added -* Add download size & hash check to ensure downloaded files are valid and not corrupt -* Added --force-http-2 to use HTTP/2 if desired - -### Changed -* Depreciated --force-http-1.1 (enabled by default) due to OneDrive inconsistent behavior with HTTP/2 protocol - -## 2.3.5 - 2019-06-19 -### Fixed -* Handle a directory in the sync_dir when no permission to access -* Get rid of forced root necessity during installation -* Fix broken autoconf code for --enable-XXX options -* Fix so that skip_size check should only be used if configured -* Fix a OneDrive Internal Error exception occurring before attempting to download a file - -### Added -* Check for supported version of D compiler - -## 2.3.4 - 2019-06-13 -### Fixed -* Fix 'Local files not deleted' when using bad 'skip_file' entry -* Fix --dry-run logging output for faking downloading new files -* Fix install unit files to correct location on RHEL/CentOS 7 -* Fix up unit file removal on all platforms -* Fix setting times on a file by adding a check to see if the file was actually downloaded before attempting to set the times on the file -* Fix an unhandled curl exception when OneDrive throws an internal timeout error -* Check timestamp to ensure that latest timestamp is used when comparing OneDrive changes -* Fix handling responses where cTag JSON elements are missing -* Fix Docker entrypoint.sh failures when GID is defined but not UID - -### Added -* Add autoconf based build system -* Add an encoding validation check before any path length checks are performed as if the path contains any invalid UTF-8 sequences -* Implement --sync-root-files to sync all files in the OneDrive root when using a sync_list file that would normally exclude these files from being synced -* Implement skip_size feature request -* Implement feature request to support file based OneDrive authorization (request | response) - -### Updated -* Better handle initialisation issues when OneDrive / MS Graph is experiencing problems that generate 401 & 5xx error codes -* Enhance error message when unable to connect to Microsoft OneDrive service when the local CA SSL certificate(s) have issues -* Update Dockerfile to correctly build on Docker Hub -* Rework directory layout and re-factor MD files for readability - -## 2.3.3 - 2019-04-16 -### Fixed -* Fix --upload-only check for Sharepoint uploads -* Fix check to ensure item root we flag as 'root' actually is OneDrive account 'root' -* Handle object error response from OneDrive when uploading to OneDrive Business -* Fix handling of some OneDrive accounts not providing 'quota' details -* Fix 'resume_upload' handling in the event of bad OneDrive response - -### Added -* Add debugging for --get-O365-drive-id function -* Add shell (bash,zsh) completion support -* Add config options for command line switches to allow for better config handling in docker containers - -### Updated -* Implement more meaningful 5xx error responses -* Update onedrive.logrotate indentations and comments -* Update 'min_notif_changes' to 'min_notify_changes' - -## 2.3.2 - 2019-04-02 -### Fixed -* Reduce scanning the entire local system in monitor mode for local changes -* Resolve file creation loop when working directly in the synced folder and Microsoft Sharepoint - -### Added -* Add 'monitor_fullscan_frequency' config option to set the frequency of performing a full disk scan when in monitor mode - -### Updated -* Update default 'skip_file' to include tmp and lock files generated by LibreOffice -* Update database version due to changing defaults of 'skip_file' which will force a rebuild and use of new skip_file default regex - -## 2.3.1 - 2019-03-26 -### Fixed -* Resolve 'make install' issue where rebuild of application would occur due to 'version' being flagged as .PHONY -* Update readme build instructions to include 'make clean;' before build to ensure that 'version' is cleanly removed and can be updated correctly -* Update Debian Travis CI build URL's - -## 2.3.0 - 2019-03-25 -### Fixed -* Resolve application crash if no 'size' value is returned when uploading a new file -* Resolve application crash if a 5xx error is returned when uploading a new file -* Resolve not 'refreshing' version file when rebuilding -* Resolve unexpected application processing by preventing use of --synchronize & --monitor together -* Resolve high CPU usage when performing DB reads -* Update error logging around directory case-insensitive match -* Update Travis CI and ARM dependencies for LDC 1.14.0 -* Update Makefile due to build failure if building from release archive file -* Update logging as to why a OneDrive object was skipped - -### Added -* Implement config option 'skip_dir' - -## 2.2.6 - 2019-03-12 -### Fixed -* Resolve application crash when unable to delete remote folders when business retention policies are enabled -* Resolve deprecation warning: loop index implicitly converted from size_t to int -* Resolve warnings regarding 'bashisms' -* Resolve handling of notification failure is dbus server has not started or available -* Resolve handling of response JSON to ensure that 'id' key element is always checked for -* Resolve excessive & needless logging in monitor mode -* Resolve compiling with LDC on Alpine as musl lacks some standard interfaces -* Resolve notification issues when offline and cannot act on changes -* Resolve Docker entrypoint.sh to accept command line arguments -* Resolve to create a new upload session on reinit -* Resolve where on OneDrive query failure, default root and drive id is used if a response is not returned -* Resolve Key not found: nextExpectedRanges when attempting session uploads and incorrect response is returned -* Resolve application crash when re-using an authentication URI twice after previous --logout -* Resolve creating a folder on a shared personal folder appears successful but returns a JSON error -* Resolve to treat mv of new file as upload of mv target -* Update Debian i386 build dependencies -* Update handling of --get-O365-drive-id to print out all 'site names' that match the explicit search entry rather than just the last match -* Update Docker readme & documentation -* Update handling of validating local file permissions for new file uploads -### Added -* Add support for install & uninstall on RHEL / CentOS 6.x -* Add support for when notifications are enabled, display the number of OneDrive changes to process if any are found -* Add 'config' option 'min_notif_changes' for minimum number of changes to notify on, default = 5 -* Add additional Docker container builds utilising a smaller OS footprint -* Add configurable interval of logging in monitor mode -* Implement new CLI option --skip-dot-files to skip .files and .folders if option is used -* Implement new CLI option --check-for-nosync to ignore folder when special file (.nosync) present -* Implement new CLI option --dry-run - -## 2.2.5 - 2019-01-16 -### Fixed -* Update handling of HTTP 412 - Precondition Failed errors -* Update --display-config to display sync_list if configured -* Add a check for 'id' key on metadata update to prevent 'std.json.JSONException@std/json.d(494): Key not found: id' -* Update handling of 'remote' folder designation as 'root' items -* Ensure that remote deletes are handled correctly -* Handle 'Item not found' exception when unable to query OneDrive 'root' for changes -* Add handling for JSON response error when OneDrive API returns a 404 due to OneDrive API regression -* Fix items highlighted by codacy review -### Added -* Add --force-http-1.1 flag to downgrade any HTTP/2 curl operations to HTTP 1.1 protocol -* Support building with ldc2 and usage of pkg-config for lib finding - -## 2.2.4 - 2018-12-28 -### Fixed -* Resolve JSONException when supplying --get-O365-drive-id option with a string containing spaces -* Resolve 'sync_dir' not read from 'config' file when run in Docker container -* Resolve logic where potentially a 'default' ~/OneDrive sync_dir could be set despite 'config' file configured for an alternate -* Make sure sqlite checkpointing works by properly finalizing statements -* Update logic handling of --single-directory to prevent inadvertent local data loss -* Resolve signal handling and database shutdown on SIGINT and SIGTERM -* Update man page -* Implement better help output formatting -### Added -* Add debug handling for sync_dir operations -* Add debug handling for homePath calculation -* Add debug handling for configDirBase calculation -* Add debug handling if syncDir is created -* Implement Feature Request: Add status command or switch - -## 2.2.3 - 2018-12-20 -### Fixed -* Fix syncdir option is ignored - -## 2.2.2 - 2018-12-20 -### Fixed -* Handle short lived files in monitor mode -* Provide better log messages, less noise on temporary timeouts -* Deal with items that disappear during upload -* Deal with deleted move targets -* Reinitialize sync engine after three failed attempts -* Fix activation of dmd for docker builds -* Fix to check displayName rather than description for --get-O365-drive-id -* Fix checking of config file keys for validity -* Fix exception handling when missing parameter from usage option -### Added -* Notification support via libnotify -* Add very verbose (debug) mode by double -v -v -* Implement option --display-config - -## 2.2.1 - 2018-12-04 -### Fixed -* Gracefully handle connection errors in monitor mode -* Fix renaming of files when syncing -* Installation of doc files, addition of man page -* Adjust timeout values for libcurl -* Continue in monitor mode when sync timed out -* Fix unreachable statements -* Update Makefile to better support packaging -* Allow starting offline in monitor mode -### Added -* Implement --get-O365-drive-id to get correct SharePoint Shared Library (#248) -* Docker buildfiles for onedrive service (#262) - -## 2.2.0 - 2018-11-24 -### Fixed -* Updated client to output additional logging when debugging -* Resolve database assertion failure due to authentication -* Resolve unable to create folders on shared OneDrive Personal accounts -### Added -* Implement feature request to Sync from Microsoft SharePoint -* Implement feature request to specify a logging directory if logging is enabled -### Changed -* Change '--download' to '--download-only' to align with '--upload-only' -* Change logging so that logging to a separate file is no longer the default - -## 2.1.6 - 2018-11-15 -### Fixed -* Updated HTTP/2 transport handling when using curl 7.62.0 for session uploads -### Added -* Added PKGBUILD for makepkg for building packages under Arch Linux - -## 2.1.5 - 2018-11-11 -### Fixed -* Resolve 'Key not found: path' when syncing from some shared folders due to OneDrive API change -* Resolve to only upload changes on remote folder if the item is in the database - dont assert if false -* Resolve files will not download or upload when using curl 7.62.0 due to HTTP/2 being set as default for all curl operations -* Resolve to handle HTTP request returned status code 412 (Precondition Failed) for session uploads to OneDrive Personal Accounts -* Resolve unable to remove '~/.config/onedrive/resume_upload: No such file or directory' if there is a session upload error and the resume file does not get created -* Resolve handling of response codes when using 2 different systems when using '--upload-only' but the same OneDrive account and uploading the same filename to the same location -### Updated -* Updated Travis CI building on LDC v1.11.0 for ARMHF builds -* Updated Makefile to use 'install -D -m 644' rather than 'cp -raf' -* Updated default config to be aligned to code defaults - -## 2.1.4 - 2018-10-10 -### Fixed -* Resolve syncing of OneDrive Personal Shared Folders due to OneDrive API change -* Resolve incorrect systemd installation location(s) in Makefile - -## 2.1.3 - 2018-10-04 -### Fixed -* Resolve File download fails if the file is marked as malware in OneDrive -* Resolve high CPU usage when running in monitor mode -* Resolve how default path is set when running under systemd on headless systems -* Resolve incorrectly nested configDir in X11 systems -* Resolve Key not found: driveType -* Resolve to validate filename length before download to conform with Linux FS limits -* Resolve file handling to look for HTML ASCII codes which will cause uploads to fail -* Resolve Key not found: expirationDateTime on session resume -### Added -* Update Travis CI building to test build on ARM64 - -## 2.1.2 - 2018-08-27 -### Fixed -* Resolve skipping of symlinks in monitor mode -* Resolve Gateway Timeout - JSONValue is not an object -* Resolve systemd/user is not supported on CentOS / RHEL -* Resolve HTTP request returned status code 429 (Too Many Requests) -* Resolve handling of maximum path length calculation -* Resolve 'The parent item is not in the local database' -* Resolve Correctly handle file case sensitivity issues in same folder -* Update unit files documentation link - -## 2.1.1 - 2018-08-14 -### Fixed -* Fix handling no remote delete of remote directories when using --no-remote-delete -* Fix handling of no permission to access a local file / corrupt local file -* Fix application crash when unable to access login.microsoft.com upon application startup -### Added -* Build instructions for openSUSE Leap 15.0 - -## 2.1.0 - 2018-08-10 -### Fixed -* Fix handling of database exit scenarios when there is zero disk space left on drive where the items database resides -* Fix handling of incorrect database permissions -* Fix handling of different database versions to automatically re-create tables if version mis-match -* Fix handling timeout when accessing the Microsoft OneDrive Service -* Fix localFileModifiedTime to not use fraction seconds -### Added -* Implement Feature: Add a progress bar for large uploads & downloads -* Implement Feature: Make checkinterval for monitor configurable -* Implement Feature: Upload Only Option that does not perform remote delete -* Implement Feature: Add ability to skip symlinks -* Add dependency, ebuild and build instructions for Gentoo distributions -### Changed -* Build instructions for x86, x86_64 and ARM32 platforms -* Travis CI files to automate building on x32, x64 and ARM32 architectures -* Travis CI files to test built application against valid, invalid and problem files from previous issues - -## 2.0.2 - 2018-07-18 -### Fixed -* Fix systemd service install for builds with DESTDIR defined -* Fix 'HTTP 412 - Precondition Failed' error handling -* Gracefully handle OneDrive account password change -* Update logic handling of --upload-only and --local-first - -## 2.0.1 - 2018-07-11 -### Fixed -* Resolve computeQuickXorHash generates a different hash when files are > 64Kb - -## 2.0.0 - 2018-07-10 -### Fixed -* Resolve conflict resolution issue during syncing - the client does not handle conflicts very well & keeps on adding the hostname to files -* Resolve skilion #356 by adding additional check for 409 response from OneDrive -* Resolve multiple versions of file shown on website after single upload -* Resolve to gracefully fail when 'onedrive' process cannot get exclusive database lock -* Resolve 'Key not found: fileSystemInfo' when then item is a remote item (OneDrive Personal) -* Resolve skip_file config entry needs to be checked for any characters to escape -* Resolve Microsoft Naming Convention not being followed correctly -* Resolve Error when trying to upload a file with weird non printable characters present -* Resolve Crash if file is locked by online editing (status code 423) -* Resolve Resolve compilation issue with dmd-2.081.0 -* Resolve skip_file configuration doesn't handle spaces or specified directory paths -### Added -* Implement Feature: Add a flag to detect when the sync-folder is missing -* Implement Travis CI for code testing -### Changed -* Update Makefile to use DESTDIR variables -* Update OneDrive Business maximum path length from 256 to 400 -* Update OneDrive Business allowed characters for files and folders -* Update sync_dir handling to use the absolute path for setting parameter to something other than ~/OneDrive via config file or command line -* Update Fedora build instructions - -## 1.1.2 - 2018-05-17 -### Fixed -* Fix 4xx errors including (412 pre-condition, 409 conflict) -* Fix Key not found: lastModifiedDateTime (OneDrive API change) -* Fix configuration directory not found when run via init.d -* Fix skilion Issues #73, #121, #132, #224, #257, #294, #295, #297, #298, #300, #306, #315, #320, #329, #334, #337, #341 -### Added -* Add logging - log client activities to a file (/var/log/onedrive/%username%.onedrive.log or ~/onedrive.log) -* Add https debugging as a flag -* Add `--synchronize` to prevent from syncing when just blindly running the application -* Add individual folder sync -* Add sync from local directory first rather than download first then upload -* Add upload long path check -* Add upload only -* Add check for max upload file size before attempting upload -* Add systemd unit files for single & multi user configuration -* Add init.d file for older init.d based services -* Add Microsoft naming conventions and namespace validation for items that will be uploaded -* Add remaining free space counter at client initialisation to avoid out of space upload issue -* Add large file upload size check to align to OneDrive file size limitations -* Add upload file size validation & retry if does not match -* Add graceful handling of some fatal errors (OneDrive 5xx error handling) - -## Unreleased - 2018-02-19 -### Fixed -* Crash when the delta link is expired -### Changed -* Disabled buffering on stdout - -## 1.1.1 - 2018-01-20 -### Fixed -* Wrong regex for parsing authentication uri - -## 1.1.0 - 2018-01-19 -### Added -* Support for shared folders (OneDrive Personal only) -* `--download` option to only download changes -* `DC` variable in Makefile to chose the compiler -### Changed -* Print logs on stdout instead of stderr -* Improve log messages - -## 1.0.1 - 2017-08-01 -### Added -* `--syncdir` option -### Changed -* `--version` output simplified -* Updated README -### Fixed -* Fix crash caused by remotely deleted and recreated directories - -## 1.0.0 - 2017-07-14 -### Added -* `--version` option diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 94a9ed02..00000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/Makefile.in b/Makefile.in deleted file mode 100644 index 5f0ad31c..00000000 --- a/Makefile.in +++ /dev/null @@ -1,160 +0,0 @@ -package = @PACKAGE_NAME@ -version = @PACKAGE_VERSION@ -prefix = @prefix@ -# we don't use @exec_prefix@ because it usually contains '${prefix}' literally -# but we use @prefix@/bin/onedrive in the systemd unit files which are generated -# from the configure script. -# Thus, set exec_prefix unconditionally to prefix -# Alternative approach would be add dep on sed, and do manual generation in the Makefile. -# exec_prefix = @exec_prefix@ -exec_prefix = @prefix@ -datarootdir = @datarootdir@ -datadir = @datadir@ -srcdir = @srcdir@ -bindir = @bindir@ -mandir = @mandir@ -sysconfdir = @sysconfdir@ -docdir = $(datadir)/doc/$(package) -VPATH = @srcdir@ -INSTALL = @INSTALL@ - -NOTIFICATIONS = @NOTIFICATIONS@ -HAVE_SYSTEMD = @HAVE_SYSTEMD@ -systemduserunitdir = @systemduserunitdir@ -systemdsystemunitdir = @systemdsystemunitdir@ -curl_LIBS = @curl_LIBS@ -sqlite_LIBS = @sqlite_LIBS@ -notify_LIBS = @notify_LIBS@ -COMPLETIONS = @COMPLETIONS@ -BASH_COMPLETION_DIR = @BASH_COMPLETION_DIR@ -ZSH_COMPLETION_DIR = @ZSH_COMPLETION_DIR@ -FISH_COMPLETION_DIR = @FISH_COMPLETION_DIR@ -DEBUG = @DEBUG@ - -DC = @DC@ -DC_TYPE = @DC_TYPE@ -DCFLAGS = @DCFLAGS@ -DCFLAGS += -w -g -O -J. -ifeq ($(DEBUG),yes) -ifeq ($(DC_TYPE),dmd) -DCFLAGS += -debug -gs -else -DCFLAGS += -d-debug -gc -endif -endif - -ifeq ($(NOTIFICATIONS),yes) -NOTIF_VERSIONS=-version=NoPragma -version=NoGdk -version=Notifications -# support ldc2 which needs -d prefix for version specification -ifeq ($(DC_TYPE),ldc) - NOTIF_VERSIONS := $(addprefix -d,$(NOTIF_VERSIONS)) -endif -DCFLAGS += $(NOTIF_VERSIONS) -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/SharePoint-Shared-Libraries.md docs/USAGE.md docs/BusinessSharedFolders.md docs/advanced-usage.md docs/application-security.md - -ifneq ("$(wildcard /etc/redhat-release)","") -RHEL = $(shell cat /etc/redhat-release | grep -E "(Red Hat Enterprise Linux|CentOS)" | wc -l) -RHEL_VERSION = $(shell rpm --eval "%{rhel}") -else -RHEL = 0 -RHEL_VERSION = 0 -endif - -SOURCES = \ - src/config.d \ - src/itemdb.d \ - src/log.d \ - src/main.d \ - src/monitor.d \ - src/onedrive.d \ - src/qxor.d \ - src/selective.d \ - src/sqlite.d \ - src/sync.d \ - src/upload.d \ - src/util.d \ - src/progress.d \ - src/arsd/cgi.d - -ifeq ($(NOTIFICATIONS),yes) -SOURCES += src/notifications/notify.d src/notifications/dnotify.d -endif - -all: onedrive - -clean: - rm -f onedrive onedrive.o version - rm -rf autom4te.cache - rm -f config.log config.status - -# also remove files generated via ./configure -distclean: clean - rm -f Makefile contrib/pacman/PKGBUILD contrib/spec/onedrive.spec onedrive.1 \ - $(system_unit_files) $(user_unit_files) - -onedrive: $(SOURCES) - if [ -f .git/HEAD ] ; then \ - git describe --tags > version ; \ - else \ - echo $(version) > version ; \ - fi - $(DC) $(DCFLAGS) $(addprefix -L,$(curl_LIBS)) $(addprefix -L,$(sqlite_LIBS)) $(addprefix -L,$(notify_LIBS)) -L-ldl $(SOURCES) -of$@ - -install: all - $(INSTALL) -D onedrive $(DESTDIR)$(bindir)/onedrive - $(INSTALL) -D -m 0644 onedrive.1 $(DESTDIR)$(mandir)/man1/onedrive.1 - $(INSTALL) -D -m 0644 contrib/logrotate/onedrive.logrotate $(DESTDIR)$(sysconfdir)/logrotate.d/onedrive - mkdir -p $(DESTDIR)$(docdir) - $(INSTALL) -D -m 0644 $(DOCFILES) $(DESTDIR)$(docdir) -ifeq ($(HAVE_SYSTEMD),yes) - $(INSTALL) -d -m 0755 $(DESTDIR)$(systemduserunitdir) $(DESTDIR)$(systemdsystemunitdir) -ifeq ($(RHEL),1) - $(INSTALL) -m 0644 $(system_unit_files) $(DESTDIR)$(systemdsystemunitdir) - $(INSTALL) -m 0644 $(user_unit_files) $(DESTDIR)$(systemdsystemunitdir) -else - $(INSTALL) -m 0644 $(system_unit_files) $(DESTDIR)$(systemdsystemunitdir) - $(INSTALL) -m 0644 $(user_unit_files) $(DESTDIR)$(systemduserunitdir) -endif -else -ifeq ($(RHEL_VERSION),6) - install -D contrib/init.d/onedrive.init $(DESTDIR)/etc/init.d/onedrive - install -D contrib/init.d/onedrive_service.sh $(DESTDIR)$(bindir)/onedrive_service.sh -endif -endif -ifeq ($(COMPLETIONS),yes) - $(INSTALL) -D -m 0644 contrib/completions/complete.zsh $(DESTDIR)$(ZSH_COMPLETION_DIR)/_onedrive - $(INSTALL) -D -m 0644 contrib/completions/complete.bash $(DESTDIR)$(BASH_COMPLETION_DIR)/onedrive - $(INSTALL) -D -m 0644 contrib/completions/complete.fish $(DESTDIR)$(FISH_COMPLETION_DIR)/onedrive.fish -endif - - -uninstall: - rm -f $(DESTDIR)$(bindir)/onedrive - rm -f $(DESTDIR)$(mandir)/man1/onedrive.1 - rm -f $(DESTDIR)$(sysconfdir)/logrotate.d/onedrive -ifeq ($(HAVE_SYSTEMD),yes) -ifeq ($(RHEL),1) - rm -f $(DESTDIR)$(systemdsystemunitdir)/onedrive*.service -else - rm -f $(DESTDIR)$(systemdsystemunitdir)/onedrive*.service - rm -f $(DESTDIR)$(systemduserunitdir)/onedrive*.service -endif -else -ifeq ($(RHEL_VERSION),6) - rm -f $(DESTDIR)/etc/init.d/onedrive - rm -f $(DESTDIR)$(bindir)/onedrive_service.sh -endif -endif - for i in $(DOCFILES) ; do rm -f $(DESTDIR)$(docdir)/$$i ; done -ifeq ($(COMPLETIONS),yes) - rm -f $(DESTDIR)$(ZSH_COMPLETION_DIR)/_onedrive - rm -f $(DESTDIR)$(BASH_COMPLETION_DIR)/onedrive - rm -f $(DESTDIR)$(FISH_COMPLETION_DIR)/onedrive.fish -endif - - diff --git a/README.md b/README.md deleted file mode 100644 index 28b66359..00000000 --- a/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# OneDrive Client for Linux -[![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) -[![Release Date](https://img.shields.io/github/release-date/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) -[![Test Build](https://github.com/abraunegg/onedrive/actions/workflows/testbuild.yaml/badge.svg)](https://github.com/abraunegg/onedrive/actions/workflows/testbuild.yaml) -[![Build Docker Images](https://github.com/abraunegg/onedrive/actions/workflows/docker.yaml/badge.svg)](https://github.com/abraunegg/onedrive/actions/workflows/docker.yaml) -[![Docker Pulls](https://img.shields.io/docker/pulls/driveone/onedrive)](https://hub.docker.com/r/driveone/onedrive) - -A free Microsoft OneDrive Client which supports OneDrive Personal, OneDrive for Business, OneDrive for Office365 and SharePoint. - -This powerful and highly configurable client can run on all major Linux distributions, FreeBSD, or as a Docker container. It supports one-way and two-way sync capabilities and securely connects to Microsoft OneDrive services. - -This client is a 'fork' of the [skilion](https://github.com/skilion/onedrive) client, which the developer has confirmed he has no desire to maintain or support the client ([reference](https://github.com/skilion/onedrive/issues/518#issuecomment-717604726)). This fork has been in active development since mid 2018. - -## Features -* State caching -* Real-Time local file monitoring with inotify -* Real-Time syncing of remote updates via webhooks -* File upload / download validation to ensure data integrity -* Resumable uploads -* Support OneDrive for Business (part of Office 365) -* Shared Folder support for OneDrive Personal and OneDrive Business accounts -* 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) -* Supports single & multi-tenanted applications -* Supports rate limiting of traffic - -## What's missing -* Ability to encrypt/decrypt files on-the-fly when uploading/downloading files from OneDrive -* Support for Windows 'On-Demand' functionality so file is only downloaded when accessed locally - -## External Enhancements -* A GUI for configuration management: [OneDrive Client for Linux GUI](https://github.com/bpozdena/OneDriveGUI) -* Colorful log output terminal modification: [OneDrive Client for Linux Colorful log Output](https://github.com/zzzdeb/dotfiles/blob/master/scripts/tools/onedrive_log) -* System Tray Icon: [OneDrive Client for Linux System Tray Icon](https://github.com/DanielBorgesOliveira/onedrive_tray) - -## Supported Application Version -Only the current application release version or greater is supported. - -The current application release version is: [![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) - -Check the version of the application you are using `onedrive --version` and ensure that you are running either the current release or compile the application yourself from master to get the latest version. - -If you are not using the above application version or greater, you must upgrade your application to obtain support. - -## Have a Question -If you have a question or need something clarified, please raise a new disscussion post [here](https://github.com/abraunegg/onedrive/discussions) - -Be sure to review the Frequently Asked Questions as well before raising a new discussion post. - -## Frequently Asked Questions -Refer to [Frequently Asked Questions](https://github.com/abraunegg/onedrive/wiki/Frequently-Asked-Questions) - -## Reporting an Issue or Bug -If you encounter any bugs you can report them here on GitHub. Before filing an issue be sure to: - -1. Check the version of the application you are using `onedrive --version` and ensure that you are running a supported application version. If you are not using a supported application version, you must first upgrade your application to a supported version and then re-test for your issue. -2. If you are using a supported applcation version, fill in a new bug report using the [issue template](https://github.com/abraunegg/onedrive/issues/new?template=bug_report.md) -3. Generate a debug log for support using the following [process](https://github.com/abraunegg/onedrive/wiki/Generate-debug-log-for-support) - * If you are in *any* way concerned regarding the sensitivity of the data contained with in the verbose debug log file, create a new OneDrive account, configure the client to use that, use *dummy* data to simulate your environment and then replicate your original issue - * If you are still concerned, provide an NDA or confidentiality document to sign -4. Upload the debug log to [pastebin](https://pastebin.com/) or archive and email to support@mynas.com.au - * If you are concerned regarding the sensitivity of your debug data, encrypt + password protect the archive file and provide the decryption password via an out-of-band (OOB) mechanism. Email support@mynas.com.au for an OOB method for the password to be sent. - * If you are still concerned, provide an NDA or confidentiality document to sign - -## Known issues -Refer to [docs/known-issues.md](https://github.com/abraunegg/onedrive/blob/master/docs/known-issues.md) - -## Documentation and Configuration Assistance -### Installing from Distribution Packages or Building the OneDrive Client for Linux from source -Refer to [docs/INSTALL.md](https://github.com/abraunegg/onedrive/blob/master/docs/INSTALL.md) - -### Configuration and Usage -Refer to [docs/USAGE.md](https://github.com/abraunegg/onedrive/blob/master/docs/USAGE.md) - -### Configure OneDrive Business Shared Folders -Refer to [docs/BusinessSharedFolders.md](https://github.com/abraunegg/onedrive/blob/master/docs/BusinessSharedFolders.md) - -### Configure SharePoint / Office 365 Shared Libraries (Business or Education) -Refer to [docs/SharePoint-Shared-Libraries.md](https://github.com/abraunegg/onedrive/blob/master/docs/SharePoint-Shared-Libraries.md) - -### Configure National Cloud support -Refer to [docs/national-cloud-deployments.md](https://github.com/abraunegg/onedrive/blob/master/docs/national-cloud-deployments.md) - -### Docker support -Refer to [docs/Docker.md](https://github.com/abraunegg/onedrive/blob/master/docs/Docker.md) - -### Podman support -Refer to [docs/Podman.md](https://github.com/abraunegg/onedrive/blob/master/docs/Podman.md) - diff --git a/aclocal.m4 b/aclocal.m4 deleted file mode 100644 index 961fb774..00000000 --- a/aclocal.m4 +++ /dev/null @@ -1,290 +0,0 @@ -# generated automatically by aclocal 1.16.1 -*- Autoconf -*- - -# Copyright (C) 1996-2018 Free Software Foundation, Inc. - -# This file is free software; the Free Software Foundation -# gives unlimited permission to copy and/or distribute it, -# with or without modifications, as long as this notice is preserved. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY, to the extent permitted by law; without -# even the implied warranty of MERCHANTABILITY or FITNESS FOR A -# PARTICULAR PURPOSE. - -m4_ifndef([AC_CONFIG_MACRO_DIRS], [m4_defun([_AM_CONFIG_MACRO_DIRS], [])m4_defun([AC_CONFIG_MACRO_DIRS], [_AM_CONFIG_MACRO_DIRS($@)])]) -dnl pkg.m4 - Macros to locate and utilise pkg-config. -*- Autoconf -*- -dnl serial 11 (pkg-config-0.29) -dnl -dnl Copyright © 2004 Scott James Remnant . -dnl Copyright © 2012-2015 Dan Nicholson -dnl -dnl This program is free software; you can redistribute it and/or modify -dnl it under the terms of the GNU General Public License as published by -dnl the Free Software Foundation; either version 2 of the License, or -dnl (at your option) any later version. -dnl -dnl This program is distributed in the hope that it will be useful, but -dnl WITHOUT ANY WARRANTY; without even the implied warranty of -dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -dnl General Public License for more details. -dnl -dnl You should have received a copy of the GNU General Public License -dnl along with this program; if not, write to the Free Software -dnl Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA -dnl 02111-1307, USA. -dnl -dnl As a special exception to the GNU General Public License, if you -dnl distribute this file as part of a program that contains a -dnl configuration script generated by Autoconf, you may include it under -dnl the same distribution terms that you use for the rest of that -dnl program. - -dnl PKG_PREREQ(MIN-VERSION) -dnl ----------------------- -dnl Since: 0.29 -dnl -dnl Verify that the version of the pkg-config macros are at least -dnl MIN-VERSION. Unlike PKG_PROG_PKG_CONFIG, which checks the user's -dnl installed version of pkg-config, this checks the developer's version -dnl of pkg.m4 when generating configure. -dnl -dnl To ensure that this macro is defined, also add: -dnl m4_ifndef([PKG_PREREQ], -dnl [m4_fatal([must install pkg-config 0.29 or later before running autoconf/autogen])]) -dnl -dnl See the "Since" comment for each macro you use to see what version -dnl of the macros you require. -m4_defun([PKG_PREREQ], -[m4_define([PKG_MACROS_VERSION], [0.29]) -m4_if(m4_version_compare(PKG_MACROS_VERSION, [$1]), -1, - [m4_fatal([pkg.m4 version $1 or higher is required but ]PKG_MACROS_VERSION[ found])]) -])dnl PKG_PREREQ - -dnl PKG_PROG_PKG_CONFIG([MIN-VERSION]) -dnl ---------------------------------- -dnl Since: 0.16 -dnl -dnl Search for the pkg-config tool and set the PKG_CONFIG variable to -dnl first found in the path. Checks that the version of pkg-config found -dnl is at least MIN-VERSION. If MIN-VERSION is not specified, 0.9.0 is -dnl used since that's the first version where most current features of -dnl pkg-config existed. -AC_DEFUN([PKG_PROG_PKG_CONFIG], -[m4_pattern_forbid([^_?PKG_[A-Z_]+$]) -m4_pattern_allow([^PKG_CONFIG(_(PATH|LIBDIR|SYSROOT_DIR|ALLOW_SYSTEM_(CFLAGS|LIBS)))?$]) -m4_pattern_allow([^PKG_CONFIG_(DISABLE_UNINSTALLED|TOP_BUILD_DIR|DEBUG_SPEW)$]) -AC_ARG_VAR([PKG_CONFIG], [path to pkg-config utility]) -AC_ARG_VAR([PKG_CONFIG_PATH], [directories to add to pkg-config's search path]) -AC_ARG_VAR([PKG_CONFIG_LIBDIR], [path overriding pkg-config's built-in search path]) - -if test "x$ac_cv_env_PKG_CONFIG_set" != "xset"; then - AC_PATH_TOOL([PKG_CONFIG], [pkg-config]) -fi -if test -n "$PKG_CONFIG"; then - _pkg_min_version=m4_default([$1], [0.9.0]) - AC_MSG_CHECKING([pkg-config is at least version $_pkg_min_version]) - if $PKG_CONFIG --atleast-pkgconfig-version $_pkg_min_version; then - AC_MSG_RESULT([yes]) - else - AC_MSG_RESULT([no]) - PKG_CONFIG="" - fi -fi[]dnl -])dnl PKG_PROG_PKG_CONFIG - -dnl PKG_CHECK_EXISTS(MODULES, [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) -dnl ------------------------------------------------------------------- -dnl Since: 0.18 -dnl -dnl Check to see whether a particular set of modules exists. Similar to -dnl PKG_CHECK_MODULES(), but does not set variables or print errors. -dnl -dnl Please remember that m4 expands AC_REQUIRE([PKG_PROG_PKG_CONFIG]) -dnl only at the first occurence in configure.ac, so if the first place -dnl it's called might be skipped (such as if it is within an "if", you -dnl have to call PKG_CHECK_EXISTS manually -AC_DEFUN([PKG_CHECK_EXISTS], -[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl -if test -n "$PKG_CONFIG" && \ - AC_RUN_LOG([$PKG_CONFIG --exists --print-errors "$1"]); then - m4_default([$2], [:]) -m4_ifvaln([$3], [else - $3])dnl -fi]) - -dnl _PKG_CONFIG([VARIABLE], [COMMAND], [MODULES]) -dnl --------------------------------------------- -dnl Internal wrapper calling pkg-config via PKG_CONFIG and setting -dnl pkg_failed based on the result. -m4_define([_PKG_CONFIG], -[if test -n "$$1"; then - pkg_cv_[]$1="$$1" - elif test -n "$PKG_CONFIG"; then - PKG_CHECK_EXISTS([$3], - [pkg_cv_[]$1=`$PKG_CONFIG --[]$2 "$3" 2>/dev/null` - test "x$?" != "x0" && pkg_failed=yes ], - [pkg_failed=yes]) - else - pkg_failed=untried -fi[]dnl -])dnl _PKG_CONFIG - -dnl _PKG_SHORT_ERRORS_SUPPORTED -dnl --------------------------- -dnl Internal check to see if pkg-config supports short errors. -AC_DEFUN([_PKG_SHORT_ERRORS_SUPPORTED], -[AC_REQUIRE([PKG_PROG_PKG_CONFIG]) -if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then - _pkg_short_errors_supported=yes -else - _pkg_short_errors_supported=no -fi[]dnl -])dnl _PKG_SHORT_ERRORS_SUPPORTED - - -dnl PKG_CHECK_MODULES(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND], -dnl [ACTION-IF-NOT-FOUND]) -dnl -------------------------------------------------------------- -dnl Since: 0.4.0 -dnl -dnl Note that if there is a possibility the first call to -dnl PKG_CHECK_MODULES might not happen, you should be sure to include an -dnl explicit call to PKG_PROG_PKG_CONFIG in your configure.ac -AC_DEFUN([PKG_CHECK_MODULES], -[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl -AC_ARG_VAR([$1][_CFLAGS], [C compiler flags for $1, overriding pkg-config])dnl -AC_ARG_VAR([$1][_LIBS], [linker flags for $1, overriding pkg-config])dnl - -pkg_failed=no -AC_MSG_CHECKING([for $1]) - -_PKG_CONFIG([$1][_CFLAGS], [cflags], [$2]) -_PKG_CONFIG([$1][_LIBS], [libs], [$2]) - -m4_define([_PKG_TEXT], [Alternatively, you may set the environment variables $1[]_CFLAGS -and $1[]_LIBS to avoid the need to call pkg-config. -See the pkg-config man page for more details.]) - -if test $pkg_failed = yes; then - AC_MSG_RESULT([no]) - _PKG_SHORT_ERRORS_SUPPORTED - if test $_pkg_short_errors_supported = yes; then - $1[]_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$2" 2>&1` - else - $1[]_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$2" 2>&1` - fi - # Put the nasty error message in config.log where it belongs - echo "$$1[]_PKG_ERRORS" >&AS_MESSAGE_LOG_FD - - m4_default([$4], [AC_MSG_ERROR( -[Package requirements ($2) were not met: - -$$1_PKG_ERRORS - -Consider adjusting the PKG_CONFIG_PATH environment variable if you -installed software in a non-standard prefix. - -_PKG_TEXT])[]dnl - ]) -elif test $pkg_failed = untried; then - AC_MSG_RESULT([no]) - m4_default([$4], [AC_MSG_FAILURE( -[The pkg-config script could not be found or is too old. Make sure it -is in your PATH or set the PKG_CONFIG environment variable to the full -path to pkg-config. - -_PKG_TEXT - -To get pkg-config, see .])[]dnl - ]) -else - $1[]_CFLAGS=$pkg_cv_[]$1[]_CFLAGS - $1[]_LIBS=$pkg_cv_[]$1[]_LIBS - AC_MSG_RESULT([yes]) - $3 -fi[]dnl -])dnl PKG_CHECK_MODULES - - -dnl PKG_CHECK_MODULES_STATIC(VARIABLE-PREFIX, MODULES, [ACTION-IF-FOUND], -dnl [ACTION-IF-NOT-FOUND]) -dnl --------------------------------------------------------------------- -dnl Since: 0.29 -dnl -dnl Checks for existence of MODULES and gathers its build flags with -dnl static libraries enabled. Sets VARIABLE-PREFIX_CFLAGS from --cflags -dnl and VARIABLE-PREFIX_LIBS from --libs. -dnl -dnl Note that if there is a possibility the first call to -dnl PKG_CHECK_MODULES_STATIC might not happen, you should be sure to -dnl include an explicit call to PKG_PROG_PKG_CONFIG in your -dnl configure.ac. -AC_DEFUN([PKG_CHECK_MODULES_STATIC], -[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl -_save_PKG_CONFIG=$PKG_CONFIG -PKG_CONFIG="$PKG_CONFIG --static" -PKG_CHECK_MODULES($@) -PKG_CONFIG=$_save_PKG_CONFIG[]dnl -])dnl PKG_CHECK_MODULES_STATIC - - -dnl PKG_INSTALLDIR([DIRECTORY]) -dnl ------------------------- -dnl Since: 0.27 -dnl -dnl Substitutes the variable pkgconfigdir as the location where a module -dnl should install pkg-config .pc files. By default the directory is -dnl $libdir/pkgconfig, but the default can be changed by passing -dnl DIRECTORY. The user can override through the --with-pkgconfigdir -dnl parameter. -AC_DEFUN([PKG_INSTALLDIR], -[m4_pushdef([pkg_default], [m4_default([$1], ['${libdir}/pkgconfig'])]) -m4_pushdef([pkg_description], - [pkg-config installation directory @<:@]pkg_default[@:>@]) -AC_ARG_WITH([pkgconfigdir], - [AS_HELP_STRING([--with-pkgconfigdir], pkg_description)],, - [with_pkgconfigdir=]pkg_default) -AC_SUBST([pkgconfigdir], [$with_pkgconfigdir]) -m4_popdef([pkg_default]) -m4_popdef([pkg_description]) -])dnl PKG_INSTALLDIR - - -dnl PKG_NOARCH_INSTALLDIR([DIRECTORY]) -dnl -------------------------------- -dnl Since: 0.27 -dnl -dnl Substitutes the variable noarch_pkgconfigdir as the location where a -dnl module should install arch-independent pkg-config .pc files. By -dnl default the directory is $datadir/pkgconfig, but the default can be -dnl changed by passing DIRECTORY. The user can override through the -dnl --with-noarch-pkgconfigdir parameter. -AC_DEFUN([PKG_NOARCH_INSTALLDIR], -[m4_pushdef([pkg_default], [m4_default([$1], ['${datadir}/pkgconfig'])]) -m4_pushdef([pkg_description], - [pkg-config arch-independent installation directory @<:@]pkg_default[@:>@]) -AC_ARG_WITH([noarch-pkgconfigdir], - [AS_HELP_STRING([--with-noarch-pkgconfigdir], pkg_description)],, - [with_noarch_pkgconfigdir=]pkg_default) -AC_SUBST([noarch_pkgconfigdir], [$with_noarch_pkgconfigdir]) -m4_popdef([pkg_default]) -m4_popdef([pkg_description]) -])dnl PKG_NOARCH_INSTALLDIR - - -dnl PKG_CHECK_VAR(VARIABLE, MODULE, CONFIG-VARIABLE, -dnl [ACTION-IF-FOUND], [ACTION-IF-NOT-FOUND]) -dnl ------------------------------------------- -dnl Since: 0.28 -dnl -dnl Retrieves the value of the pkg-config variable for the given module. -AC_DEFUN([PKG_CHECK_VAR], -[AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl -AC_ARG_VAR([$1], [value of $3 for $2, overriding pkg-config])dnl - -_PKG_CONFIG([$1], [variable="][$3]["], [$2]) -AS_VAR_COPY([$1], [pkg_cv_][$1]) - -AS_VAR_IF([$1], [""], [$5], [$4])dnl -])dnl PKG_CHECK_VAR - diff --git a/config b/config deleted file mode 100644 index 807180ea..00000000 --- a/config +++ /dev/null @@ -1,61 +0,0 @@ -# Configuration for OneDrive Linux Client -# This file contains the list of supported configuration fields -# with their default values. -# All values need to be enclosed in quotes -# 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_dir = "~/OneDrive" -# skip_file = "~*|.~*|*.tmp" -# monitor_interval = "300" -# skip_dir = "" -# log_dir = "/var/log/onedrive/" -# drive_id = "" -# upload_only = "false" -# check_nomount = "false" -# check_nosync = "false" -# download_only = "false" -# disable_notifications = "false" -# disable_upload_validation = "false" -# enable_logging = "false" -# force_http_11 = "false" -# local_first = "false" -# no_remote_delete = "false" -# skip_symlinks = "false" -# debug_https = "false" -# skip_dotfiles = "false" -# skip_size = "1000" -# dry_run = "false" -# min_notify_changes = "5" -# monitor_log_frequency = "6" -# monitor_fullscan_frequency = "12" -# sync_root_files = "false" -# classify_as_big_delete = "1000" -# user_agent = "" -# remove_source_files = "false" -# skip_dir_strict_match = "false" -# application_id = "" -# resync = "false" -# resync_auth = "false" -# bypass_data_preservation = "false" -# azure_ad_endpoint = "" -# azure_tenant_id = "common" -# sync_business_shared_folders = "false" -# sync_dir_permissions = "700" -# sync_file_permissions = "600" -# rate_limit = "131072" -# webhook_enabled = "false" -# webhook_public_url = "" -# webhook_listening_host = "" -# webhook_listening_port = "8888" -# webhook_expiration_interval = "86400" -# webhook_renewal_interval = "43200" -# space_reservation = "50" -# display_running_config = "false" -# read_only_auth_scope = "false" -# cleanup_local_files = "false" -# operation_timeout = "3600" -# dns_timeout = "60" -# connect_timeout = "10" -# data_timeout = "600" -# ip_protocol_version = "0" diff --git a/configure b/configure deleted file mode 100755 index f68a775c..00000000 --- a/configure +++ /dev/null @@ -1,3785 +0,0 @@ -#! /bin/sh -# Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.69 for onedrive v2.4.25. -# -# Report bugs to . -# -# -# Copyright (C) 1992-1996, 1998-2012 Free Software Foundation, Inc. -# -# -# This configure script is free software; the Free Software Foundation -# gives unlimited permission to copy, distribute and modify it. -## -------------------- ## -## M4sh Initialization. ## -## -------------------- ## - -# Be more Bourne compatible -DUALCASE=1; export DUALCASE # for MKS sh -if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then : - emulate sh - NULLCMD=: - # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which - # is contrary to our usage. Disable this feature. - alias -g '${1+"$@"}'='"$@"' - setopt NO_GLOB_SUBST -else - case `(set -o) 2>/dev/null` in #( - *posix*) : - set -o posix ;; #( - *) : - ;; -esac -fi - - -as_nl=' -' -export as_nl -# Printing a long string crashes Solaris 7 /usr/bin/printf. -as_echo='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' -as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo -as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo$as_echo -# Prefer a ksh shell builtin over an external printf program on Solaris, -# but without wasting forks for bash or zsh. -if test -z "$BASH_VERSION$ZSH_VERSION" \ - && (test "X`print -r -- $as_echo`" = "X$as_echo") 2>/dev/null; then - as_echo='print -r --' - as_echo_n='print -rn --' -elif (test "X`printf %s $as_echo`" = "X$as_echo") 2>/dev/null; then - as_echo='printf %s\n' - as_echo_n='printf %s' -else - if test "X`(/usr/ucb/echo -n -n $as_echo) 2>/dev/null`" = "X-n $as_echo"; then - as_echo_body='eval /usr/ucb/echo -n "$1$as_nl"' - as_echo_n='/usr/ucb/echo -n' - else - as_echo_body='eval expr "X$1" : "X\\(.*\\)"' - as_echo_n_body='eval - arg=$1; - case $arg in #( - *"$as_nl"*) - expr "X$arg" : "X\\(.*\\)$as_nl"; - arg=`expr "X$arg" : ".*$as_nl\\(.*\\)"`;; - esac; - expr "X$arg" : "X\\(.*\\)" | tr -d "$as_nl" - ' - export as_echo_n_body - as_echo_n='sh -c $as_echo_n_body as_echo' - fi - export as_echo_body - as_echo='sh -c $as_echo_body as_echo' -fi - -# The user is always right. -if test "${PATH_SEPARATOR+set}" != set; then - PATH_SEPARATOR=: - (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && { - (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 || - PATH_SEPARATOR=';' - } -fi - - -# IFS -# We need space, tab and new line, in precisely that order. Quoting is -# there to prevent editors from complaining about space-tab. -# (If _AS_PATH_WALK were called with IFS unset, it would disable word -# splitting by setting IFS to empty value.) -IFS=" "" $as_nl" - -# Find who we are. Look in the path if we contain no directory separator. -as_myself= -case $0 in #(( - *[\\/]* ) as_myself=$0 ;; - *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR -for as_dir in $PATH -do - IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - test -r "$as_dir/$0" && as_myself=$as_dir/$0 && break - done -IFS=$as_save_IFS - - ;; -esac -# We did not find ourselves, most probably we were run as `sh COMMAND' -# in which case we are not to be found in the path. -if test "x$as_myself" = x; then - as_myself=$0 -fi -if test ! -f "$as_myself"; then - $as_echo "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 - exit 1 -fi - -# Unset variables that we do not need and which cause bugs (e.g. in -# pre-3.0 UWIN ksh). But do not cause bugs in bash 2.01; the "|| exit 1" -# suppresses any "Segmentation fault" message there. '((' could -# trigger a bug in pdksh 5.2.14. -for as_var in BASH_ENV ENV MAIL MAILPATH -do eval test x\${$as_var+set} = xset \ - && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : -done -PS1='$ ' -PS2='> ' -PS4='+ ' - -# NLS nuisances. -LC_ALL=C -export LC_ALL -LANGUAGE=C -export LANGUAGE - -# CDPATH. -(unset CDPATH) >/dev/null 2>&1 && unset CDPATH - -# Use a proper internal environment variable to ensure we don't fall - # into an infinite loop, continuously re-executing ourselves. - if test x"${_as_can_reexec}" != xno && test "x$CONFIG_SHELL" != x; then - _as_can_reexec=no; export _as_can_reexec; - # We cannot yet assume a decent shell, so we have to provide a -# neutralization value for shells without unset; and this also -# works around shells that cannot unset nonexistent variables. -# Preserve -v and -x to the replacement shell. -BASH_ENV=/dev/null -ENV=/dev/null -(unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV -case $- in # (((( - *v*x* | *x*v* ) as_opts=-vx ;; - *v* ) as_opts=-v ;; - *x* ) as_opts=-x ;; - * ) as_opts= ;; -esac -exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} -# Admittedly, this is quite paranoid, since all the known shells bail -# out after a failed `exec'. -$as_echo "$0: could not re-execute with $CONFIG_SHELL" >&2 -as_fn_exit 255 - fi - # We don't want this to propagate to other subprocesses. - { _as_can_reexec=; unset _as_can_reexec;} -if test "x$CONFIG_SHELL" = x; then - as_bourne_compatible="if test -n \"\${ZSH_VERSION+set}\" && (emulate sh) >/dev/null 2>&1; then : - emulate sh - NULLCMD=: - # Pre-4.2 versions of Zsh do word splitting on \${1+\"\$@\"}, which - # is contrary to our usage. Disable this feature. - alias -g '\${1+\"\$@\"}'='\"\$@\"' - setopt NO_GLOB_SUBST -else - case \`(set -o) 2>/dev/null\` in #( - *posix*) : - set -o posix ;; #( - *) : - ;; -esac -fi -" - as_required="as_fn_return () { (exit \$1); } -as_fn_success () { as_fn_return 0; } -as_fn_failure () { as_fn_return 1; } -as_fn_ret_success () { return 0; } -as_fn_ret_failure () { return 1; } - -exitcode=0 -as_fn_success || { exitcode=1; echo as_fn_success failed.; } -as_fn_failure && { exitcode=1; echo as_fn_failure succeeded.; } -as_fn_ret_success || { exitcode=1; echo as_fn_ret_success failed.; } -as_fn_ret_failure && { exitcode=1; echo as_fn_ret_failure succeeded.; } -if ( set x; as_fn_ret_success y && test x = \"\$1\" ); then : - -else - exitcode=1; echo positional parameters were not saved. -fi -test x\$exitcode = x0 || exit 1 -test -x / || exit 1" - as_suggested=" as_lineno_1=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_1a=\$LINENO - as_lineno_2=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_2a=\$LINENO - eval 'test \"x\$as_lineno_1'\$as_run'\" != \"x\$as_lineno_2'\$as_run'\" && - test \"x\`expr \$as_lineno_1'\$as_run' + 1\`\" = \"x\$as_lineno_2'\$as_run'\"' || exit 1" - if (eval "$as_required") 2>/dev/null; then : - as_have_required=yes -else - as_have_required=no -fi - if test x$as_have_required = xyes && (eval "$as_suggested") 2>/dev/null; then : - -else - as_save_IFS=$IFS; IFS=$PATH_SEPARATOR -as_found=false -for as_dir in /bin$PATH_SEPARATOR/usr/bin$PATH_SEPARATOR$PATH -do - IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - as_found=: - case $as_dir in #( - /*) - for as_base in sh bash ksh sh5; do - # Try only shells that exist, to save several forks. - as_shell=$as_dir/$as_base - if { test -f "$as_shell" || test -f "$as_shell.exe"; } && - { $as_echo "$as_bourne_compatible""$as_required" | as_run=a "$as_shell"; } 2>/dev/null; then : - CONFIG_SHELL=$as_shell as_have_required=yes - if { $as_echo "$as_bourne_compatible""$as_suggested" | as_run=a "$as_shell"; } 2>/dev/null; then : - break 2 -fi -fi - done;; - esac - as_found=false -done -$as_found || { if { test -f "$SHELL" || test -f "$SHELL.exe"; } && - { $as_echo "$as_bourne_compatible""$as_required" | as_run=a "$SHELL"; } 2>/dev/null; then : - CONFIG_SHELL=$SHELL as_have_required=yes -fi; } -IFS=$as_save_IFS - - - if test "x$CONFIG_SHELL" != x; then : - export CONFIG_SHELL - # We cannot yet assume a decent shell, so we have to provide a -# neutralization value for shells without unset; and this also -# works around shells that cannot unset nonexistent variables. -# Preserve -v and -x to the replacement shell. -BASH_ENV=/dev/null -ENV=/dev/null -(unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV -case $- in # (((( - *v*x* | *x*v* ) as_opts=-vx ;; - *v* ) as_opts=-v ;; - *x* ) as_opts=-x ;; - * ) as_opts= ;; -esac -exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} -# Admittedly, this is quite paranoid, since all the known shells bail -# out after a failed `exec'. -$as_echo "$0: could not re-execute with $CONFIG_SHELL" >&2 -exit 255 -fi - - if test x$as_have_required = xno; then : - $as_echo "$0: This script requires a shell more modern than all" - $as_echo "$0: the shells that I found on your system." - if test x${ZSH_VERSION+set} = xset ; then - $as_echo "$0: In particular, zsh $ZSH_VERSION has bugs and should" - $as_echo "$0: be upgraded to zsh 4.3.4 or later." - else - $as_echo "$0: Please tell bug-autoconf@gnu.org and -$0: https://github.com/abraunegg/onedrive about your -$0: system, including any error possibly output before this -$0: message. Then install a modern shell, or manually run -$0: the script under such a shell if you do have one." - fi - exit 1 -fi -fi -fi -SHELL=${CONFIG_SHELL-/bin/sh} -export SHELL -# Unset more variables known to interfere with behavior of common tools. -CLICOLOR_FORCE= GREP_OPTIONS= -unset CLICOLOR_FORCE GREP_OPTIONS - -## --------------------- ## -## M4sh Shell Functions. ## -## --------------------- ## -# as_fn_unset VAR -# --------------- -# Portably unset VAR. -as_fn_unset () -{ - { eval $1=; unset $1;} -} -as_unset=as_fn_unset - -# as_fn_set_status STATUS -# ----------------------- -# Set $? to STATUS, without forking. -as_fn_set_status () -{ - return $1 -} # as_fn_set_status - -# as_fn_exit STATUS -# ----------------- -# Exit the shell with STATUS, even in a "trap 0" or "set -e" context. -as_fn_exit () -{ - set +e - as_fn_set_status $1 - exit $1 -} # as_fn_exit - -# as_fn_mkdir_p -# ------------- -# Create "$as_dir" as a directory, including parents if necessary. -as_fn_mkdir_p () -{ - - case $as_dir in #( - -*) as_dir=./$as_dir;; - esac - test -d "$as_dir" || eval $as_mkdir_p || { - as_dirs= - while :; do - case $as_dir in #( - *\'*) as_qdir=`$as_echo "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( - *) as_qdir=$as_dir;; - esac - as_dirs="'$as_qdir' $as_dirs" - as_dir=`$as_dirname -- "$as_dir" || -$as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ - X"$as_dir" : 'X\(//\)[^/]' \| \ - X"$as_dir" : 'X\(//\)$' \| \ - X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || -$as_echo X"$as_dir" | - sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ - s//\1/ - q - } - /^X\(\/\/\)[^/].*/{ - s//\1/ - q - } - /^X\(\/\/\)$/{ - s//\1/ - q - } - /^X\(\/\).*/{ - s//\1/ - q - } - s/.*/./; q'` - test -d "$as_dir" && break - done - test -z "$as_dirs" || eval "mkdir $as_dirs" - } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir" - - -} # as_fn_mkdir_p - -# as_fn_executable_p FILE -# ----------------------- -# Test if FILE is an executable regular file. -as_fn_executable_p () -{ - test -f "$1" && test -x "$1" -} # as_fn_executable_p -# as_fn_append VAR VALUE -# ---------------------- -# Append the text in VALUE to the end of the definition contained in VAR. Take -# advantage of any shell optimizations that allow amortized linear growth over -# repeated appends, instead of the typical quadratic growth present in naive -# implementations. -if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null; then : - eval 'as_fn_append () - { - eval $1+=\$2 - }' -else - as_fn_append () - { - eval $1=\$$1\$2 - } -fi # as_fn_append - -# as_fn_arith ARG... -# ------------------ -# Perform arithmetic evaluation on the ARGs, and store the result in the -# global $as_val. Take advantage of shells that can avoid forks. The arguments -# must be portable across $(()) and expr. -if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null; then : - eval 'as_fn_arith () - { - as_val=$(( $* )) - }' -else - as_fn_arith () - { - as_val=`expr "$@" || test $? -eq 1` - } -fi # as_fn_arith - - -# as_fn_error STATUS ERROR [LINENO LOG_FD] -# ---------------------------------------- -# Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are -# provided, also output the error to LOG_FD, referencing LINENO. Then exit the -# script with STATUS, using 1 if that was 0. -as_fn_error () -{ - as_status=$1; test $as_status -eq 0 && as_status=1 - if test "$4"; then - as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - $as_echo "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 - fi - $as_echo "$as_me: error: $2" >&2 - as_fn_exit $as_status -} # as_fn_error - -if expr a : '\(a\)' >/dev/null 2>&1 && - test "X`expr 00001 : '.*\(...\)'`" = X001; then - as_expr=expr -else - as_expr=false -fi - -if (basename -- /) >/dev/null 2>&1 && test "X`basename -- / 2>&1`" = "X/"; then - as_basename=basename -else - as_basename=false -fi - -if (as_dir=`dirname -- /` && test "X$as_dir" = X/) >/dev/null 2>&1; then - as_dirname=dirname -else - as_dirname=false -fi - -as_me=`$as_basename -- "$0" || -$as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ - X"$0" : 'X\(//\)$' \| \ - X"$0" : 'X\(/\)' \| . 2>/dev/null || -$as_echo X/"$0" | - sed '/^.*\/\([^/][^/]*\)\/*$/{ - s//\1/ - q - } - /^X\/\(\/\/\)$/{ - s//\1/ - q - } - /^X\/\(\/\).*/{ - s//\1/ - q - } - s/.*/./; q'` - -# Avoid depending upon Character Ranges. -as_cr_letters='abcdefghijklmnopqrstuvwxyz' -as_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ' -as_cr_Letters=$as_cr_letters$as_cr_LETTERS -as_cr_digits='0123456789' -as_cr_alnum=$as_cr_Letters$as_cr_digits - - - as_lineno_1=$LINENO as_lineno_1a=$LINENO - as_lineno_2=$LINENO as_lineno_2a=$LINENO - eval 'test "x$as_lineno_1'$as_run'" != "x$as_lineno_2'$as_run'" && - test "x`expr $as_lineno_1'$as_run' + 1`" = "x$as_lineno_2'$as_run'"' || { - # Blame Lee E. McMahon (1931-1989) for sed's syntax. :-) - sed -n ' - p - /[$]LINENO/= - ' <$as_myself | - sed ' - s/[$]LINENO.*/&-/ - t lineno - b - :lineno - N - :loop - s/[$]LINENO\([^'$as_cr_alnum'_].*\n\)\(.*\)/\2\1\2/ - t loop - s/-\n.*// - ' >$as_me.lineno && - chmod +x "$as_me.lineno" || - { $as_echo "$as_me: error: cannot create $as_me.lineno; rerun with a POSIX shell" >&2; as_fn_exit 1; } - - # If we had to re-execute with $CONFIG_SHELL, we're ensured to have - # already done that, so ensure we don't try to do so again and fall - # in an infinite loop. This has already happened in practice. - _as_can_reexec=no; export _as_can_reexec - # Don't try to exec as it changes $[0], causing all sort of problems - # (the dirname of $[0] is not the place where we might find the - # original and so on. Autoconf is especially sensitive to this). - . "./$as_me.lineno" - # Exit status is that of the last command. - exit -} - -ECHO_C= ECHO_N= ECHO_T= -case `echo -n x` in #((((( --n*) - case `echo 'xy\c'` in - *c*) ECHO_T=' ';; # ECHO_T is single tab character. - xy) ECHO_C='\c';; - *) echo `echo ksh88 bug on AIX 6.1` > /dev/null - ECHO_T=' ';; - esac;; -*) - ECHO_N='-n';; -esac - -rm -f conf$$ conf$$.exe conf$$.file -if test -d conf$$.dir; then - rm -f conf$$.dir/conf$$.file -else - rm -f conf$$.dir - mkdir conf$$.dir 2>/dev/null -fi -if (echo >conf$$.file) 2>/dev/null; then - if ln -s conf$$.file conf$$ 2>/dev/null; then - as_ln_s='ln -s' - # ... but there are two gotchas: - # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail. - # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable. - # In both cases, we have to default to `cp -pR'. - ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe || - as_ln_s='cp -pR' - elif ln conf$$.file conf$$ 2>/dev/null; then - as_ln_s=ln - else - as_ln_s='cp -pR' - fi -else - as_ln_s='cp -pR' -fi -rm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file -rmdir conf$$.dir 2>/dev/null - -if mkdir -p . 2>/dev/null; then - as_mkdir_p='mkdir -p "$as_dir"' -else - test -d ./-p && rmdir ./-p - as_mkdir_p=false -fi - -as_test_x='test -x' -as_executable_p=as_fn_executable_p - -# Sed expression to map a string onto a valid CPP name. -as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'" - -# Sed expression to map a string onto a valid variable name. -as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'" - - -test -n "$DJDIR" || exec 7<&0 &1 - -# Name of the host. -# hostname on some systems (SVR3.2, old GNU/Linux) returns a bogus exit status, -# so uname gets run too. -ac_hostname=`(hostname || uname -n) 2>/dev/null | sed 1q` - -# -# Initializations. -# -ac_default_prefix=/usr/local -ac_clean_files= -ac_config_libobj_dir=. -LIBOBJS= -cross_compiling=no -subdirs= -MFLAGS= -MAKEFLAGS= - -# Identity of this package. -PACKAGE_NAME='onedrive' -PACKAGE_TARNAME='onedrive' -PACKAGE_VERSION='v2.4.25' -PACKAGE_STRING='onedrive v2.4.25' -PACKAGE_BUGREPORT='https://github.com/abraunegg/onedrive' -PACKAGE_URL='' - -ac_unique_file="src/main.d" -ac_subst_vars='LTLIBOBJS -LIBOBJS -DEBUG -FISH_COMPLETION_DIR -ZSH_COMPLETION_DIR -BASH_COMPLETION_DIR -bashcompdir -COMPLETIONS -NOTIFICATIONS -notify_LIBS -notify_CFLAGS -HAVE_SYSTEMD -systemduserunitdir -systemdsystemunitdir -sqlite_LIBS -sqlite_CFLAGS -curl_LIBS -curl_CFLAGS -PACKAGE_DATE -DC_TYPE -PKG_CONFIG_LIBDIR -PKG_CONFIG_PATH -PKG_CONFIG -INSTALL_DATA -INSTALL_SCRIPT -INSTALL_PROGRAM -DCFLAGS -DC -target_alias -host_alias -build_alias -LIBS -ECHO_T -ECHO_N -ECHO_C -DEFS -mandir -localedir -libdir -psdir -pdfdir -dvidir -htmldir -infodir -docdir -oldincludedir -includedir -localstatedir -sharedstatedir -sysconfdir -datadir -datarootdir -libexecdir -sbindir -bindir -program_transform_name -prefix -exec_prefix -PACKAGE_URL -PACKAGE_BUGREPORT -PACKAGE_STRING -PACKAGE_VERSION -PACKAGE_TARNAME -PACKAGE_NAME -PATH_SEPARATOR -SHELL' -ac_subst_files='' -ac_user_opts=' -enable_option_checking -enable_version_check -with_systemdsystemunitdir -with_systemduserunitdir -enable_notifications -enable_completions -with_bash_completion_dir -with_zsh_completion_dir -with_fish_completion_dir -enable_debug -' - ac_precious_vars='build_alias -host_alias -target_alias -DC -DCFLAGS -PKG_CONFIG -PKG_CONFIG_PATH -PKG_CONFIG_LIBDIR -curl_CFLAGS -curl_LIBS -sqlite_CFLAGS -sqlite_LIBS -notify_CFLAGS -notify_LIBS -bashcompdir' - - -# Initialize some variables set by options. -ac_init_help= -ac_init_version=false -ac_unrecognized_opts= -ac_unrecognized_sep= -# The variables have the same names as the options, with -# dashes changed to underlines. -cache_file=/dev/null -exec_prefix=NONE -no_create= -no_recursion= -prefix=NONE -program_prefix=NONE -program_suffix=NONE -program_transform_name=s,x,x, -silent= -site= -srcdir= -verbose= -x_includes=NONE -x_libraries=NONE - -# Installation directory options. -# These are left unexpanded so users can "make install exec_prefix=/foo" -# and all the variables that are supposed to be based on exec_prefix -# by default will actually change. -# Use braces instead of parens because sh, perl, etc. also accept them. -# (The list follows the same order as the GNU Coding Standards.) -bindir='${exec_prefix}/bin' -sbindir='${exec_prefix}/sbin' -libexecdir='${exec_prefix}/libexec' -datarootdir='${prefix}/share' -datadir='${datarootdir}' -sysconfdir='${prefix}/etc' -sharedstatedir='${prefix}/com' -localstatedir='${prefix}/var' -includedir='${prefix}/include' -oldincludedir='/usr/include' -docdir='${datarootdir}/doc/${PACKAGE_TARNAME}' -infodir='${datarootdir}/info' -htmldir='${docdir}' -dvidir='${docdir}' -pdfdir='${docdir}' -psdir='${docdir}' -libdir='${exec_prefix}/lib' -localedir='${datarootdir}/locale' -mandir='${datarootdir}/man' - -ac_prev= -ac_dashdash= -for ac_option -do - # If the previous option needs an argument, assign it. - if test -n "$ac_prev"; then - eval $ac_prev=\$ac_option - ac_prev= - continue - fi - - case $ac_option in - *=?*) ac_optarg=`expr "X$ac_option" : '[^=]*=\(.*\)'` ;; - *=) ac_optarg= ;; - *) ac_optarg=yes ;; - esac - - # Accept the important Cygnus configure options, so we can diagnose typos. - - case $ac_dashdash$ac_option in - --) - ac_dashdash=yes ;; - - -bindir | --bindir | --bindi | --bind | --bin | --bi) - ac_prev=bindir ;; - -bindir=* | --bindir=* | --bindi=* | --bind=* | --bin=* | --bi=*) - bindir=$ac_optarg ;; - - -build | --build | --buil | --bui | --bu) - ac_prev=build_alias ;; - -build=* | --build=* | --buil=* | --bui=* | --bu=*) - build_alias=$ac_optarg ;; - - -cache-file | --cache-file | --cache-fil | --cache-fi \ - | --cache-f | --cache- | --cache | --cach | --cac | --ca | --c) - ac_prev=cache_file ;; - -cache-file=* | --cache-file=* | --cache-fil=* | --cache-fi=* \ - | --cache-f=* | --cache-=* | --cache=* | --cach=* | --cac=* | --ca=* | --c=*) - cache_file=$ac_optarg ;; - - --config-cache | -C) - cache_file=config.cache ;; - - -datadir | --datadir | --datadi | --datad) - ac_prev=datadir ;; - -datadir=* | --datadir=* | --datadi=* | --datad=*) - datadir=$ac_optarg ;; - - -datarootdir | --datarootdir | --datarootdi | --datarootd | --dataroot \ - | --dataroo | --dataro | --datar) - ac_prev=datarootdir ;; - -datarootdir=* | --datarootdir=* | --datarootdi=* | --datarootd=* \ - | --dataroot=* | --dataroo=* | --dataro=* | --datar=*) - datarootdir=$ac_optarg ;; - - -disable-* | --disable-*) - ac_useropt=`expr "x$ac_option" : 'x-*disable-\(.*\)'` - # Reject names that are not valid shell variable names. - expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && - as_fn_error $? "invalid feature name: $ac_useropt" - ac_useropt_orig=$ac_useropt - ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` - case $ac_user_opts in - *" -"enable_$ac_useropt" -"*) ;; - *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--disable-$ac_useropt_orig" - ac_unrecognized_sep=', ';; - esac - eval enable_$ac_useropt=no ;; - - -docdir | --docdir | --docdi | --doc | --do) - ac_prev=docdir ;; - -docdir=* | --docdir=* | --docdi=* | --doc=* | --do=*) - docdir=$ac_optarg ;; - - -dvidir | --dvidir | --dvidi | --dvid | --dvi | --dv) - ac_prev=dvidir ;; - -dvidir=* | --dvidir=* | --dvidi=* | --dvid=* | --dvi=* | --dv=*) - dvidir=$ac_optarg ;; - - -enable-* | --enable-*) - ac_useropt=`expr "x$ac_option" : 'x-*enable-\([^=]*\)'` - # Reject names that are not valid shell variable names. - expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && - as_fn_error $? "invalid feature name: $ac_useropt" - ac_useropt_orig=$ac_useropt - ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` - case $ac_user_opts in - *" -"enable_$ac_useropt" -"*) ;; - *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--enable-$ac_useropt_orig" - ac_unrecognized_sep=', ';; - esac - eval enable_$ac_useropt=\$ac_optarg ;; - - -exec-prefix | --exec_prefix | --exec-prefix | --exec-prefi \ - | --exec-pref | --exec-pre | --exec-pr | --exec-p | --exec- \ - | --exec | --exe | --ex) - ac_prev=exec_prefix ;; - -exec-prefix=* | --exec_prefix=* | --exec-prefix=* | --exec-prefi=* \ - | --exec-pref=* | --exec-pre=* | --exec-pr=* | --exec-p=* | --exec-=* \ - | --exec=* | --exe=* | --ex=*) - exec_prefix=$ac_optarg ;; - - -gas | --gas | --ga | --g) - # Obsolete; use --with-gas. - with_gas=yes ;; - - -help | --help | --hel | --he | -h) - ac_init_help=long ;; - -help=r* | --help=r* | --hel=r* | --he=r* | -hr*) - ac_init_help=recursive ;; - -help=s* | --help=s* | --hel=s* | --he=s* | -hs*) - ac_init_help=short ;; - - -host | --host | --hos | --ho) - ac_prev=host_alias ;; - -host=* | --host=* | --hos=* | --ho=*) - host_alias=$ac_optarg ;; - - -htmldir | --htmldir | --htmldi | --htmld | --html | --htm | --ht) - ac_prev=htmldir ;; - -htmldir=* | --htmldir=* | --htmldi=* | --htmld=* | --html=* | --htm=* \ - | --ht=*) - htmldir=$ac_optarg ;; - - -includedir | --includedir | --includedi | --included | --include \ - | --includ | --inclu | --incl | --inc) - ac_prev=includedir ;; - -includedir=* | --includedir=* | --includedi=* | --included=* | --include=* \ - | --includ=* | --inclu=* | --incl=* | --inc=*) - includedir=$ac_optarg ;; - - -infodir | --infodir | --infodi | --infod | --info | --inf) - ac_prev=infodir ;; - -infodir=* | --infodir=* | --infodi=* | --infod=* | --info=* | --inf=*) - infodir=$ac_optarg ;; - - -libdir | --libdir | --libdi | --libd) - ac_prev=libdir ;; - -libdir=* | --libdir=* | --libdi=* | --libd=*) - libdir=$ac_optarg ;; - - -libexecdir | --libexecdir | --libexecdi | --libexecd | --libexec \ - | --libexe | --libex | --libe) - ac_prev=libexecdir ;; - -libexecdir=* | --libexecdir=* | --libexecdi=* | --libexecd=* | --libexec=* \ - | --libexe=* | --libex=* | --libe=*) - libexecdir=$ac_optarg ;; - - -localedir | --localedir | --localedi | --localed | --locale) - ac_prev=localedir ;; - -localedir=* | --localedir=* | --localedi=* | --localed=* | --locale=*) - localedir=$ac_optarg ;; - - -localstatedir | --localstatedir | --localstatedi | --localstated \ - | --localstate | --localstat | --localsta | --localst | --locals) - ac_prev=localstatedir ;; - -localstatedir=* | --localstatedir=* | --localstatedi=* | --localstated=* \ - | --localstate=* | --localstat=* | --localsta=* | --localst=* | --locals=*) - localstatedir=$ac_optarg ;; - - -mandir | --mandir | --mandi | --mand | --man | --ma | --m) - ac_prev=mandir ;; - -mandir=* | --mandir=* | --mandi=* | --mand=* | --man=* | --ma=* | --m=*) - mandir=$ac_optarg ;; - - -nfp | --nfp | --nf) - # Obsolete; use --without-fp. - with_fp=no ;; - - -no-create | --no-create | --no-creat | --no-crea | --no-cre \ - | --no-cr | --no-c | -n) - no_create=yes ;; - - -no-recursion | --no-recursion | --no-recursio | --no-recursi \ - | --no-recurs | --no-recur | --no-recu | --no-rec | --no-re | --no-r) - no_recursion=yes ;; - - -oldincludedir | --oldincludedir | --oldincludedi | --oldincluded \ - | --oldinclude | --oldinclud | --oldinclu | --oldincl | --oldinc \ - | --oldin | --oldi | --old | --ol | --o) - ac_prev=oldincludedir ;; - -oldincludedir=* | --oldincludedir=* | --oldincludedi=* | --oldincluded=* \ - | --oldinclude=* | --oldinclud=* | --oldinclu=* | --oldincl=* | --oldinc=* \ - | --oldin=* | --oldi=* | --old=* | --ol=* | --o=*) - oldincludedir=$ac_optarg ;; - - -prefix | --prefix | --prefi | --pref | --pre | --pr | --p) - ac_prev=prefix ;; - -prefix=* | --prefix=* | --prefi=* | --pref=* | --pre=* | --pr=* | --p=*) - prefix=$ac_optarg ;; - - -program-prefix | --program-prefix | --program-prefi | --program-pref \ - | --program-pre | --program-pr | --program-p) - ac_prev=program_prefix ;; - -program-prefix=* | --program-prefix=* | --program-prefi=* \ - | --program-pref=* | --program-pre=* | --program-pr=* | --program-p=*) - program_prefix=$ac_optarg ;; - - -program-suffix | --program-suffix | --program-suffi | --program-suff \ - | --program-suf | --program-su | --program-s) - ac_prev=program_suffix ;; - -program-suffix=* | --program-suffix=* | --program-suffi=* \ - | --program-suff=* | --program-suf=* | --program-su=* | --program-s=*) - program_suffix=$ac_optarg ;; - - -program-transform-name | --program-transform-name \ - | --program-transform-nam | --program-transform-na \ - | --program-transform-n | --program-transform- \ - | --program-transform | --program-transfor \ - | --program-transfo | --program-transf \ - | --program-trans | --program-tran \ - | --progr-tra | --program-tr | --program-t) - ac_prev=program_transform_name ;; - -program-transform-name=* | --program-transform-name=* \ - | --program-transform-nam=* | --program-transform-na=* \ - | --program-transform-n=* | --program-transform-=* \ - | --program-transform=* | --program-transfor=* \ - | --program-transfo=* | --program-transf=* \ - | --program-trans=* | --program-tran=* \ - | --progr-tra=* | --program-tr=* | --program-t=*) - program_transform_name=$ac_optarg ;; - - -pdfdir | --pdfdir | --pdfdi | --pdfd | --pdf | --pd) - ac_prev=pdfdir ;; - -pdfdir=* | --pdfdir=* | --pdfdi=* | --pdfd=* | --pdf=* | --pd=*) - pdfdir=$ac_optarg ;; - - -psdir | --psdir | --psdi | --psd | --ps) - ac_prev=psdir ;; - -psdir=* | --psdir=* | --psdi=* | --psd=* | --ps=*) - psdir=$ac_optarg ;; - - -q | -quiet | --quiet | --quie | --qui | --qu | --q \ - | -silent | --silent | --silen | --sile | --sil) - silent=yes ;; - - -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb) - ac_prev=sbindir ;; - -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \ - | --sbi=* | --sb=*) - sbindir=$ac_optarg ;; - - -sharedstatedir | --sharedstatedir | --sharedstatedi \ - | --sharedstated | --sharedstate | --sharedstat | --sharedsta \ - | --sharedst | --shareds | --shared | --share | --shar \ - | --sha | --sh) - ac_prev=sharedstatedir ;; - -sharedstatedir=* | --sharedstatedir=* | --sharedstatedi=* \ - | --sharedstated=* | --sharedstate=* | --sharedstat=* | --sharedsta=* \ - | --sharedst=* | --shareds=* | --shared=* | --share=* | --shar=* \ - | --sha=* | --sh=*) - sharedstatedir=$ac_optarg ;; - - -site | --site | --sit) - ac_prev=site ;; - -site=* | --site=* | --sit=*) - site=$ac_optarg ;; - - -srcdir | --srcdir | --srcdi | --srcd | --src | --sr) - ac_prev=srcdir ;; - -srcdir=* | --srcdir=* | --srcdi=* | --srcd=* | --src=* | --sr=*) - srcdir=$ac_optarg ;; - - -sysconfdir | --sysconfdir | --sysconfdi | --sysconfd | --sysconf \ - | --syscon | --sysco | --sysc | --sys | --sy) - ac_prev=sysconfdir ;; - -sysconfdir=* | --sysconfdir=* | --sysconfdi=* | --sysconfd=* | --sysconf=* \ - | --syscon=* | --sysco=* | --sysc=* | --sys=* | --sy=*) - sysconfdir=$ac_optarg ;; - - -target | --target | --targe | --targ | --tar | --ta | --t) - ac_prev=target_alias ;; - -target=* | --target=* | --targe=* | --targ=* | --tar=* | --ta=* | --t=*) - target_alias=$ac_optarg ;; - - -v | -verbose | --verbose | --verbos | --verbo | --verb) - verbose=yes ;; - - -version | --version | --versio | --versi | --vers | -V) - ac_init_version=: ;; - - -with-* | --with-*) - ac_useropt=`expr "x$ac_option" : 'x-*with-\([^=]*\)'` - # Reject names that are not valid shell variable names. - expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && - as_fn_error $? "invalid package name: $ac_useropt" - ac_useropt_orig=$ac_useropt - ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` - case $ac_user_opts in - *" -"with_$ac_useropt" -"*) ;; - *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--with-$ac_useropt_orig" - ac_unrecognized_sep=', ';; - esac - eval with_$ac_useropt=\$ac_optarg ;; - - -without-* | --without-*) - ac_useropt=`expr "x$ac_option" : 'x-*without-\(.*\)'` - # Reject names that are not valid shell variable names. - expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && - as_fn_error $? "invalid package name: $ac_useropt" - ac_useropt_orig=$ac_useropt - ac_useropt=`$as_echo "$ac_useropt" | sed 's/[-+.]/_/g'` - case $ac_user_opts in - *" -"with_$ac_useropt" -"*) ;; - *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--without-$ac_useropt_orig" - ac_unrecognized_sep=', ';; - esac - eval with_$ac_useropt=no ;; - - --x) - # Obsolete; use --with-x. - with_x=yes ;; - - -x-includes | --x-includes | --x-include | --x-includ | --x-inclu \ - | --x-incl | --x-inc | --x-in | --x-i) - ac_prev=x_includes ;; - -x-includes=* | --x-includes=* | --x-include=* | --x-includ=* | --x-inclu=* \ - | --x-incl=* | --x-inc=* | --x-in=* | --x-i=*) - x_includes=$ac_optarg ;; - - -x-libraries | --x-libraries | --x-librarie | --x-librari \ - | --x-librar | --x-libra | --x-libr | --x-lib | --x-li | --x-l) - ac_prev=x_libraries ;; - -x-libraries=* | --x-libraries=* | --x-librarie=* | --x-librari=* \ - | --x-librar=* | --x-libra=* | --x-libr=* | --x-lib=* | --x-li=* | --x-l=*) - x_libraries=$ac_optarg ;; - - -*) as_fn_error $? "unrecognized option: \`$ac_option' -Try \`$0 --help' for more information" - ;; - - *=*) - ac_envvar=`expr "x$ac_option" : 'x\([^=]*\)='` - # Reject names that are not valid shell variable names. - case $ac_envvar in #( - '' | [0-9]* | *[!_$as_cr_alnum]* ) - as_fn_error $? "invalid variable name: \`$ac_envvar'" ;; - esac - eval $ac_envvar=\$ac_optarg - export $ac_envvar ;; - - *) - # FIXME: should be removed in autoconf 3.0. - $as_echo "$as_me: WARNING: you should use --build, --host, --target" >&2 - expr "x$ac_option" : ".*[^-._$as_cr_alnum]" >/dev/null && - $as_echo "$as_me: WARNING: invalid host type: $ac_option" >&2 - : "${build_alias=$ac_option} ${host_alias=$ac_option} ${target_alias=$ac_option}" - ;; - - esac -done - -if test -n "$ac_prev"; then - ac_option=--`echo $ac_prev | sed 's/_/-/g'` - as_fn_error $? "missing argument to $ac_option" -fi - -if test -n "$ac_unrecognized_opts"; then - case $enable_option_checking in - no) ;; - fatal) as_fn_error $? "unrecognized options: $ac_unrecognized_opts" ;; - *) $as_echo "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2 ;; - esac -fi - -# Check all directory arguments for consistency. -for ac_var in exec_prefix prefix bindir sbindir libexecdir datarootdir \ - datadir sysconfdir sharedstatedir localstatedir includedir \ - oldincludedir docdir infodir htmldir dvidir pdfdir psdir \ - libdir localedir mandir -do - eval ac_val=\$$ac_var - # Remove trailing slashes. - case $ac_val in - */ ) - ac_val=`expr "X$ac_val" : 'X\(.*[^/]\)' \| "X$ac_val" : 'X\(.*\)'` - eval $ac_var=\$ac_val;; - esac - # Be sure to have absolute directory names. - case $ac_val in - [\\/$]* | ?:[\\/]* ) continue;; - NONE | '' ) case $ac_var in *prefix ) continue;; esac;; - esac - as_fn_error $? "expected an absolute directory name for --$ac_var: $ac_val" -done - -# There might be people who depend on the old broken behavior: `$host' -# used to hold the argument of --host etc. -# FIXME: To remove some day. -build=$build_alias -host=$host_alias -target=$target_alias - -# FIXME: To remove some day. -if test "x$host_alias" != x; then - if test "x$build_alias" = x; then - cross_compiling=maybe - elif test "x$build_alias" != "x$host_alias"; then - cross_compiling=yes - fi -fi - -ac_tool_prefix= -test -n "$host_alias" && ac_tool_prefix=$host_alias- - -test "$silent" = yes && exec 6>/dev/null - - -ac_pwd=`pwd` && test -n "$ac_pwd" && -ac_ls_di=`ls -di .` && -ac_pwd_ls_di=`cd "$ac_pwd" && ls -di .` || - as_fn_error $? "working directory cannot be determined" -test "X$ac_ls_di" = "X$ac_pwd_ls_di" || - as_fn_error $? "pwd does not report name of working directory" - - -# Find the source files, if location was not specified. -if test -z "$srcdir"; then - ac_srcdir_defaulted=yes - # Try the directory containing this script, then the parent directory. - ac_confdir=`$as_dirname -- "$as_myself" || -$as_expr X"$as_myself" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ - X"$as_myself" : 'X\(//\)[^/]' \| \ - X"$as_myself" : 'X\(//\)$' \| \ - X"$as_myself" : 'X\(/\)' \| . 2>/dev/null || -$as_echo X"$as_myself" | - sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ - s//\1/ - q - } - /^X\(\/\/\)[^/].*/{ - s//\1/ - q - } - /^X\(\/\/\)$/{ - s//\1/ - q - } - /^X\(\/\).*/{ - s//\1/ - q - } - s/.*/./; q'` - srcdir=$ac_confdir - if test ! -r "$srcdir/$ac_unique_file"; then - srcdir=.. - fi -else - ac_srcdir_defaulted=no -fi -if test ! -r "$srcdir/$ac_unique_file"; then - test "$ac_srcdir_defaulted" = yes && srcdir="$ac_confdir or .." - as_fn_error $? "cannot find sources ($ac_unique_file) in $srcdir" -fi -ac_msg="sources are in $srcdir, but \`cd $srcdir' does not work" -ac_abs_confdir=`( - cd "$srcdir" && test -r "./$ac_unique_file" || as_fn_error $? "$ac_msg" - pwd)` -# When building in place, set srcdir=. -if test "$ac_abs_confdir" = "$ac_pwd"; then - srcdir=. -fi -# Remove unnecessary trailing slashes from srcdir. -# Double slashes in file names in object file debugging info -# mess up M-x gdb in Emacs. -case $srcdir in -*/) srcdir=`expr "X$srcdir" : 'X\(.*[^/]\)' \| "X$srcdir" : 'X\(.*\)'`;; -esac -for ac_var in $ac_precious_vars; do - eval ac_env_${ac_var}_set=\${${ac_var}+set} - eval ac_env_${ac_var}_value=\$${ac_var} - eval ac_cv_env_${ac_var}_set=\${${ac_var}+set} - eval ac_cv_env_${ac_var}_value=\$${ac_var} -done - -# -# Report the --help message. -# -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.25 to adapt to many kinds of systems. - -Usage: $0 [OPTION]... [VAR=VALUE]... - -To assign environment variables (e.g., CC, CFLAGS...), specify them as -VAR=VALUE. See below for descriptions of some of the useful variables. - -Defaults for the options are specified in brackets. - -Configuration: - -h, --help display this help and exit - --help=short display options specific to this package - --help=recursive display the short help of all the included packages - -V, --version display version information and exit - -q, --quiet, --silent do not print \`checking ...' messages - --cache-file=FILE cache test results in FILE [disabled] - -C, --config-cache alias for \`--cache-file=config.cache' - -n, --no-create do not create output files - --srcdir=DIR find the sources in DIR [configure dir or \`..'] - -Installation directories: - --prefix=PREFIX install architecture-independent files in PREFIX - [$ac_default_prefix] - --exec-prefix=EPREFIX install architecture-dependent files in EPREFIX - [PREFIX] - -By default, \`make install' will install all the files in -\`$ac_default_prefix/bin', \`$ac_default_prefix/lib' etc. You can specify -an installation prefix other than \`$ac_default_prefix' using \`--prefix', -for instance \`--prefix=\$HOME'. - -For better control, use the options below. - -Fine tuning of the installation directories: - --bindir=DIR user executables [EPREFIX/bin] - --sbindir=DIR system admin executables [EPREFIX/sbin] - --libexecdir=DIR program executables [EPREFIX/libexec] - --sysconfdir=DIR read-only single-machine data [PREFIX/etc] - --sharedstatedir=DIR modifiable architecture-independent data [PREFIX/com] - --localstatedir=DIR modifiable single-machine data [PREFIX/var] - --libdir=DIR object code libraries [EPREFIX/lib] - --includedir=DIR C header files [PREFIX/include] - --oldincludedir=DIR C header files for non-gcc [/usr/include] - --datarootdir=DIR read-only arch.-independent data root [PREFIX/share] - --datadir=DIR read-only architecture-independent data [DATAROOTDIR] - --infodir=DIR info documentation [DATAROOTDIR/info] - --localedir=DIR locale-dependent data [DATAROOTDIR/locale] - --mandir=DIR man documentation [DATAROOTDIR/man] - --docdir=DIR documentation root [DATAROOTDIR/doc/onedrive] - --htmldir=DIR html documentation [DOCDIR] - --dvidir=DIR dvi documentation [DOCDIR] - --pdfdir=DIR pdf documentation [DOCDIR] - --psdir=DIR ps documentation [DOCDIR] -_ACEOF - - cat <<\_ACEOF -_ACEOF -fi - -if test -n "$ac_init_help"; then - case $ac_init_help in - short | recursive ) echo "Configuration of onedrive v2.4.25:";; - esac - cat <<\_ACEOF - -Optional Features: - --disable-option-checking ignore unrecognized --enable/--with options - --disable-FEATURE do not include FEATURE (same as --enable-FEATURE=no) - --enable-FEATURE[=ARG] include FEATURE [ARG=yes] - --disable-version-check Disable checks of compiler version during configure - time - --enable-notifications Enable desktop notifications via libnotify - --enable-completions Install shell completions for bash, zsh, and fish - --enable-debug Pass debug option to the compiler - -Optional Packages: - --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] - --without-PACKAGE do not use PACKAGE (same as --with-PACKAGE=no) - --with-systemdsystemunitdir=DIR - Directory for systemd system service files - --with-systemduserunitdir=DIR - Directory for systemd user service files - --with-bash-completion-dir=DIR - Directory for bash completion files - --with-zsh-completion-dir=DIR - Directory for zsh completion files - --with-fish-completion-dir=DIR - Directory for fish completion files - -Some influential environment variables: - DC D compiler executable - DCFLAGS flags for D compiler - PKG_CONFIG path to pkg-config utility - PKG_CONFIG_PATH - directories to add to pkg-config's search path - PKG_CONFIG_LIBDIR - path overriding pkg-config's built-in search path - curl_CFLAGS C compiler flags for curl, overriding pkg-config - curl_LIBS linker flags for curl, overriding pkg-config - sqlite_CFLAGS - C compiler flags for sqlite, overriding pkg-config - sqlite_LIBS linker flags for sqlite, overriding pkg-config - notify_CFLAGS - C compiler flags for notify, overriding pkg-config - notify_LIBS linker flags for notify, overriding pkg-config - bashcompdir value of completionsdir for bash-completion, overriding - pkg-config - -Use these variables to override the choices made by `configure' or to help -it to find libraries and programs with nonstandard names/locations. - -Report bugs to . -_ACEOF -ac_status=$? -fi - -if test "$ac_init_help" = "recursive"; then - # If there are subdirs, report their specific --help. - for ac_dir in : $ac_subdirs_all; do test "x$ac_dir" = x: && continue - test -d "$ac_dir" || - { cd "$srcdir" && ac_pwd=`pwd` && srcdir=. && test -d "$ac_dir"; } || - continue - ac_builddir=. - -case "$ac_dir" in -.) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; -*) - ac_dir_suffix=/`$as_echo "$ac_dir" | sed 's|^\.[\\/]||'` - # A ".." for each directory in $ac_dir_suffix. - ac_top_builddir_sub=`$as_echo "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` - case $ac_top_builddir_sub in - "") ac_top_builddir_sub=. ac_top_build_prefix= ;; - *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; - esac ;; -esac -ac_abs_top_builddir=$ac_pwd -ac_abs_builddir=$ac_pwd$ac_dir_suffix -# for backward compatibility: -ac_top_builddir=$ac_top_build_prefix - -case $srcdir in - .) # We are building in place. - ac_srcdir=. - ac_top_srcdir=$ac_top_builddir_sub - ac_abs_top_srcdir=$ac_pwd ;; - [\\/]* | ?:[\\/]* ) # Absolute name. - ac_srcdir=$srcdir$ac_dir_suffix; - ac_top_srcdir=$srcdir - ac_abs_top_srcdir=$srcdir ;; - *) # Relative name. - ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix - ac_top_srcdir=$ac_top_build_prefix$srcdir - ac_abs_top_srcdir=$ac_pwd/$srcdir ;; -esac -ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix - - cd "$ac_dir" || { ac_status=$?; continue; } - # Check for guested configure. - if test -f "$ac_srcdir/configure.gnu"; then - echo && - $SHELL "$ac_srcdir/configure.gnu" --help=recursive - elif test -f "$ac_srcdir/configure"; then - echo && - $SHELL "$ac_srcdir/configure" --help=recursive - else - $as_echo "$as_me: WARNING: no configuration information is in $ac_dir" >&2 - fi || ac_status=$? - cd "$ac_pwd" || { ac_status=$?; break; } - done -fi - -test -n "$ac_init_help" && exit $ac_status -if $ac_init_version; then - cat <<\_ACEOF -onedrive configure v2.4.25 -generated by GNU Autoconf 2.69 - -Copyright (C) 2012 Free Software Foundation, Inc. -This configure script is free software; the Free Software Foundation -gives unlimited permission to copy, distribute and modify it. -_ACEOF - exit -fi - -## ------------------------ ## -## Autoconf initialization. ## -## ------------------------ ## -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.25, which was -generated by GNU Autoconf 2.69. Invocation command line was - - $ $0 $@ - -_ACEOF -exec 5>>config.log -{ -cat <<_ASUNAME -## --------- ## -## Platform. ## -## --------- ## - -hostname = `(hostname || uname -n) 2>/dev/null | sed 1q` -uname -m = `(uname -m) 2>/dev/null || echo unknown` -uname -r = `(uname -r) 2>/dev/null || echo unknown` -uname -s = `(uname -s) 2>/dev/null || echo unknown` -uname -v = `(uname -v) 2>/dev/null || echo unknown` - -/usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null || echo unknown` -/bin/uname -X = `(/bin/uname -X) 2>/dev/null || echo unknown` - -/bin/arch = `(/bin/arch) 2>/dev/null || echo unknown` -/usr/bin/arch -k = `(/usr/bin/arch -k) 2>/dev/null || echo unknown` -/usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null || echo unknown` -/usr/bin/hostinfo = `(/usr/bin/hostinfo) 2>/dev/null || echo unknown` -/bin/machine = `(/bin/machine) 2>/dev/null || echo unknown` -/usr/bin/oslevel = `(/usr/bin/oslevel) 2>/dev/null || echo unknown` -/bin/universe = `(/bin/universe) 2>/dev/null || echo unknown` - -_ASUNAME - -as_save_IFS=$IFS; IFS=$PATH_SEPARATOR -for as_dir in $PATH -do - IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - $as_echo "PATH: $as_dir" - done -IFS=$as_save_IFS - -} >&5 - -cat >&5 <<_ACEOF - - -## ----------- ## -## Core tests. ## -## ----------- ## - -_ACEOF - - -# Keep a trace of the command line. -# Strip out --no-create and --no-recursion so they do not pile up. -# Strip out --silent because we don't want to record it for future runs. -# Also quote any args containing shell meta-characters. -# Make two passes to allow for proper duplicate-argument suppression. -ac_configure_args= -ac_configure_args0= -ac_configure_args1= -ac_must_keep_next=false -for ac_pass in 1 2 -do - for ac_arg - do - case $ac_arg in - -no-create | --no-c* | -n | -no-recursion | --no-r*) continue ;; - -q | -quiet | --quiet | --quie | --qui | --qu | --q \ - | -silent | --silent | --silen | --sile | --sil) - continue ;; - *\'*) - ac_arg=`$as_echo "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; - esac - case $ac_pass in - 1) as_fn_append ac_configure_args0 " '$ac_arg'" ;; - 2) - as_fn_append ac_configure_args1 " '$ac_arg'" - if test $ac_must_keep_next = true; then - ac_must_keep_next=false # Got value, back to normal. - else - case $ac_arg in - *=* | --config-cache | -C | -disable-* | --disable-* \ - | -enable-* | --enable-* | -gas | --g* | -nfp | --nf* \ - | -q | -quiet | --q* | -silent | --sil* | -v | -verb* \ - | -with-* | --with-* | -without-* | --without-* | --x) - case "$ac_configure_args0 " in - "$ac_configure_args1"*" '$ac_arg' "* ) continue ;; - esac - ;; - -* ) ac_must_keep_next=true ;; - esac - fi - as_fn_append ac_configure_args " '$ac_arg'" - ;; - esac - done -done -{ ac_configure_args0=; unset ac_configure_args0;} -{ ac_configure_args1=; unset ac_configure_args1;} - -# When interrupted or exit'd, cleanup temporary files, and complete -# config.log. We remove comments because anyway the quotes in there -# would cause problems or look ugly. -# WARNING: Use '\'' to represent an apostrophe within the trap. -# WARNING: Do not start the trap code with a newline, due to a FreeBSD 4.0 bug. -trap 'exit_status=$? - # Save into config.log some information that might help in debugging. - { - echo - - $as_echo "## ---------------- ## -## Cache variables. ## -## ---------------- ##" - echo - # The following way of writing the cache mishandles newlines in values, -( - for ac_var in `(set) 2>&1 | sed -n '\''s/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'\''`; do - eval ac_val=\$$ac_var - case $ac_val in #( - *${as_nl}*) - case $ac_var in #( - *_cv_*) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 -$as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; - esac - case $ac_var in #( - _ | IFS | as_nl) ;; #( - BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #( - *) { eval $ac_var=; unset $ac_var;} ;; - esac ;; - esac - done - (set) 2>&1 | - case $as_nl`(ac_space='\'' '\''; set) 2>&1` in #( - *${as_nl}ac_space=\ *) - sed -n \ - "s/'\''/'\''\\\\'\'''\''/g; - s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\''\\2'\''/p" - ;; #( - *) - sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" - ;; - esac | - sort -) - echo - - $as_echo "## ----------------- ## -## Output variables. ## -## ----------------- ##" - echo - for ac_var in $ac_subst_vars - do - eval ac_val=\$$ac_var - case $ac_val in - *\'\''*) ac_val=`$as_echo "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; - esac - $as_echo "$ac_var='\''$ac_val'\''" - done | sort - echo - - if test -n "$ac_subst_files"; then - $as_echo "## ------------------- ## -## File substitutions. ## -## ------------------- ##" - echo - for ac_var in $ac_subst_files - do - eval ac_val=\$$ac_var - case $ac_val in - *\'\''*) ac_val=`$as_echo "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; - esac - $as_echo "$ac_var='\''$ac_val'\''" - done | sort - echo - fi - - if test -s confdefs.h; then - $as_echo "## ----------- ## -## confdefs.h. ## -## ----------- ##" - echo - cat confdefs.h - echo - fi - test "$ac_signal" != 0 && - $as_echo "$as_me: caught signal $ac_signal" - $as_echo "$as_me: exit $exit_status" - } >&5 - rm -f core *.core core.conftest.* && - rm -f -r conftest* confdefs* conf$$* $ac_clean_files && - exit $exit_status -' 0 -for ac_signal in 1 2 13 15; do - trap 'ac_signal='$ac_signal'; as_fn_exit 1' $ac_signal -done -ac_signal=0 - -# confdefs.h avoids OS command line length limits that DEFS can exceed. -rm -f -r conftest* confdefs.h - -$as_echo "/* confdefs.h */" > confdefs.h - -# Predefined preprocessor variables. - -cat >>confdefs.h <<_ACEOF -#define PACKAGE_NAME "$PACKAGE_NAME" -_ACEOF - -cat >>confdefs.h <<_ACEOF -#define PACKAGE_TARNAME "$PACKAGE_TARNAME" -_ACEOF - -cat >>confdefs.h <<_ACEOF -#define PACKAGE_VERSION "$PACKAGE_VERSION" -_ACEOF - -cat >>confdefs.h <<_ACEOF -#define PACKAGE_STRING "$PACKAGE_STRING" -_ACEOF - -cat >>confdefs.h <<_ACEOF -#define PACKAGE_BUGREPORT "$PACKAGE_BUGREPORT" -_ACEOF - -cat >>confdefs.h <<_ACEOF -#define PACKAGE_URL "$PACKAGE_URL" -_ACEOF - - -# Let the site file select an alternate cache file if it wants to. -# Prefer an explicitly selected file to automatically selected ones. -ac_site_file1=NONE -ac_site_file2=NONE -if test -n "$CONFIG_SITE"; then - # We do not want a PATH search for config.site. - case $CONFIG_SITE in #(( - -*) ac_site_file1=./$CONFIG_SITE;; - */*) ac_site_file1=$CONFIG_SITE;; - *) ac_site_file1=./$CONFIG_SITE;; - esac -elif test "x$prefix" != xNONE; then - ac_site_file1=$prefix/share/config.site - ac_site_file2=$prefix/etc/config.site -else - ac_site_file1=$ac_default_prefix/share/config.site - ac_site_file2=$ac_default_prefix/etc/config.site -fi -for ac_site_file in "$ac_site_file1" "$ac_site_file2" -do - test "x$ac_site_file" = xNONE && continue - if test /dev/null != "$ac_site_file" && test -r "$ac_site_file"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5 -$as_echo "$as_me: loading site script $ac_site_file" >&6;} - sed 's/^/| /' "$ac_site_file" >&5 - . "$ac_site_file" \ - || { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} -as_fn_error $? "failed to load site script $ac_site_file -See \`config.log' for more details" "$LINENO" 5; } - fi -done - -if test -r "$cache_file"; then - # Some versions of bash will fail to source /dev/null (special files - # actually), so we avoid doing that. DJGPP emulates it as a regular file. - if test /dev/null != "$cache_file" && test -f "$cache_file"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5 -$as_echo "$as_me: loading cache $cache_file" >&6;} - case $cache_file in - [\\/]* | ?:[\\/]* ) . "$cache_file";; - *) . "./$cache_file";; - esac - fi -else - { $as_echo "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5 -$as_echo "$as_me: creating cache $cache_file" >&6;} - >$cache_file -fi - -# Check that the precious variables saved in the cache have kept the same -# value. -ac_cache_corrupted=false -for ac_var in $ac_precious_vars; do - eval ac_old_set=\$ac_cv_env_${ac_var}_set - eval ac_new_set=\$ac_env_${ac_var}_set - eval ac_old_val=\$ac_cv_env_${ac_var}_value - eval ac_new_val=\$ac_env_${ac_var}_value - case $ac_old_set,$ac_new_set in - set,) - { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&5 -$as_echo "$as_me: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&2;} - ac_cache_corrupted=: ;; - ,set) - { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was not set in the previous run" >&5 -$as_echo "$as_me: error: \`$ac_var' was not set in the previous run" >&2;} - ac_cache_corrupted=: ;; - ,);; - *) - if test "x$ac_old_val" != "x$ac_new_val"; then - # differences in whitespace do not lead to failure. - ac_old_val_w=`echo x $ac_old_val` - ac_new_val_w=`echo x $ac_new_val` - if test "$ac_old_val_w" != "$ac_new_val_w"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' has changed since the previous run:" >&5 -$as_echo "$as_me: error: \`$ac_var' has changed since the previous run:" >&2;} - ac_cache_corrupted=: - else - { $as_echo "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&5 -$as_echo "$as_me: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&2;} - eval $ac_var=\$ac_old_val - fi - { $as_echo "$as_me:${as_lineno-$LINENO}: former value: \`$ac_old_val'" >&5 -$as_echo "$as_me: former value: \`$ac_old_val'" >&2;} - { $as_echo "$as_me:${as_lineno-$LINENO}: current value: \`$ac_new_val'" >&5 -$as_echo "$as_me: current value: \`$ac_new_val'" >&2;} - fi;; - esac - # Pass precious variables to config.status. - if test "$ac_new_set" = set; then - case $ac_new_val in - *\'*) ac_arg=$ac_var=`$as_echo "$ac_new_val" | sed "s/'/'\\\\\\\\''/g"` ;; - *) ac_arg=$ac_var=$ac_new_val ;; - esac - case " $ac_configure_args " in - *" '$ac_arg' "*) ;; # Avoid dups. Use of quotes ensures accuracy. - *) as_fn_append ac_configure_args " '$ac_arg'" ;; - esac - fi -done -if $ac_cache_corrupted; then - { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} - { $as_echo "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5 -$as_echo "$as_me: error: changes in the environment can compromise the build" >&2;} - as_fn_error $? "run \`make distclean' and/or \`rm $cache_file' and start over" "$LINENO" 5 -fi -## -------------------- ## -## Main body of script. ## -## -------------------- ## - -ac_ext=c -ac_cpp='$CPP $CPPFLAGS' -ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' -ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' -ac_compiler_gnu=$ac_cv_c_compiler_gnu - - - - - - - - -ac_aux_dir= -for ac_dir in "$srcdir" "$srcdir/.." "$srcdir/../.."; do - if test -f "$ac_dir/install-sh"; then - ac_aux_dir=$ac_dir - ac_install_sh="$ac_aux_dir/install-sh -c" - break - elif test -f "$ac_dir/install.sh"; then - ac_aux_dir=$ac_dir - ac_install_sh="$ac_aux_dir/install.sh -c" - break - elif test -f "$ac_dir/shtool"; then - ac_aux_dir=$ac_dir - ac_install_sh="$ac_aux_dir/shtool install -c" - break - fi -done -if test -z "$ac_aux_dir"; then - as_fn_error $? "cannot find install-sh, install.sh, or shtool in \"$srcdir\" \"$srcdir/..\" \"$srcdir/../..\"" "$LINENO" 5 -fi - -# These three variables are undocumented and unsupported, -# and are intended to be withdrawn in a future Autoconf release. -# They can cause serious problems if a builder's source tree is in a directory -# whose full name contains unusual characters. -ac_config_guess="$SHELL $ac_aux_dir/config.guess" # Please don't use this var. -ac_config_sub="$SHELL $ac_aux_dir/config.sub" # Please don't use this var. -ac_configure="$SHELL $ac_aux_dir/configure" # Please don't use this var. - - -# Find a good install program. We prefer a C program (faster), -# so one script is as good as another. But avoid the broken or -# incompatible versions: -# SysV /etc/install, /usr/sbin/install -# SunOS /usr/etc/install -# IRIX /sbin/install -# AIX /bin/install -# AmigaOS /C/install, which installs bootblocks on floppy discs -# AIX 4 /usr/bin/installbsd, which doesn't work without a -g flag -# AFS /usr/afsws/bin/install, which mishandles nonexistent args -# SVR4 /usr/ucb/install, which tries to use the nonexistent group "staff" -# OS/2's system install, which has a completely different semantic -# ./install, which can be erroneously created by make from ./install.sh. -# Reject install programs that cannot install multiple files. -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for a BSD-compatible install" >&5 -$as_echo_n "checking for a BSD-compatible install... " >&6; } -if test -z "$INSTALL"; then -if ${ac_cv_path_install+:} false; then : - $as_echo_n "(cached) " >&6 -else - as_save_IFS=$IFS; IFS=$PATH_SEPARATOR -for as_dir in $PATH -do - IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - # Account for people who put trailing slashes in PATH elements. -case $as_dir/ in #(( - ./ | .// | /[cC]/* | \ - /etc/* | /usr/sbin/* | /usr/etc/* | /sbin/* | /usr/afsws/bin/* | \ - ?:[\\/]os2[\\/]install[\\/]* | ?:[\\/]OS2[\\/]INSTALL[\\/]* | \ - /usr/ucb/* ) ;; - *) - # OSF1 and SCO ODT 3.0 have their own names for install. - # Don't use installbsd from OSF since it installs stuff as root - # by default. - for ac_prog in ginstall scoinst install; do - for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_prog$ac_exec_ext"; then - if test $ac_prog = install && - grep dspmsg "$as_dir/$ac_prog$ac_exec_ext" >/dev/null 2>&1; then - # AIX install. It has an incompatible calling convention. - : - elif test $ac_prog = install && - grep pwplus "$as_dir/$ac_prog$ac_exec_ext" >/dev/null 2>&1; then - # program-specific install script used by HP pwplus--don't use. - : - else - rm -rf conftest.one conftest.two conftest.dir - echo one > conftest.one - echo two > conftest.two - mkdir conftest.dir - if "$as_dir/$ac_prog$ac_exec_ext" -c conftest.one conftest.two "`pwd`/conftest.dir" && - test -s conftest.one && test -s conftest.two && - test -s conftest.dir/conftest.one && - test -s conftest.dir/conftest.two - then - ac_cv_path_install="$as_dir/$ac_prog$ac_exec_ext -c" - break 3 - fi - fi - fi - done - done - ;; -esac - - done -IFS=$as_save_IFS - -rm -rf conftest.one conftest.two conftest.dir - -fi - if test "${ac_cv_path_install+set}" = set; then - INSTALL=$ac_cv_path_install - else - # As a last resort, use the slow shell script. Don't cache a - # value for INSTALL within a source directory, because that will - # break other packages using the cache if that directory is - # removed, or if the value is a relative name. - INSTALL=$ac_install_sh - fi -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $INSTALL" >&5 -$as_echo "$INSTALL" >&6; } - -# Use test -z because SunOS4 sh mishandles braces in ${var-val}. -# It thinks the first close brace ends the variable substitution. -test -z "$INSTALL_PROGRAM" && INSTALL_PROGRAM='${INSTALL}' - -test -z "$INSTALL_SCRIPT" && INSTALL_SCRIPT='${INSTALL}' - -test -z "$INSTALL_DATA" && INSTALL_DATA='${INSTALL} -m 644' - - - - - - - - -if test "x$ac_cv_env_PKG_CONFIG_set" != "xset"; then - if test -n "$ac_tool_prefix"; then - # Extract the first word of "${ac_tool_prefix}pkg-config", so it can be a program name with args. -set dummy ${ac_tool_prefix}pkg-config; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_path_PKG_CONFIG+:} false; then : - $as_echo_n "(cached) " >&6 -else - case $PKG_CONFIG in - [\\/]* | ?:[\\/]*) - ac_cv_path_PKG_CONFIG="$PKG_CONFIG" # Let the user override the test with a path. - ;; - *) - as_save_IFS=$IFS; IFS=$PATH_SEPARATOR -for as_dir in $PATH -do - IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then - ac_cv_path_PKG_CONFIG="$as_dir/$ac_word$ac_exec_ext" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 - break 2 - fi -done - done -IFS=$as_save_IFS - - ;; -esac -fi -PKG_CONFIG=$ac_cv_path_PKG_CONFIG -if test -n "$PKG_CONFIG"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $PKG_CONFIG" >&5 -$as_echo "$PKG_CONFIG" >&6; } -else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } -fi - - -fi -if test -z "$ac_cv_path_PKG_CONFIG"; then - ac_pt_PKG_CONFIG=$PKG_CONFIG - # Extract the first word of "pkg-config", so it can be a program name with args. -set dummy pkg-config; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_path_ac_pt_PKG_CONFIG+:} false; then : - $as_echo_n "(cached) " >&6 -else - case $ac_pt_PKG_CONFIG in - [\\/]* | ?:[\\/]*) - ac_cv_path_ac_pt_PKG_CONFIG="$ac_pt_PKG_CONFIG" # Let the user override the test with a path. - ;; - *) - as_save_IFS=$IFS; IFS=$PATH_SEPARATOR -for as_dir in $PATH -do - IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then - ac_cv_path_ac_pt_PKG_CONFIG="$as_dir/$ac_word$ac_exec_ext" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 - break 2 - fi -done - done -IFS=$as_save_IFS - - ;; -esac -fi -ac_pt_PKG_CONFIG=$ac_cv_path_ac_pt_PKG_CONFIG -if test -n "$ac_pt_PKG_CONFIG"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_pt_PKG_CONFIG" >&5 -$as_echo "$ac_pt_PKG_CONFIG" >&6; } -else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } -fi - - if test "x$ac_pt_PKG_CONFIG" = x; then - PKG_CONFIG="" - else - case $cross_compiling:$ac_tool_warned in -yes:) -{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 -$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} -ac_tool_warned=yes ;; -esac - PKG_CONFIG=$ac_pt_PKG_CONFIG - fi -else - PKG_CONFIG="$ac_cv_path_PKG_CONFIG" -fi - -fi -if test -n "$PKG_CONFIG"; then - _pkg_min_version=0.9.0 - { $as_echo "$as_me:${as_lineno-$LINENO}: checking pkg-config is at least version $_pkg_min_version" >&5 -$as_echo_n "checking pkg-config is at least version $_pkg_min_version... " >&6; } - if $PKG_CONFIG --atleast-pkgconfig-version $_pkg_min_version; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -$as_echo "yes" >&6; } - else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } - PKG_CONFIG="" - fi -fi - -for ac_prog in dmd ldmd2 ldc2 -do - # Extract the first word of "$ac_prog", so it can be a program name with args. -set dummy $ac_prog; ac_word=$2 -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 -$as_echo_n "checking for $ac_word... " >&6; } -if ${ac_cv_prog_DC+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test -n "$DC"; then - ac_cv_prog_DC="$DC" # Let the user override the test. -else -as_save_IFS=$IFS; IFS=$PATH_SEPARATOR -for as_dir in $PATH -do - IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then - ac_cv_prog_DC="$ac_prog" - $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 - break 2 - fi -done - done -IFS=$as_save_IFS - -fi -fi -DC=$ac_cv_prog_DC -if test -n "$DC"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: $DC" >&5 -$as_echo "$DC" >&6; } -else - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } -fi - - - test -n "$DC" && break -done -test -n "$DC" || DC="NOT_FOUND" - -DC_TYPE= -case $(basename $DC) in - dmd) DC_TYPE=dmd ;; - ldmd2) DC_TYPE=dmd ;; - ldc2) DC_TYPE=ldc ;; - NOT_FOUND) as_fn_error 1 "Could not find any compatible D compiler" "$LINENO" 5 -esac - -vercomp () { - IFS=. read -r a0 a1 a2 aa <' $bb - then - return 1 - else - return 0 - fi - fi - fi - fi -} - -DO_VERSION_CHECK=1 -# Check whether --enable-version-check was given. -if test "${enable_version_check+set}" = set; then : - enableval=$enable_version_check; -fi - -if test "x$enable_version_check" = "xno"; then : - DO_VERSION_CHECK=0 -fi - -if test "$DO_VERSION_CHECK" = "1"; then : - { $as_echo "$as_me:${as_lineno-$LINENO}: checking version of D compiler" >&5 -$as_echo_n "checking version of D compiler... " >&6; } -# check for valid versions -case $(basename $DC) in - ldmd2|ldc2) - # LDC - the LLVM D compiler (1.12.0): ... - VERSION=`$DC --version` - # remove everything up to first ( - VERSION=${VERSION#* (} - # remove everthing after ): - VERSION=${VERSION%%):*} - # now version should be something like L.M.N - MINVERSION=1.18.0 - ;; - dmd) - # DMD64 D Compiler v2.085.1\n... - VERSION=`$DC --version | tr '\n' ' '` - VERSION=${VERSION#*Compiler v} - VERSION=${VERSION%% *} - # now version should be something like L.M.N - MINVERSION=2.088.0 - ;; -esac - -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $VERSION" >&5 -$as_echo "$VERSION" >&6; } - -vercomp $MINVERSION $VERSION -if test $? = 1 -then - as_fn_error 1 "Compiler version insufficient, current compiler version $VERSION, minimum version $MINVERSION" "$LINENO" 5 -fi -#echo "MINVERSION=$MINVERSION VERSION=$VERSION" - -fi - - - - - - -PACKAGE_DATE="June 2023" - - - -pkg_failed=no -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl" >&5 -$as_echo_n "checking for curl... " >&6; } - -if test -n "$curl_CFLAGS"; then - pkg_cv_curl_CFLAGS="$curl_CFLAGS" - elif test -n "$PKG_CONFIG"; then - if test -n "$PKG_CONFIG" && \ - { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libcurl\""; } >&5 - ($PKG_CONFIG --exists --print-errors "libcurl") 2>&5 - ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; }; then - pkg_cv_curl_CFLAGS=`$PKG_CONFIG --cflags "libcurl" 2>/dev/null` - test "x$?" != "x0" && pkg_failed=yes -else - pkg_failed=yes -fi - else - pkg_failed=untried -fi -if test -n "$curl_LIBS"; then - pkg_cv_curl_LIBS="$curl_LIBS" - elif test -n "$PKG_CONFIG"; then - if test -n "$PKG_CONFIG" && \ - { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libcurl\""; } >&5 - ($PKG_CONFIG --exists --print-errors "libcurl") 2>&5 - ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; }; then - pkg_cv_curl_LIBS=`$PKG_CONFIG --libs "libcurl" 2>/dev/null` - test "x$?" != "x0" && pkg_failed=yes -else - pkg_failed=yes -fi - else - pkg_failed=untried -fi - - - -if test $pkg_failed = yes; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } - -if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then - _pkg_short_errors_supported=yes -else - _pkg_short_errors_supported=no -fi - if test $_pkg_short_errors_supported = yes; then - curl_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libcurl" 2>&1` - else - curl_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libcurl" 2>&1` - fi - # Put the nasty error message in config.log where it belongs - echo "$curl_PKG_ERRORS" >&5 - - as_fn_error $? "Package requirements (libcurl) were not met: - -$curl_PKG_ERRORS - -Consider adjusting the PKG_CONFIG_PATH environment variable if you -installed software in a non-standard prefix. - -Alternatively, you may set the environment variables curl_CFLAGS -and curl_LIBS to avoid the need to call pkg-config. -See the pkg-config man page for more details." "$LINENO" 5 -elif test $pkg_failed = untried; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} -as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it -is in your PATH or set the PKG_CONFIG environment variable to the full -path to pkg-config. - -Alternatively, you may set the environment variables curl_CFLAGS -and curl_LIBS to avoid the need to call pkg-config. -See the pkg-config man page for more details. - -To get pkg-config, see . -See \`config.log' for more details" "$LINENO" 5; } -else - curl_CFLAGS=$pkg_cv_curl_CFLAGS - curl_LIBS=$pkg_cv_curl_LIBS - { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -$as_echo "yes" >&6; } - -fi - -pkg_failed=no -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for sqlite" >&5 -$as_echo_n "checking for sqlite... " >&6; } - -if test -n "$sqlite_CFLAGS"; then - pkg_cv_sqlite_CFLAGS="$sqlite_CFLAGS" - elif test -n "$PKG_CONFIG"; then - if test -n "$PKG_CONFIG" && \ - { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"sqlite3\""; } >&5 - ($PKG_CONFIG --exists --print-errors "sqlite3") 2>&5 - ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; }; then - pkg_cv_sqlite_CFLAGS=`$PKG_CONFIG --cflags "sqlite3" 2>/dev/null` - test "x$?" != "x0" && pkg_failed=yes -else - pkg_failed=yes -fi - else - pkg_failed=untried -fi -if test -n "$sqlite_LIBS"; then - pkg_cv_sqlite_LIBS="$sqlite_LIBS" - elif test -n "$PKG_CONFIG"; then - if test -n "$PKG_CONFIG" && \ - { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"sqlite3\""; } >&5 - ($PKG_CONFIG --exists --print-errors "sqlite3") 2>&5 - ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; }; then - pkg_cv_sqlite_LIBS=`$PKG_CONFIG --libs "sqlite3" 2>/dev/null` - test "x$?" != "x0" && pkg_failed=yes -else - pkg_failed=yes -fi - else - pkg_failed=untried -fi - - - -if test $pkg_failed = yes; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } - -if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then - _pkg_short_errors_supported=yes -else - _pkg_short_errors_supported=no -fi - if test $_pkg_short_errors_supported = yes; then - sqlite_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "sqlite3" 2>&1` - else - sqlite_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "sqlite3" 2>&1` - fi - # Put the nasty error message in config.log where it belongs - echo "$sqlite_PKG_ERRORS" >&5 - - as_fn_error $? "Package requirements (sqlite3) were not met: - -$sqlite_PKG_ERRORS - -Consider adjusting the PKG_CONFIG_PATH environment variable if you -installed software in a non-standard prefix. - -Alternatively, you may set the environment variables sqlite_CFLAGS -and sqlite_LIBS to avoid the need to call pkg-config. -See the pkg-config man page for more details." "$LINENO" 5 -elif test $pkg_failed = untried; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } - { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 -$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} -as_fn_error $? "The pkg-config script could not be found or is too old. Make sure it -is in your PATH or set the PKG_CONFIG environment variable to the full -path to pkg-config. - -Alternatively, you may set the environment variables sqlite_CFLAGS -and sqlite_LIBS to avoid the need to call pkg-config. -See the pkg-config man page for more details. - -To get pkg-config, see . -See \`config.log' for more details" "$LINENO" 5; } -else - sqlite_CFLAGS=$pkg_cv_sqlite_CFLAGS - sqlite_LIBS=$pkg_cv_sqlite_LIBS - { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -$as_echo "yes" >&6; } - -fi - - -# Check whether --with-systemdsystemunitdir was given. -if test "${with_systemdsystemunitdir+set}" = set; then : - withval=$with_systemdsystemunitdir; -else - with_systemdsystemunitdir=auto -fi - -if test "x$with_systemdsystemunitdir" = "xyes" -o "x$with_systemdsystemunitdir" = "xauto"; then : - def_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd) - if test "x$def_systemdsystemunitdir" = "x"; then : - if test "x$with_systemdsystemunitdir" = "xyes"; then : - as_fn_error $? "systemd support requested but pkg-config unable to query systemd package" "$LINENO" 5 -fi - with_systemdsystemunitdir=no - -else - with_systemdsystemunitdir="$def_systemdsystemunitdir" - - -fi - - -fi -if test "x$with_systemdsystemunitdir" != "xno"; then : - systemdsystemunitdir=$with_systemdsystemunitdir - -fi - - -# Check whether --with-systemduserunitdir was given. -if test "${with_systemduserunitdir+set}" = set; then : - withval=$with_systemduserunitdir; -else - with_systemduserunitdir=auto -fi - -if test "x$with_systemduserunitdir" = "xyes" -o "x$with_systemduserunitdir" = "xauto"; then : - - def_systemduserunitdir=$($PKG_CONFIG --variable=systemduserunitdir systemd) - if test "x$def_systemduserunitdir" = "x"; then : - - if test "x$with_systemduserunitdir" = "xyes"; then : - as_fn_error $? "systemd support requested but pkg-config unable to query systemd package" "$LINENO" 5 -fi - with_systemduserunitdir=no - -else - - with_systemduserunitdir="$def_systemduserunitdir" - - -fi - - -fi -if test "x$with_systemduserunitdir" != "xno"; then : - systemduserunitdir=$with_systemduserunitdir - -fi - -if test "x$with_systemduserunitdir" != "xno" -a "x$with_systemdsystemunitdir" != "xno"; then : - havesystemd=yes -else - havesystemd=no -fi -HAVE_SYSTEMD=$havesystemd - - - - -# Check whether --enable-notifications was given. -if test "${enable_notifications+set}" = set; then : - enableval=$enable_notifications; -fi - - -if test "x$enable_notifications" = "xyes"; then : - enable_notifications=yes -else - enable_notifications=no -fi - -if test "x$enable_notifications" = "xyes"; then : - -pkg_failed=no -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for notify" >&5 -$as_echo_n "checking for notify... " >&6; } - -if test -n "$notify_CFLAGS"; then - pkg_cv_notify_CFLAGS="$notify_CFLAGS" - elif test -n "$PKG_CONFIG"; then - if test -n "$PKG_CONFIG" && \ - { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libnotify\""; } >&5 - ($PKG_CONFIG --exists --print-errors "libnotify") 2>&5 - ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; }; then - pkg_cv_notify_CFLAGS=`$PKG_CONFIG --cflags "libnotify" 2>/dev/null` - test "x$?" != "x0" && pkg_failed=yes -else - pkg_failed=yes -fi - else - pkg_failed=untried -fi -if test -n "$notify_LIBS"; then - pkg_cv_notify_LIBS="$notify_LIBS" - elif test -n "$PKG_CONFIG"; then - if test -n "$PKG_CONFIG" && \ - { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libnotify\""; } >&5 - ($PKG_CONFIG --exists --print-errors "libnotify") 2>&5 - ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; }; then - pkg_cv_notify_LIBS=`$PKG_CONFIG --libs "libnotify" 2>/dev/null` - test "x$?" != "x0" && pkg_failed=yes -else - pkg_failed=yes -fi - else - pkg_failed=untried -fi - - - -if test $pkg_failed = yes; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } - -if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then - _pkg_short_errors_supported=yes -else - _pkg_short_errors_supported=no -fi - if test $_pkg_short_errors_supported = yes; then - notify_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libnotify" 2>&1` - else - notify_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libnotify" 2>&1` - fi - # Put the nasty error message in config.log where it belongs - echo "$notify_PKG_ERRORS" >&5 - - enable_notifications=no -elif test $pkg_failed = untried; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 -$as_echo "no" >&6; } - enable_notifications=no -else - notify_CFLAGS=$pkg_cv_notify_CFLAGS - notify_LIBS=$pkg_cv_notify_LIBS - { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 -$as_echo "yes" >&6; } - -fi -else - notify_LIBS="" - -fi -NOTIFICATIONS=$enable_notifications - - -# Check whether --enable-completions was given. -if test "${enable_completions+set}" = set; then : - enableval=$enable_completions; -fi - - -if test "x$enable_completions" = "xyes"; then : - enable_completions=yes -else - enable_completions=no -fi - -COMPLETIONS=$enable_completions - - - -if test "x$enable_completions" = "xyes"; then : - - -# Check whether --with-bash-completion-dir was given. -if test "${with_bash_completion_dir+set}" = set; then : - withval=$with_bash_completion_dir; -else - with_bash_completion_dir=auto -fi - - if test "x$with_bash_completion_dir" = "xyes" -o "x$with_bash_completion_dir" = "xauto"; then : - - -if test -n "$bashcompdir"; then - pkg_cv_bashcompdir="$bashcompdir" - elif test -n "$PKG_CONFIG"; then - if test -n "$PKG_CONFIG" && \ - { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"bash-completion\""; } >&5 - ($PKG_CONFIG --exists --print-errors "bash-completion") 2>&5 - ac_status=$? - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; }; then - pkg_cv_bashcompdir=`$PKG_CONFIG --variable="completionsdir" "bash-completion" 2>/dev/null` - test "x$?" != "x0" && pkg_failed=yes -else - pkg_failed=yes -fi - else - pkg_failed=untried -fi -bashcompdir=$pkg_cv_bashcompdir - -if test "x$bashcompdir" = x""; then : - bashcompdir="${sysconfdir}/bash_completion.d" -fi - with_bash_completion_dir=$bashcompdir - -fi - BASH_COMPLETION_DIR=$with_bash_completion_dir - - - -# Check whether --with-zsh-completion-dir was given. -if test "${with_zsh_completion_dir+set}" = set; then : - withval=$with_zsh_completion_dir; -else - with_zsh_completion_dir=auto -fi - - if test "x$with_zsh_completion_dir" = "xyes" -o "x$with_zsh_completion_dir" = "xauto"; then : - - with_zsh_completion_dir="/usr/local/share/zsh/site-functions" - -fi - ZSH_COMPLETION_DIR=$with_zsh_completion_dir - - - -# Check whether --with-fish-completion-dir was given. -if test "${with_fish_completion_dir+set}" = set; then : - withval=$with_fish_completion_dir; -else - with_fish_completion_dir=auto -fi - - if test "x$with_fish_completion_dir" = "xyes" -o "x$with_fish_completion_dir" = "xauto"; then : - - with_fish_completion_dir="/usr/local/share/fish/completions" - -fi - FISH_COMPLETION_DIR=$with_fish_completion_dir - - - -fi - -# Check whether --enable-debug was given. -if test "${enable_debug+set}" = set; then : - enableval=$enable_debug; -fi - -if test "x$enable_debug" = "xyes"; then : - DEBUG=yes - -else - DEBUG=no - -fi - -ac_config_files="$ac_config_files Makefile contrib/pacman/PKGBUILD contrib/spec/onedrive.spec onedrive.1 contrib/systemd/onedrive.service contrib/systemd/onedrive@.service" - -cat >confcache <<\_ACEOF -# This file is a shell script that caches the results of configure -# tests run on this system so they can be shared between configure -# scripts and configure runs, see configure's option --config-cache. -# It is not useful on other systems. If it contains results you don't -# want to keep, you may remove or edit it. -# -# config.status only pays attention to the cache file if you give it -# the --recheck option to rerun configure. -# -# `ac_cv_env_foo' variables (set or unset) will be overridden when -# loading this file, other *unset* `ac_cv_foo' will be assigned the -# following values. - -_ACEOF - -# The following way of writing the cache mishandles newlines in values, -# but we know of no workaround that is simple, portable, and efficient. -# So, we kill variables containing newlines. -# Ultrix sh set writes to stderr and can't be redirected directly, -# and sets the high bit in the cache file unless we assign to the vars. -( - for ac_var in `(set) 2>&1 | sed -n 's/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'`; do - eval ac_val=\$$ac_var - case $ac_val in #( - *${as_nl}*) - case $ac_var in #( - *_cv_*) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 -$as_echo "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; - esac - case $ac_var in #( - _ | IFS | as_nl) ;; #( - BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #( - *) { eval $ac_var=; unset $ac_var;} ;; - esac ;; - esac - done - - (set) 2>&1 | - case $as_nl`(ac_space=' '; set) 2>&1` in #( - *${as_nl}ac_space=\ *) - # `set' does not quote correctly, so add quotes: double-quote - # substitution turns \\\\ into \\, and sed turns \\ into \. - sed -n \ - "s/'/'\\\\''/g; - s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\\2'/p" - ;; #( - *) - # `set' quotes correctly as required by POSIX, so do not add quotes. - sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" - ;; - esac | - sort -) | - sed ' - /^ac_cv_env_/b end - t clear - :clear - s/^\([^=]*\)=\(.*[{}].*\)$/test "${\1+set}" = set || &/ - t end - s/^\([^=]*\)=\(.*\)$/\1=${\1=\2}/ - :end' >>confcache -if diff "$cache_file" confcache >/dev/null 2>&1; then :; else - if test -w "$cache_file"; then - if test "x$cache_file" != "x/dev/null"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5 -$as_echo "$as_me: updating cache $cache_file" >&6;} - if test ! -f "$cache_file" || test -h "$cache_file"; then - cat confcache >"$cache_file" - else - case $cache_file in #( - */* | ?:*) - mv -f confcache "$cache_file"$$ && - mv -f "$cache_file"$$ "$cache_file" ;; #( - *) - mv -f confcache "$cache_file" ;; - esac - fi - fi - else - { $as_echo "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5 -$as_echo "$as_me: not updating unwritable cache $cache_file" >&6;} - fi -fi -rm -f confcache - -test "x$prefix" = xNONE && prefix=$ac_default_prefix -# Let make expand exec_prefix. -test "x$exec_prefix" = xNONE && exec_prefix='${prefix}' - -# Transform confdefs.h into DEFS. -# Protect against shell expansion while executing Makefile rules. -# Protect against Makefile macro expansion. -# -# If the first sed substitution is executed (which looks for macros that -# take arguments), then branch to the quote section. Otherwise, -# look for a macro that doesn't take arguments. -ac_script=' -:mline -/\\$/{ - N - s,\\\n,, - b mline -} -t clear -:clear -s/^[ ]*#[ ]*define[ ][ ]*\([^ (][^ (]*([^)]*)\)[ ]*\(.*\)/-D\1=\2/g -t quote -s/^[ ]*#[ ]*define[ ][ ]*\([^ ][^ ]*\)[ ]*\(.*\)/-D\1=\2/g -t quote -b any -:quote -s/[ `~#$^&*(){}\\|;'\''"<>?]/\\&/g -s/\[/\\&/g -s/\]/\\&/g -s/\$/$$/g -H -:any -${ - g - s/^\n// - s/\n/ /g - p -} -' -DEFS=`sed -n "$ac_script" confdefs.h` - - -ac_libobjs= -ac_ltlibobjs= -U= -for ac_i in : $LIBOBJS; do test "x$ac_i" = x: && continue - # 1. Remove the extension, and $U if already installed. - ac_script='s/\$U\././;s/\.o$//;s/\.obj$//' - ac_i=`$as_echo "$ac_i" | sed "$ac_script"` - # 2. Prepend LIBOBJDIR. When used with automake>=1.10 LIBOBJDIR - # will be set to the directory where LIBOBJS objects are built. - as_fn_append ac_libobjs " \${LIBOBJDIR}$ac_i\$U.$ac_objext" - as_fn_append ac_ltlibobjs " \${LIBOBJDIR}$ac_i"'$U.lo' -done -LIBOBJS=$ac_libobjs - -LTLIBOBJS=$ac_ltlibobjs - - - -: "${CONFIG_STATUS=./config.status}" -ac_write_fail=0 -ac_clean_files_save=$ac_clean_files -ac_clean_files="$ac_clean_files $CONFIG_STATUS" -{ $as_echo "$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS" >&5 -$as_echo "$as_me: creating $CONFIG_STATUS" >&6;} -as_write_fail=0 -cat >$CONFIG_STATUS <<_ASEOF || as_write_fail=1 -#! $SHELL -# Generated by $as_me. -# Run this file to recreate the current configuration. -# Compiler output produced by configure, useful for debugging -# configure, is in config.log if it exists. - -debug=false -ac_cs_recheck=false -ac_cs_silent=false - -SHELL=\${CONFIG_SHELL-$SHELL} -export SHELL -_ASEOF -cat >>$CONFIG_STATUS <<\_ASEOF || as_write_fail=1 -## -------------------- ## -## M4sh Initialization. ## -## -------------------- ## - -# Be more Bourne compatible -DUALCASE=1; export DUALCASE # for MKS sh -if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then : - emulate sh - NULLCMD=: - # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which - # is contrary to our usage. Disable this feature. - alias -g '${1+"$@"}'='"$@"' - setopt NO_GLOB_SUBST -else - case `(set -o) 2>/dev/null` in #( - *posix*) : - set -o posix ;; #( - *) : - ;; -esac -fi - - -as_nl=' -' -export as_nl -# Printing a long string crashes Solaris 7 /usr/bin/printf. -as_echo='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' -as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo -as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo$as_echo -# Prefer a ksh shell builtin over an external printf program on Solaris, -# but without wasting forks for bash or zsh. -if test -z "$BASH_VERSION$ZSH_VERSION" \ - && (test "X`print -r -- $as_echo`" = "X$as_echo") 2>/dev/null; then - as_echo='print -r --' - as_echo_n='print -rn --' -elif (test "X`printf %s $as_echo`" = "X$as_echo") 2>/dev/null; then - as_echo='printf %s\n' - as_echo_n='printf %s' -else - if test "X`(/usr/ucb/echo -n -n $as_echo) 2>/dev/null`" = "X-n $as_echo"; then - as_echo_body='eval /usr/ucb/echo -n "$1$as_nl"' - as_echo_n='/usr/ucb/echo -n' - else - as_echo_body='eval expr "X$1" : "X\\(.*\\)"' - as_echo_n_body='eval - arg=$1; - case $arg in #( - *"$as_nl"*) - expr "X$arg" : "X\\(.*\\)$as_nl"; - arg=`expr "X$arg" : ".*$as_nl\\(.*\\)"`;; - esac; - expr "X$arg" : "X\\(.*\\)" | tr -d "$as_nl" - ' - export as_echo_n_body - as_echo_n='sh -c $as_echo_n_body as_echo' - fi - export as_echo_body - as_echo='sh -c $as_echo_body as_echo' -fi - -# The user is always right. -if test "${PATH_SEPARATOR+set}" != set; then - PATH_SEPARATOR=: - (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && { - (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 || - PATH_SEPARATOR=';' - } -fi - - -# IFS -# We need space, tab and new line, in precisely that order. Quoting is -# there to prevent editors from complaining about space-tab. -# (If _AS_PATH_WALK were called with IFS unset, it would disable word -# splitting by setting IFS to empty value.) -IFS=" "" $as_nl" - -# Find who we are. Look in the path if we contain no directory separator. -as_myself= -case $0 in #(( - *[\\/]* ) as_myself=$0 ;; - *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR -for as_dir in $PATH -do - IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - test -r "$as_dir/$0" && as_myself=$as_dir/$0 && break - done -IFS=$as_save_IFS - - ;; -esac -# We did not find ourselves, most probably we were run as `sh COMMAND' -# in which case we are not to be found in the path. -if test "x$as_myself" = x; then - as_myself=$0 -fi -if test ! -f "$as_myself"; then - $as_echo "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 - exit 1 -fi - -# Unset variables that we do not need and which cause bugs (e.g. in -# pre-3.0 UWIN ksh). But do not cause bugs in bash 2.01; the "|| exit 1" -# suppresses any "Segmentation fault" message there. '((' could -# trigger a bug in pdksh 5.2.14. -for as_var in BASH_ENV ENV MAIL MAILPATH -do eval test x\${$as_var+set} = xset \ - && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : -done -PS1='$ ' -PS2='> ' -PS4='+ ' - -# NLS nuisances. -LC_ALL=C -export LC_ALL -LANGUAGE=C -export LANGUAGE - -# CDPATH. -(unset CDPATH) >/dev/null 2>&1 && unset CDPATH - - -# as_fn_error STATUS ERROR [LINENO LOG_FD] -# ---------------------------------------- -# Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are -# provided, also output the error to LOG_FD, referencing LINENO. Then exit the -# script with STATUS, using 1 if that was 0. -as_fn_error () -{ - as_status=$1; test $as_status -eq 0 && as_status=1 - if test "$4"; then - as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - $as_echo "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 - fi - $as_echo "$as_me: error: $2" >&2 - as_fn_exit $as_status -} # as_fn_error - - -# as_fn_set_status STATUS -# ----------------------- -# Set $? to STATUS, without forking. -as_fn_set_status () -{ - return $1 -} # as_fn_set_status - -# as_fn_exit STATUS -# ----------------- -# Exit the shell with STATUS, even in a "trap 0" or "set -e" context. -as_fn_exit () -{ - set +e - as_fn_set_status $1 - exit $1 -} # as_fn_exit - -# as_fn_unset VAR -# --------------- -# Portably unset VAR. -as_fn_unset () -{ - { eval $1=; unset $1;} -} -as_unset=as_fn_unset -# as_fn_append VAR VALUE -# ---------------------- -# Append the text in VALUE to the end of the definition contained in VAR. Take -# advantage of any shell optimizations that allow amortized linear growth over -# repeated appends, instead of the typical quadratic growth present in naive -# implementations. -if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null; then : - eval 'as_fn_append () - { - eval $1+=\$2 - }' -else - as_fn_append () - { - eval $1=\$$1\$2 - } -fi # as_fn_append - -# as_fn_arith ARG... -# ------------------ -# Perform arithmetic evaluation on the ARGs, and store the result in the -# global $as_val. Take advantage of shells that can avoid forks. The arguments -# must be portable across $(()) and expr. -if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null; then : - eval 'as_fn_arith () - { - as_val=$(( $* )) - }' -else - as_fn_arith () - { - as_val=`expr "$@" || test $? -eq 1` - } -fi # as_fn_arith - - -if expr a : '\(a\)' >/dev/null 2>&1 && - test "X`expr 00001 : '.*\(...\)'`" = X001; then - as_expr=expr -else - as_expr=false -fi - -if (basename -- /) >/dev/null 2>&1 && test "X`basename -- / 2>&1`" = "X/"; then - as_basename=basename -else - as_basename=false -fi - -if (as_dir=`dirname -- /` && test "X$as_dir" = X/) >/dev/null 2>&1; then - as_dirname=dirname -else - as_dirname=false -fi - -as_me=`$as_basename -- "$0" || -$as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ - X"$0" : 'X\(//\)$' \| \ - X"$0" : 'X\(/\)' \| . 2>/dev/null || -$as_echo X/"$0" | - sed '/^.*\/\([^/][^/]*\)\/*$/{ - s//\1/ - q - } - /^X\/\(\/\/\)$/{ - s//\1/ - q - } - /^X\/\(\/\).*/{ - s//\1/ - q - } - s/.*/./; q'` - -# Avoid depending upon Character Ranges. -as_cr_letters='abcdefghijklmnopqrstuvwxyz' -as_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ' -as_cr_Letters=$as_cr_letters$as_cr_LETTERS -as_cr_digits='0123456789' -as_cr_alnum=$as_cr_Letters$as_cr_digits - -ECHO_C= ECHO_N= ECHO_T= -case `echo -n x` in #((((( --n*) - case `echo 'xy\c'` in - *c*) ECHO_T=' ';; # ECHO_T is single tab character. - xy) ECHO_C='\c';; - *) echo `echo ksh88 bug on AIX 6.1` > /dev/null - ECHO_T=' ';; - esac;; -*) - ECHO_N='-n';; -esac - -rm -f conf$$ conf$$.exe conf$$.file -if test -d conf$$.dir; then - rm -f conf$$.dir/conf$$.file -else - rm -f conf$$.dir - mkdir conf$$.dir 2>/dev/null -fi -if (echo >conf$$.file) 2>/dev/null; then - if ln -s conf$$.file conf$$ 2>/dev/null; then - as_ln_s='ln -s' - # ... but there are two gotchas: - # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail. - # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable. - # In both cases, we have to default to `cp -pR'. - ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe || - as_ln_s='cp -pR' - elif ln conf$$.file conf$$ 2>/dev/null; then - as_ln_s=ln - else - as_ln_s='cp -pR' - fi -else - as_ln_s='cp -pR' -fi -rm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file -rmdir conf$$.dir 2>/dev/null - - -# as_fn_mkdir_p -# ------------- -# Create "$as_dir" as a directory, including parents if necessary. -as_fn_mkdir_p () -{ - - case $as_dir in #( - -*) as_dir=./$as_dir;; - esac - test -d "$as_dir" || eval $as_mkdir_p || { - as_dirs= - while :; do - case $as_dir in #( - *\'*) as_qdir=`$as_echo "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( - *) as_qdir=$as_dir;; - esac - as_dirs="'$as_qdir' $as_dirs" - as_dir=`$as_dirname -- "$as_dir" || -$as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ - X"$as_dir" : 'X\(//\)[^/]' \| \ - X"$as_dir" : 'X\(//\)$' \| \ - X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || -$as_echo X"$as_dir" | - sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ - s//\1/ - q - } - /^X\(\/\/\)[^/].*/{ - s//\1/ - q - } - /^X\(\/\/\)$/{ - s//\1/ - q - } - /^X\(\/\).*/{ - s//\1/ - q - } - s/.*/./; q'` - test -d "$as_dir" && break - done - test -z "$as_dirs" || eval "mkdir $as_dirs" - } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir" - - -} # as_fn_mkdir_p -if mkdir -p . 2>/dev/null; then - as_mkdir_p='mkdir -p "$as_dir"' -else - test -d ./-p && rmdir ./-p - as_mkdir_p=false -fi - - -# as_fn_executable_p FILE -# ----------------------- -# Test if FILE is an executable regular file. -as_fn_executable_p () -{ - test -f "$1" && test -x "$1" -} # as_fn_executable_p -as_test_x='test -x' -as_executable_p=as_fn_executable_p - -# Sed expression to map a string onto a valid CPP name. -as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'" - -# Sed expression to map a string onto a valid variable name. -as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'" - - -exec 6>&1 -## ----------------------------------- ## -## Main body of $CONFIG_STATUS script. ## -## ----------------------------------- ## -_ASEOF -test $as_write_fail = 0 && chmod +x $CONFIG_STATUS || ac_write_fail=1 - -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 -# Save the log message, to keep $0 and so on meaningful, and to -# 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.25, which was -generated by GNU Autoconf 2.69. Invocation command line was - - CONFIG_FILES = $CONFIG_FILES - CONFIG_HEADERS = $CONFIG_HEADERS - CONFIG_LINKS = $CONFIG_LINKS - CONFIG_COMMANDS = $CONFIG_COMMANDS - $ $0 $@ - -on `(hostname || uname -n) 2>/dev/null | sed 1q` -" - -_ACEOF - -case $ac_config_files in *" -"*) set x $ac_config_files; shift; ac_config_files=$*;; -esac - - - -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 -# Files that config.status was made for. -config_files="$ac_config_files" - -_ACEOF - -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 -ac_cs_usage="\ -\`$as_me' instantiates files and other configuration actions -from templates according to the current configuration. Unless the files -and actions are specified as TAGs, all are instantiated by default. - -Usage: $0 [OPTION]... [TAG]... - - -h, --help print this help, then exit - -V, --version print version number and configuration settings, then exit - --config print configuration, then exit - -q, --quiet, --silent - do not print progress messages - -d, --debug don't remove temporary files - --recheck update $as_me by reconfiguring in the same conditions - --file=FILE[:TEMPLATE] - instantiate the configuration file FILE - -Configuration files: -$config_files - -Report bugs to ." - -_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.25 -configured by $0, generated by GNU Autoconf 2.69, - with options \\"\$ac_cs_config\\" - -Copyright (C) 2012 Free Software Foundation, Inc. -This config.status script is free software; the Free Software Foundation -gives unlimited permission to copy, distribute and modify it." - -ac_pwd='$ac_pwd' -srcdir='$srcdir' -INSTALL='$INSTALL' -test -n "\$AWK" || AWK=awk -_ACEOF - -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 -# The default lists apply if the user does not specify any file. -ac_need_defaults=: -while test $# != 0 -do - case $1 in - --*=?*) - ac_option=`expr "X$1" : 'X\([^=]*\)='` - ac_optarg=`expr "X$1" : 'X[^=]*=\(.*\)'` - ac_shift=: - ;; - --*=) - ac_option=`expr "X$1" : 'X\([^=]*\)='` - ac_optarg= - ac_shift=: - ;; - *) - ac_option=$1 - ac_optarg=$2 - ac_shift=shift - ;; - esac - - case $ac_option in - # Handling of the options. - -recheck | --recheck | --rechec | --reche | --rech | --rec | --re | --r) - ac_cs_recheck=: ;; - --version | --versio | --versi | --vers | --ver | --ve | --v | -V ) - $as_echo "$ac_cs_version"; exit ;; - --config | --confi | --conf | --con | --co | --c ) - $as_echo "$ac_cs_config"; exit ;; - --debug | --debu | --deb | --de | --d | -d ) - debug=: ;; - --file | --fil | --fi | --f ) - $ac_shift - case $ac_optarg in - *\'*) ac_optarg=`$as_echo "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; - '') as_fn_error $? "missing file argument" ;; - esac - as_fn_append CONFIG_FILES " '$ac_optarg'" - ac_need_defaults=false;; - --he | --h | --help | --hel | -h ) - $as_echo "$ac_cs_usage"; exit ;; - -q | -quiet | --quiet | --quie | --qui | --qu | --q \ - | -silent | --silent | --silen | --sile | --sil | --si | --s) - ac_cs_silent=: ;; - - # This is an error. - -*) as_fn_error $? "unrecognized option: \`$1' -Try \`$0 --help' for more information." ;; - - *) as_fn_append ac_config_targets " $1" - ac_need_defaults=false ;; - - esac - shift -done - -ac_configure_extra_args= - -if $ac_cs_silent; then - exec 6>/dev/null - ac_configure_extra_args="$ac_configure_extra_args --silent" -fi - -_ACEOF -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 -if \$ac_cs_recheck; then - set X $SHELL '$0' $ac_configure_args \$ac_configure_extra_args --no-create --no-recursion - shift - \$as_echo "running CONFIG_SHELL=$SHELL \$*" >&6 - CONFIG_SHELL='$SHELL' - export CONFIG_SHELL - exec "\$@" -fi - -_ACEOF -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 -exec 5>>config.log -{ - echo - sed 'h;s/./-/g;s/^.../## /;s/...$/ ##/;p;x;p;x' <<_ASBOX -## Running $as_me. ## -_ASBOX - $as_echo "$ac_log" -} >&5 - -_ACEOF -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 -_ACEOF - -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 - -# Handling of arguments. -for ac_config_target in $ac_config_targets -do - case $ac_config_target in - "Makefile") CONFIG_FILES="$CONFIG_FILES Makefile" ;; - "contrib/pacman/PKGBUILD") CONFIG_FILES="$CONFIG_FILES contrib/pacman/PKGBUILD" ;; - "contrib/spec/onedrive.spec") CONFIG_FILES="$CONFIG_FILES contrib/spec/onedrive.spec" ;; - "onedrive.1") CONFIG_FILES="$CONFIG_FILES onedrive.1" ;; - "contrib/systemd/onedrive.service") CONFIG_FILES="$CONFIG_FILES contrib/systemd/onedrive.service" ;; - "contrib/systemd/onedrive@.service") CONFIG_FILES="$CONFIG_FILES contrib/systemd/onedrive@.service" ;; - - *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;; - esac -done - - -# If the user did not use the arguments to specify the items to instantiate, -# then the envvar interface is used. Set only those that are not. -# We use the long form for the default assignment because of an extremely -# bizarre bug on SunOS 4.1.3. -if $ac_need_defaults; then - test "${CONFIG_FILES+set}" = set || CONFIG_FILES=$config_files -fi - -# Have a temporary directory for convenience. Make it in the build tree -# simply because there is no reason against having it here, and in addition, -# creating and moving files from /tmp can sometimes cause problems. -# Hook for its removal unless debugging. -# Note that there is a small window in which the directory will not be cleaned: -# after its creation but before its name has been assigned to `$tmp'. -$debug || -{ - tmp= ac_tmp= - trap 'exit_status=$? - : "${ac_tmp:=$tmp}" - { test ! -d "$ac_tmp" || rm -fr "$ac_tmp"; } && exit $exit_status -' 0 - trap 'as_fn_exit 1' 1 2 13 15 -} -# Create a (secure) tmp directory for tmp files. - -{ - tmp=`(umask 077 && mktemp -d "./confXXXXXX") 2>/dev/null` && - test -d "$tmp" -} || -{ - tmp=./conf$$-$RANDOM - (umask 077 && mkdir "$tmp") -} || as_fn_error $? "cannot create a temporary directory in ." "$LINENO" 5 -ac_tmp=$tmp - -# Set up the scripts for CONFIG_FILES section. -# No need to generate them if there are no CONFIG_FILES. -# This happens for instance with `./config.status config.h'. -if test -n "$CONFIG_FILES"; then - - -ac_cr=`echo X | tr X '\015'` -# On cygwin, bash can eat \r inside `` if the user requested igncr. -# But we know of no other shell where ac_cr would be empty at this -# point, so we can use a bashism as a fallback. -if test "x$ac_cr" = x; then - eval ac_cr=\$\'\\r\' -fi -ac_cs_awk_cr=`$AWK 'BEGIN { print "a\rb" }' /dev/null` -if test "$ac_cs_awk_cr" = "a${ac_cr}b"; then - ac_cs_awk_cr='\\r' -else - ac_cs_awk_cr=$ac_cr -fi - -echo 'BEGIN {' >"$ac_tmp/subs1.awk" && -_ACEOF - - -{ - echo "cat >conf$$subs.awk <<_ACEOF" && - echo "$ac_subst_vars" | sed 's/.*/&!$&$ac_delim/' && - echo "_ACEOF" -} >conf$$subs.sh || - as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 -ac_delim_num=`echo "$ac_subst_vars" | grep -c '^'` -ac_delim='%!_!# ' -for ac_last_try in false false false false false :; do - . ./conf$$subs.sh || - as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 - - ac_delim_n=`sed -n "s/.*$ac_delim\$/X/p" conf$$subs.awk | grep -c X` - if test $ac_delim_n = $ac_delim_num; then - break - elif $ac_last_try; then - as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 - else - ac_delim="$ac_delim!$ac_delim _$ac_delim!! " - fi -done -rm -f conf$$subs.sh - -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 -cat >>"\$ac_tmp/subs1.awk" <<\\_ACAWK && -_ACEOF -sed -n ' -h -s/^/S["/; s/!.*/"]=/ -p -g -s/^[^!]*!// -:repl -t repl -s/'"$ac_delim"'$// -t delim -:nl -h -s/\(.\{148\}\)..*/\1/ -t more1 -s/["\\]/\\&/g; s/^/"/; s/$/\\n"\\/ -p -n -b repl -:more1 -s/["\\]/\\&/g; s/^/"/; s/$/"\\/ -p -g -s/.\{148\}// -t nl -:delim -h -s/\(.\{148\}\)..*/\1/ -t more2 -s/["\\]/\\&/g; s/^/"/; s/$/"/ -p -b -:more2 -s/["\\]/\\&/g; s/^/"/; s/$/"\\/ -p -g -s/.\{148\}// -t delim -' >$CONFIG_STATUS || ac_write_fail=1 -rm -f conf$$subs.awk -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 -_ACAWK -cat >>"\$ac_tmp/subs1.awk" <<_ACAWK && - for (key in S) S_is_set[key] = 1 - FS = "" - -} -{ - line = $ 0 - nfields = split(line, field, "@") - substed = 0 - len = length(field[1]) - for (i = 2; i < nfields; i++) { - key = field[i] - keylen = length(key) - if (S_is_set[key]) { - value = S[key] - line = substr(line, 1, len) "" value "" substr(line, len + keylen + 3) - len += length(value) + length(field[++i]) - substed = 1 - } else - len += 1 + keylen - } - - print line -} - -_ACAWK -_ACEOF -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 -if sed "s/$ac_cr//" < /dev/null > /dev/null 2>&1; then - sed "s/$ac_cr\$//; s/$ac_cr/$ac_cs_awk_cr/g" -else - cat -fi < "$ac_tmp/subs1.awk" > "$ac_tmp/subs.awk" \ - || as_fn_error $? "could not setup config files machinery" "$LINENO" 5 -_ACEOF - -# VPATH may cause trouble with some makes, so we remove sole $(srcdir), -# ${srcdir} and @srcdir@ entries from VPATH if srcdir is ".", strip leading and -# trailing colons and then remove the whole line if VPATH becomes empty -# (actually we leave an empty line to preserve line numbers). -if test "x$srcdir" = x.; then - ac_vpsub='/^[ ]*VPATH[ ]*=[ ]*/{ -h -s/// -s/^/:/ -s/[ ]*$/:/ -s/:\$(srcdir):/:/g -s/:\${srcdir}:/:/g -s/:@srcdir@:/:/g -s/^:*// -s/:*$// -x -s/\(=[ ]*\).*/\1/ -G -s/\n// -s/^[^=]*=[ ]*$// -}' -fi - -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 -fi # test -n "$CONFIG_FILES" - - -eval set X " :F $CONFIG_FILES " -shift -for ac_tag -do - case $ac_tag in - :[FHLC]) ac_mode=$ac_tag; continue;; - esac - case $ac_mode$ac_tag in - :[FHL]*:*);; - :L* | :C*:*) as_fn_error $? "invalid tag \`$ac_tag'" "$LINENO" 5;; - :[FH]-) ac_tag=-:-;; - :[FH]*) ac_tag=$ac_tag:$ac_tag.in;; - esac - ac_save_IFS=$IFS - IFS=: - set x $ac_tag - IFS=$ac_save_IFS - shift - ac_file=$1 - shift - - case $ac_mode in - :L) ac_source=$1;; - :[FH]) - ac_file_inputs= - for ac_f - do - case $ac_f in - -) ac_f="$ac_tmp/stdin";; - *) # Look for the file first in the build tree, then in the source tree - # (if the path is not absolute). The absolute path cannot be DOS-style, - # because $ac_f cannot contain `:'. - test -f "$ac_f" || - case $ac_f in - [\\/$]*) false;; - *) test -f "$srcdir/$ac_f" && ac_f="$srcdir/$ac_f";; - esac || - as_fn_error 1 "cannot find input file: \`$ac_f'" "$LINENO" 5;; - esac - case $ac_f in *\'*) ac_f=`$as_echo "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac - as_fn_append ac_file_inputs " '$ac_f'" - done - - # Let's still pretend it is `configure' which instantiates (i.e., don't - # use $as_me), people would be surprised to read: - # /* config.h. Generated by config.status. */ - configure_input='Generated from '` - $as_echo "$*" | sed 's|^[^:]*/||;s|:[^:]*/|, |g' - `' by configure.' - if test x"$ac_file" != x-; then - configure_input="$ac_file. $configure_input" - { $as_echo "$as_me:${as_lineno-$LINENO}: creating $ac_file" >&5 -$as_echo "$as_me: creating $ac_file" >&6;} - fi - # Neutralize special characters interpreted by sed in replacement strings. - case $configure_input in #( - *\&* | *\|* | *\\* ) - ac_sed_conf_input=`$as_echo "$configure_input" | - sed 's/[\\\\&|]/\\\\&/g'`;; #( - *) ac_sed_conf_input=$configure_input;; - esac - - case $ac_tag in - *:-:* | *:-) cat >"$ac_tmp/stdin" \ - || as_fn_error $? "could not create $ac_file" "$LINENO" 5 ;; - esac - ;; - esac - - ac_dir=`$as_dirname -- "$ac_file" || -$as_expr X"$ac_file" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ - X"$ac_file" : 'X\(//\)[^/]' \| \ - X"$ac_file" : 'X\(//\)$' \| \ - X"$ac_file" : 'X\(/\)' \| . 2>/dev/null || -$as_echo X"$ac_file" | - sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ - s//\1/ - q - } - /^X\(\/\/\)[^/].*/{ - s//\1/ - q - } - /^X\(\/\/\)$/{ - s//\1/ - q - } - /^X\(\/\).*/{ - s//\1/ - q - } - s/.*/./; q'` - as_dir="$ac_dir"; as_fn_mkdir_p - ac_builddir=. - -case "$ac_dir" in -.) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; -*) - ac_dir_suffix=/`$as_echo "$ac_dir" | sed 's|^\.[\\/]||'` - # A ".." for each directory in $ac_dir_suffix. - ac_top_builddir_sub=`$as_echo "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` - case $ac_top_builddir_sub in - "") ac_top_builddir_sub=. ac_top_build_prefix= ;; - *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; - esac ;; -esac -ac_abs_top_builddir=$ac_pwd -ac_abs_builddir=$ac_pwd$ac_dir_suffix -# for backward compatibility: -ac_top_builddir=$ac_top_build_prefix - -case $srcdir in - .) # We are building in place. - ac_srcdir=. - ac_top_srcdir=$ac_top_builddir_sub - ac_abs_top_srcdir=$ac_pwd ;; - [\\/]* | ?:[\\/]* ) # Absolute name. - ac_srcdir=$srcdir$ac_dir_suffix; - ac_top_srcdir=$srcdir - ac_abs_top_srcdir=$srcdir ;; - *) # Relative name. - ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix - ac_top_srcdir=$ac_top_build_prefix$srcdir - ac_abs_top_srcdir=$ac_pwd/$srcdir ;; -esac -ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix - - - case $ac_mode in - :F) - # - # CONFIG_FILE - # - - case $INSTALL in - [\\/$]* | ?:[\\/]* ) ac_INSTALL=$INSTALL ;; - *) ac_INSTALL=$ac_top_build_prefix$INSTALL ;; - esac -_ACEOF - -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 -# If the template does not know about datarootdir, expand it. -# FIXME: This hack should be removed a few years after 2.60. -ac_datarootdir_hack=; ac_datarootdir_seen= -ac_sed_dataroot=' -/datarootdir/ { - p - q -} -/@datadir@/p -/@docdir@/p -/@infodir@/p -/@localedir@/p -/@mandir@/p' -case `eval "sed -n \"\$ac_sed_dataroot\" $ac_file_inputs"` in -*datarootdir*) ac_datarootdir_seen=yes;; -*@datadir@*|*@docdir@*|*@infodir@*|*@localedir@*|*@mandir@*) - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&5 -$as_echo "$as_me: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&2;} -_ACEOF -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 - ac_datarootdir_hack=' - s&@datadir@&$datadir&g - s&@docdir@&$docdir&g - s&@infodir@&$infodir&g - s&@localedir@&$localedir&g - s&@mandir@&$mandir&g - s&\\\${datarootdir}&$datarootdir&g' ;; -esac -_ACEOF - -# Neutralize VPATH when `$srcdir' = `.'. -# Shell code in configure.ac might set extrasub. -# FIXME: do we really want to maintain this feature? -cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 -ac_sed_extra="$ac_vpsub -$extrasub -_ACEOF -cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 -:t -/@[a-zA-Z_][a-zA-Z_0-9]*@/!b -s|@configure_input@|$ac_sed_conf_input|;t t -s&@top_builddir@&$ac_top_builddir_sub&;t t -s&@top_build_prefix@&$ac_top_build_prefix&;t t -s&@srcdir@&$ac_srcdir&;t t -s&@abs_srcdir@&$ac_abs_srcdir&;t t -s&@top_srcdir@&$ac_top_srcdir&;t t -s&@abs_top_srcdir@&$ac_abs_top_srcdir&;t t -s&@builddir@&$ac_builddir&;t t -s&@abs_builddir@&$ac_abs_builddir&;t t -s&@abs_top_builddir@&$ac_abs_top_builddir&;t t -s&@INSTALL@&$ac_INSTALL&;t t -$ac_datarootdir_hack -" -eval sed \"\$ac_sed_extra\" "$ac_file_inputs" | $AWK -f "$ac_tmp/subs.awk" \ - >$ac_tmp/out || as_fn_error $? "could not create $ac_file" "$LINENO" 5 - -test -z "$ac_datarootdir_hack$ac_datarootdir_seen" && - { ac_out=`sed -n '/\${datarootdir}/p' "$ac_tmp/out"`; test -n "$ac_out"; } && - { ac_out=`sed -n '/^[ ]*datarootdir[ ]*:*=/p' \ - "$ac_tmp/out"`; test -z "$ac_out"; } && - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable \`datarootdir' -which seems to be undefined. Please make sure it is defined" >&5 -$as_echo "$as_me: WARNING: $ac_file contains a reference to the variable \`datarootdir' -which seems to be undefined. Please make sure it is defined" >&2;} - - rm -f "$ac_tmp/stdin" - case $ac_file in - -) cat "$ac_tmp/out" && rm -f "$ac_tmp/out";; - *) rm -f "$ac_file" && mv "$ac_tmp/out" "$ac_file";; - esac \ - || as_fn_error $? "could not create $ac_file" "$LINENO" 5 - ;; - - - - esac - -done # for ac_tag - - -as_fn_exit 0 -_ACEOF -ac_clean_files=$ac_clean_files_save - -test $ac_write_fail = 0 || - as_fn_error $? "write failure creating $CONFIG_STATUS" "$LINENO" 5 - - -# configure is writing to config.log, and then calls config.status. -# config.status does its own redirection, appending to config.log. -# Unfortunately, on DOS this fails, as config.log is still kept open -# by configure, so config.status won't be able to write to it; its -# output is simply discarded. So we exec the FD to /dev/null, -# effectively closing config.log, so it can be properly (re)opened and -# appended to by config.status. When coming back to configure, we -# need to make the FD available again. -if test "$no_create" != yes; then - ac_cs_success=: - ac_config_status_args= - test "$silent" = yes && - ac_config_status_args="$ac_config_status_args --quiet" - exec 5>/dev/null - $SHELL $CONFIG_STATUS $ac_config_status_args || ac_cs_success=false - exec 5>>config.log - # Use ||, not &&, to avoid exiting from the if with $? = 1, which - # would make configure fail if this is the last instruction. - $ac_cs_success || as_fn_exit 1 -fi -if test -n "$ac_unrecognized_opts" && test "$enable_option_checking" != no; then - { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5 -$as_echo "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} -fi - diff --git a/configure.ac b/configure.ac deleted file mode 100644 index 9c2c0db2..00000000 --- a/configure.ac +++ /dev/null @@ -1,288 +0,0 @@ -dnl configure.ac for OneDrive Linux Client -dnl Copyright 2019 Norbert Preining -dnl Licensed GPL v3 or later - -dnl How to make a release -dnl - increase the version number in the AC_INIT call below -dnl - run autoconf which generates configure -dnl - commit the changed files (configure.ac, configure) -dnl - tag the release - -AC_PREREQ([2.69]) -AC_INIT([onedrive],[v2.4.25], [https://github.com/abraunegg/onedrive], [onedrive]) -AC_CONFIG_SRCDIR([src/main.d]) - - -AC_ARG_VAR([DC], [D compiler executable]) -AC_ARG_VAR([DCFLAGS], [flags for D compiler]) - -dnl necessary programs: install, pkg-config -AC_PROG_INSTALL -PKG_PROG_PKG_CONFIG - -dnl Determine D compiler -dnl we check for dmd, dmd2, and ldc2 in this order -dnl furthermore, we set DC_TYPE to either dmd or ldc and export this into the -dnl Makefile so that we can adjust command line arguments -AC_CHECK_PROGS([DC], [dmd ldmd2 ldc2], NOT_FOUND) -DC_TYPE= -case $(basename $DC) in - dmd) DC_TYPE=dmd ;; - ldmd2) DC_TYPE=dmd ;; - ldc2) DC_TYPE=ldc ;; - NOT_FOUND) AC_MSG_ERROR(Could not find any compatible D compiler, 1) -esac - -dnl dash/POSIX version of version comparison -vercomp () { - IFS=. read -r a0 a1 a2 aa <' $bb - then - return 1 - else - return 0 - fi - fi - fi - fi -} - -DO_VERSION_CHECK=1 -AC_ARG_ENABLE(version-check, - AS_HELP_STRING([--disable-version-check], [Disable checks of compiler version during configure time])) -AS_IF([test "x$enable_version_check" = "xno"], DO_VERSION_CHECK=0,) - -AS_IF([test "$DO_VERSION_CHECK" = "1"], - [ dnl do the version check -AC_MSG_CHECKING([version of D compiler]) -# check for valid versions -case $(basename $DC) in - ldmd2|ldc2) - # LDC - the LLVM D compiler (1.12.0): ... - VERSION=`$DC --version` - # remove everything up to first ( - VERSION=${VERSION#* (} - # remove everthing after ): - VERSION=${VERSION%%):*} - # now version should be something like L.M.N - MINVERSION=1.18.0 - ;; - dmd) - # DMD64 D Compiler v2.085.1\n... - VERSION=`$DC --version | tr '\n' ' '` - VERSION=${VERSION#*Compiler v} - VERSION=${VERSION%% *} - # now version should be something like L.M.N - MINVERSION=2.088.0 - ;; -esac - -AC_MSG_RESULT([$VERSION]) - -vercomp $MINVERSION $VERSION -if test $? = 1 -then - AC_MSG_ERROR([Compiler version insufficient, current compiler version $VERSION, minimum version $MINVERSION], 1) -fi -#echo "MINVERSION=$MINVERSION VERSION=$VERSION" - ]) - - - -AC_SUBST([DC_TYPE]) -dnl In case the environment variable DCFLAGS is set, we export it to the -dnl generated Makefile at configure run: -AC_SUBST([DCFLAGS]) - -dnl The package date is only used in the man page onedrive.1.in -dnl we generate onedrive.1 from it during configure run, but we want -dnl to have the same date, namely the one when the configure script -dnl was generated from the configure.ac (i.e., on release time). -dnl this uses a call to the underlying m4 engine to call an external cmd -PACKAGE_DATE="m4_esyscmd([date "+%B %Y" | tr -d '\n'])" -AC_SUBST([PACKAGE_DATE]) - -dnl Check for required modules: curl and sqlite at the moment -PKG_CHECK_MODULES([curl],[libcurl]) -PKG_CHECK_MODULES([sqlite],[sqlite3]) - -dnl -dnl systemd and unit file directories -dnl This is a bit tricky, because we want to allow for -dnl --with-systemdsystemunitdir=auto -dnl as well as =/path/to/dir -dnl The first step is that we check whether the --with options is passed to configure run -dnl if yes, we don't do anything (the ,, at the end of the next line), and if not, we -dnl set with_systemdsystemunitdir=auto, meaning we will try pkg-config to find the correct -dnl value. -AC_ARG_WITH([systemdsystemunitdir], - [AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd system service files])],, - [with_systemdsystemunitdir=auto]) -dnl If no value is passed in (or auto/yes is passed in), then we try to find the correct -dnl value via pkg-config and put it into $def_systemdsystemunitdir -AS_IF([test "x$with_systemdsystemunitdir" = "xyes" -o "x$with_systemdsystemunitdir" = "xauto"], - [ dnl true part, so try to determine with pkg-config - def_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd) - dnl if we cannot find it via pkg-config, *and* the user explicitely passed it in with, - dnl we warn, and in all cases we unset (set to no) the respective variable - AS_IF([test "x$def_systemdsystemunitdir" = "x"], - [ dnl we couldn't find the default value via pkg-config - AS_IF([test "x$with_systemdsystemunitdir" = "xyes"], - [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])]) - with_systemdsystemunitdir=no - ], - [ dnl pkg-config found the value, use it - with_systemdsystemunitdir="$def_systemdsystemunitdir" - ] - ) - ] -) -dnl finally, if we found a value, put it into the generated Makefile -AS_IF([test "x$with_systemdsystemunitdir" != "xno"], - [AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir])]) - -dnl Now do the same as above for systemduserunitdir! -AC_ARG_WITH([systemduserunitdir], - [AS_HELP_STRING([--with-systemduserunitdir=DIR], [Directory for systemd user service files])],, - [with_systemduserunitdir=auto]) -AS_IF([test "x$with_systemduserunitdir" = "xyes" -o "x$with_systemduserunitdir" = "xauto"], - [ - def_systemduserunitdir=$($PKG_CONFIG --variable=systemduserunitdir systemd) - AS_IF([test "x$def_systemduserunitdir" = "x"], - [ - AS_IF([test "x$with_systemduserunitdir" = "xyes"], - [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])]) - with_systemduserunitdir=no - ], - [ - with_systemduserunitdir="$def_systemduserunitdir" - ] - ) - ] -) -AS_IF([test "x$with_systemduserunitdir" != "xno"], - [AC_SUBST([systemduserunitdir], [$with_systemduserunitdir])]) - -dnl We enable systemd integration only if we have found both user/system unit dirs -AS_IF([test "x$with_systemduserunitdir" != "xno" -a "x$with_systemdsystemunitdir" != "xno"], - [havesystemd=yes], [havesystemd=no]) -AC_SUBST([HAVE_SYSTEMD], $havesystemd) - - - -dnl -dnl Notification support -dnl only check for libnotify if --enable-notifications is given -AC_ARG_ENABLE(notifications, - AS_HELP_STRING([--enable-notifications], [Enable desktop notifications via libnotify])) - -AS_IF([test "x$enable_notifications" = "xyes"], [enable_notifications=yes], [enable_notifications=no]) - -dnl if --enable-notifications was given, check for libnotify, and disable if not found -dnl otherwise substitute the notifu -AS_IF([test "x$enable_notifications" = "xyes"], - [PKG_CHECK_MODULES(notify,libnotify,,enable_notifications=no)], - [AC_SUBST([notify_LIBS],"")]) -AC_SUBST([NOTIFICATIONS],$enable_notifications) - -dnl -dnl Completion support -dnl First determine whether completions are requested, pass that to Makefile -AC_ARG_ENABLE([completions], - AS_HELP_STRING([--enable-completions], [Install shell completions for bash, zsh, and fish])) - -AS_IF([test "x$enable_completions" = "xyes"], [enable_completions=yes], [enable_completions=no]) - -AC_SUBST([COMPLETIONS],$enable_completions) - - -dnl if completions are enabled, search for the bash/zsh completion directory in the -dnl similar way as we did for the systemd directories -AS_IF([test "x$enable_completions" = "xyes"],[ - AC_ARG_WITH([bash-completion-dir], - [AS_HELP_STRING([--with-bash-completion-dir=DIR], [Directory for bash completion files])], - , - [with_bash_completion_dir=auto]) - AS_IF([test "x$with_bash_completion_dir" = "xyes" -o "x$with_bash_completion_dir" = "xauto"], - [ - PKG_CHECK_VAR(bashcompdir, [bash-completion], [completionsdir], , - bashcompdir="${sysconfdir}/bash_completion.d") - with_bash_completion_dir=$bashcompdir - ]) - AC_SUBST([BASH_COMPLETION_DIR], $with_bash_completion_dir) - - AC_ARG_WITH([zsh-completion-dir], - [AS_HELP_STRING([--with-zsh-completion-dir=DIR], [Directory for zsh completion files])],, - [with_zsh_completion_dir=auto]) - AS_IF([test "x$with_zsh_completion_dir" = "xyes" -o "x$with_zsh_completion_dir" = "xauto"], - [ - with_zsh_completion_dir="/usr/local/share/zsh/site-functions" - ]) - AC_SUBST([ZSH_COMPLETION_DIR], $with_zsh_completion_dir) - - AC_ARG_WITH([fish-completion-dir], - [AS_HELP_STRING([--with-fish-completion-dir=DIR], [Directory for fish completion files])],, - [with_fish_completion_dir=auto]) - AS_IF([test "x$with_fish_completion_dir" = "xyes" -o "x$with_fish_completion_dir" = "xauto"], - [ - with_fish_completion_dir="/usr/local/share/fish/completions" - ]) - AC_SUBST([FISH_COMPLETION_DIR], $with_fish_completion_dir) - - ]) - -dnl -dnl Debug support -AC_ARG_ENABLE(debug, - AS_HELP_STRING([--enable-debug], [Pass debug option to the compiler])) -AS_IF([test "x$enable_debug" = "xyes"], AC_SUBST([DEBUG],yes), AC_SUBST([DEBUG],no)) - -dnl generate necessary files -AC_CONFIG_FILES([ - Makefile - contrib/pacman/PKGBUILD - contrib/spec/onedrive.spec - onedrive.1 - contrib/systemd/onedrive.service - contrib/systemd/onedrive@.service - ]) -AC_OUTPUT diff --git a/contrib/completions/complete.bash b/contrib/completions/complete.bash deleted file mode 100644 index 35808464..00000000 --- a/contrib/completions/complete.bash +++ /dev/null @@ -1,49 +0,0 @@ -# BASH completion code for OneDrive Linux Client -# (c) 2019 Norbert Preining -# License: GPLv3+ (as with the rest of the OneDrive Linux client project) - -_onedrive() -{ - local cur prev - - COMPREPLY=() - cur=${COMP_WORDS[COMP_CWORD]} - prev=${COMP_WORDS[COMP_CWORD-1]} - - options='--check-for-nomount --check-for-nosync --debug-https --disable-notifications --display-config --display-sync-status --download-only --disable-upload-validation --dry-run --enable-logging --force-http-1.1 --force-http-2 --get-file-link --local-first --logout -m --monitor --no-remote-delete --print-token --reauth --resync --skip-dot-files --skip-symlinks --synchronize --upload-only -v --verbose --version -h --help' - argopts='--create-directory --get-O365-drive-id --operation-timeout --remove-directory --single-directory --source-directory' - - # Loop on the arguments to manage conflicting options - for (( i=0; i < ${#COMP_WORDS[@]}-1; i++ )); do - #exclude some mutually exclusive options - [[ ${COMP_WORDS[i]} == '--synchronize' ]] && options=${options/--monitor} - [[ ${COMP_WORDS[i]} == '--monitor' ]] && options=${options/--synchronize} - done - - case "$prev" in - --confdir|--syncdir) - _filedir - return 0 - ;; - - --get-file-link) - if command -v sed &> /dev/null; then - pushd "$(onedrive --display-config | sed -n "/sync_dir/s/.*= //p")" &> /dev/null - _filedir - popd &> /dev/null - fi - return 0 - ;; - --create-directory|--get-O365-drive-id|--operation-timeout|--remove-directory|--single-directory|--source-directory) - return 0 - ;; - *) - COMPREPLY=( $( compgen -W "$options $argopts" -- "$cur")) - return 0 - ;; - esac - - # notreached - return 0 -} -complete -F _onedrive onedrive diff --git a/contrib/completions/complete.fish b/contrib/completions/complete.fish deleted file mode 100644 index 7547574c..00000000 --- a/contrib/completions/complete.fish +++ /dev/null @@ -1,38 +0,0 @@ -# FISH completions for OneDrive Linux Client -# License: GPLv3+ (as with the rest of the OneDrive Linux client project) - -complete -c onedrive -f - -complete -c onedrive -l check-for-nomount -d 'Check for the presence of .nosync in the syncdir root. If found, do not perform sync.' -complete -c onedrive -l check-for-nosync -d 'Check for the presence of .nosync in each directory. If found, skip directory from sync.' -complete -c onedrive -l create-directory -d 'Create a directory on OneDrive - no sync will be performed.' -complete -c onedrive -l debug-https -d 'Debug OneDrive HTTPS communication.' -complete -c onedrive -l disable-notifications -d 'Do not use desktop notifications in monitor mode.' -complete -c onedrive -l disable-upload-validation -d 'Disable upload validation when uploading to OneDrive.' -complete -c onedrive -l display-config -d 'Display what options the client will use as currently configured - no sync will be performed.' -complete -c onedrive -l display-sync-status -d 'Display the sync status of the client - no sync will be performed.' -complete -c onedrive -l download-only -d 'Only download remote changes.' -complete -c onedrive -l dry-run -d 'Perform a trial sync with no changes made.' -complete -c onedrive -l enable-logging -d 'Enable client activity to a separate log file.' -complete -c onedrive -l force-http-1.1 -d 'Force the use of HTTP 1.1 for all operations.' -complete -c onedrive -l force-http-2 -d 'Force the use of HTTP 2 for all operations.' -complete -c onedrive -l get-file-link -d 'Display the file link of a synced file.' -complete -c onedrive -l get-O365-drive-id -d 'Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library.' -complete -c onedrive -s h -l help -d 'Print help information.' -complete -c onedrive -l local-first -d 'Synchronize from the local directory source first, before downloading changes from OneDrive.' -complete -c onedrive -l logout -d 'Logout the current user.' -complete -c onedrive -n "not __fish_seen_subcommand_from --synchronize" -a "-m --monitor" -d 'Keep monitoring for local and remote changes.' -complete -c onedrive -l no-remote-delete -d 'Do not delete local file deletes from OneDrive when using --upload-only.' -complete -c onedrive -l operation-timeout -d 'Specify the maximum amount of time (in seconds) an operation is allowed to take.' -complete -c onedrive -l print-token -d 'Print the access token, useful for debugging.' -complete -c onedrive -l remote-directory -d 'Remove a directory on OneDrive - no sync will be performed.' -complete -c onedrive -l reauth -d 'Reauthenticate the client with OneDrive.' -complete -c onedrive -l resync -d 'Forget the last saved state, perform a full sync.' -complete -c onedrive -l single-directory -d 'Specify a single local directory within the OneDrive root to sync.' -complete -c onedrive -l skip-dot-files -d 'Skip dot files and folders from syncing.' -complete -c onedrive -l skip-symlinks -d 'Skip syncing of symlinks.' -complete -c onedrive -l source-directory -d 'Source directory to rename or move on OneDrive - no sync will be performed.' -complete -c onedrive -n "not __fish_seen_subcommand_from --monitor; and not __fish_seen_subcommand_from -m" -l synchronize -d 'Perform a synchronization.' -complete -c onedrive -l upload-only -d 'Only upload to OneDrive, do not sync changes from OneDrive locally' -complete -c onedrive -s v -l verbose -d 'Print more details, useful for debugging (repeat for extra debugging).' -complete -c onedrive -l version -d 'Print the version and exit.' diff --git a/contrib/completions/complete.zsh b/contrib/completions/complete.zsh deleted file mode 100644 index b03ea686..00000000 --- a/contrib/completions/complete.zsh +++ /dev/null @@ -1,48 +0,0 @@ -#compdef onedrive -# -# ZSH completion code for OneDrive Linux Client -# (c) 2019 Norbert Preining -# License: GPLv3+ (as with the rest of the OneDrive Linux client project) - -local -a all_opts -all_opts=( - '--check-for-nomount[Check for the presence of .nosync in the syncdir root. If found, do not perform sync.]' - '--check-for-nosync[Check for the presence of .nosync in each directory. If found, skip directory from sync.]' - '--confdir[Set the directory used to store the configuration files]:config directory:_files -/' - '--create-directory[Create a directory on OneDrive - no sync will be performed.]:directory name:' - '--debug-https[Debug OneDrive HTTPS communication.]' - '--destination-directory[Destination directory for renamed or move on OneDrive - no sync will be performed.]:directory name:' - '--disable-notifications[Do not use desktop notifications in monitor mode.]' - '--display-config[Display what options the client will use as currently configured - no sync will be performed.]' - '--display-sync-status[Display the sync status of the client - no sync will be performed.]' - '--download-only[Only download remote changes]' - '--disable-upload-validation[Disable upload validation when uploading to OneDrive]' - '--dry-run[Perform a trial sync with no changes made]' - '--enable-logging[Enable client activity to a separate log file]' - '--force-http-1.1[Force the use of HTTP 1.1 for all operations]' - '--force-http-2[Force the use of HTTP 2 for all operations]' - '--get-file-link[Display the file link of a synced file.]:file name:' - '--get-O365-drive-id[Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library]:' - '--local-first[Synchronize from the local directory source first, before downloading changes from OneDrive.]' - '--logout[Logout the current user]' - '(-m --monitor)'{-m,--monitor}'[Keep monitoring for local and remote changes]' - '--no-remote-delete[Do not delete local file deletes from OneDrive when using --upload-only]' - '--operation-timeout[Specify the maximum amount of time (in seconds) an operation is allowed to take.]:seconds:' - '--print-token[Print the access token, useful for debugging]' - '--reauth[Reauthenticate the client with OneDrive]' - '--resync[Forget the last saved state, perform a full sync]' - '--remove-directory[Remove a directory on OneDrive - no sync will be performed.]:directory name:' - '--single-directory[Specify a single local directory within the OneDrive root to sync.]:source directory:_files -/' - '--skip-dot-files[Skip dot files and folders from syncing]' - '--skip-symlinks[Skip syncing of symlinks]' - '--source-directory[Source directory to rename or move on OneDrive - no sync will be performed.]:source directory:' - '--syncdir[Specify the local directory used for synchronization to OneDrive]:sync directory:_files -/' - '--synchronize[Perform a synchronization]' - '--upload-only[Only upload to OneDrive, do not sync changes from OneDrive locally]' - '(-v --verbose)'{-v,--verbose}'[Print more details, useful for debugging (repeat for extra debugging)]' - '--version[Print the version and exit]' - '(-h --help)'{-h,--help}'[Print help information]' -) - -_arguments -S "$all_opts[@]" && return 0 - diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile deleted file mode 100644 index fca95731..00000000 --- a/contrib/docker/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -# -*-Dockerfile-*- - -ARG FEDORA_VERSION=38 -ARG DEBIAN_VERSION=bullseye -ARG GO_VERSION=1.20 -ARG GOSU_VERSION=1.16 - -FROM golang:${GO_VERSION}-${DEBIAN_VERSION} AS builder-gosu -ARG GOSU_VERSION -RUN go install -ldflags "-s -w" github.com/tianon/gosu@${GOSU_VERSION} - -FROM fedora:${FEDORA_VERSION} AS builder-onedrive - -RUN dnf install -y ldc pkgconf libcurl-devel sqlite-devel git - -ENV PKG_CONFIG=/usr/bin/pkgconf - -COPY . /usr/src/onedrive -WORKDIR /usr/src/onedrive - -RUN ./configure \ - && make clean \ - && make \ - && make install - -FROM fedora:${FEDORA_VERSION} - -RUN dnf clean all \ - && dnf -y update - -RUN dnf install -y libcurl sqlite ldc-libs \ - && dnf clean all \ - && mkdir -p /onedrive/conf /onedrive/data - -COPY --from=builder-gosu /go/bin/gosu /usr/local/bin/ -COPY --from=builder-onedrive /usr/local/bin/onedrive /usr/local/bin/ - -COPY contrib/docker/entrypoint.sh / -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/contrib/docker/Dockerfile-alpine b/contrib/docker/Dockerfile-alpine deleted file mode 100644 index 4db39a69..00000000 --- a/contrib/docker/Dockerfile-alpine +++ /dev/null @@ -1,38 +0,0 @@ -# -*-Dockerfile-*- - -ARG ALPINE_VERSION=3.18 -ARG GO_VERSION=1.20 -ARG GOSU_VERSION=1.16 - -FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder-gosu -ARG GOSU_VERSION -RUN go install -ldflags "-s -w" github.com/tianon/gosu@${GOSU_VERSION} - -FROM alpine:${ALPINE_VERSION} AS builder-onedrive - -RUN apk add --update --no-cache alpine-sdk gnupg xz curl-dev sqlite-dev binutils-gold autoconf automake ldc - -COPY . /usr/src/onedrive -WORKDIR /usr/src/onedrive - -RUN autoreconf -fiv \ - && ./configure \ - && make clean \ - && make \ - && make install - -FROM alpine:${ALPINE_VERSION} - -RUN apk add --upgrade apk-tools \ - && apk upgrade --available - -RUN apk add --update --no-cache bash libcurl libgcc shadow sqlite-libs ldc-runtime \ - && mkdir -p /onedrive/conf /onedrive/data - -COPY --from=builder-gosu /go/bin/gosu /usr/local/bin/ -COPY --from=builder-onedrive /usr/local/bin/onedrive /usr/local/bin/ - -COPY contrib/docker/entrypoint.sh / -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/contrib/docker/Dockerfile-debian b/contrib/docker/Dockerfile-debian deleted file mode 100644 index a4cae5a4..00000000 --- a/contrib/docker/Dockerfile-debian +++ /dev/null @@ -1,36 +0,0 @@ -# -*-Dockerfile-*- - -ARG DEBIAN_VERSION=stable - -FROM debian:${DEBIAN_VERSION} AS builder-onedrive - -RUN apt-get clean \ - && apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential curl ca-certificates libcurl4-openssl-dev libsqlite3-dev libxml2-dev pkg-config git ldc \ - && rm -rf /var/lib/apt/lists/* - -COPY . /usr/src/onedrive -WORKDIR /usr/src/onedrive - -RUN ./configure DC=/usr/bin/ldmd2 \ - && make clean \ - && make \ - && make install - -FROM debian:${DEBIAN_VERSION}-slim - -RUN apt-get clean \ - && apt-get update \ - && apt-get upgrade -y \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends gosu libcurl4 libsqlite3-0 ca-certificates libphobos2-ldc-shared100 \ - && rm -rf /var/lib/apt/lists/* \ - # Fix bug with ssl on armhf: https://serverfault.com/a/1045189 - && /usr/bin/c_rehash \ - && mkdir -p /onedrive/conf /onedrive/data - -COPY --from=builder-onedrive /usr/local/bin/onedrive /usr/local/bin/ - -COPY contrib/docker/entrypoint.sh / -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/contrib/docker/entrypoint.sh b/contrib/docker/entrypoint.sh deleted file mode 100755 index 8eb52948..00000000 --- a/contrib/docker/entrypoint.sh +++ /dev/null @@ -1,132 +0,0 @@ -#!/bin/bash -eu - -set +H -euo pipefail - -: ${ONEDRIVE_UID:=$(stat /onedrive/data -c '%u')} -: ${ONEDRIVE_GID:=$(stat /onedrive/data -c '%g')} - -# Create new group using target GID -if ! odgroup="$(getent group "$ONEDRIVE_GID")"; then - odgroup='onedrive' - groupadd "${odgroup}" -g "$ONEDRIVE_GID" -else - odgroup=${odgroup%%:*} -fi - -# Create new user using target UID -if ! oduser="$(getent passwd "$ONEDRIVE_UID")"; then - oduser='onedrive' - useradd -m "${oduser}" -u "$ONEDRIVE_UID" -g "$ONEDRIVE_GID" -else - oduser="${oduser%%:*}" - usermod -g "${odgroup}" "${oduser}" - grep -qv root <( groups "${oduser}" ) || { echo 'ROOT level privileges prohibited!'; exit 1; } -fi - -# Default parameters -ARGS=(--monitor --confdir /onedrive/conf --syncdir /onedrive/data) -echo "Base Args: ${ARGS}" - -# Make Verbose output optional, based on an environment variable -if [ "${ONEDRIVE_VERBOSE:=0}" == "1" ]; then - echo "# We are being verbose" - echo "# Adding --verbose" - ARGS=(--verbose ${ARGS[@]}) -fi - -# Tell client to perform debug output, based on an environment variable -if [ "${ONEDRIVE_DEBUG:=0}" == "1" ]; then - echo "# We are performing debug output" - echo "# Adding --verbose --verbose" - 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" - echo "# Adding --debug-https" - 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" - echo "# Adding --resync --resync-auth" - ARGS=(--resync --resync-auth ${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" - echo "# Adding --download-only" - ARGS=(--download-only ${ARGS[@]}) -fi - -# Tell client to sync in upload-only mode based on environment variable -if [ "${ONEDRIVE_UPLOADONLY:=0}" == "1" ]; then - echo "# We are synchronizing in upload-only mode" - echo "# Adding --upload-only" - ARGS=(--upload-only ${ARGS[@]}) -fi - -# Tell client to sync in no-remote-delete mode based on environment variable -if [ "${ONEDRIVE_NOREMOTEDELETE:=0}" == "1" ]; then - echo "# We are synchronizing in no-remote-delete mode" - echo "# Adding --no-remote-delete" - ARGS=(--no-remote-delete ${ARGS[@]}) -fi - -# Tell client to logout based on environment variable -if [ "${ONEDRIVE_LOGOUT:=0}" == "1" ]; then - echo "# We are logging out" - echo "# Adding --logout" - ARGS=(--logout ${ARGS[@]}) -fi - -# Tell client to re-authenticate based on environment variable -if [ "${ONEDRIVE_REAUTH:=0}" == "1" ]; then - echo "# We are logging out to perform a reauthentication" - echo "# Adding --reauth" - ARGS=(--reauth ${ARGS[@]}) -fi - -# Tell client to utilize auth files at the provided locations based on environment variable -if [ -n "${ONEDRIVE_AUTHFILES:=""}" ]; then - echo "# We are using auth files to perform authentication" - echo "# Adding --auth-files ARG" - ARGS=(--auth-files ${ONEDRIVE_AUTHFILES} ${ARGS[@]}) -fi - -# Tell client to utilize provided auth response based on environment variable -if [ -n "${ONEDRIVE_AUTHRESPONSE:=""}" ]; then - echo "# We are providing the auth response directly to perform authentication" - echo "# Adding --auth-response ARG" - ARGS=(--auth-response \"${ONEDRIVE_AUTHRESPONSE}\" ${ARGS[@]}) -fi - -# Tell client to print the running configuration at application startup -if [ "${ONEDRIVE_DISPLAY_CONFIG:=0}" == "1" ]; then - echo "# We are printing the application running configuration at application startup" - echo "# Adding --display-running-config" - ARGS=(--display-running-config ${ARGS[@]}) -fi - -# Tell client to use sync single dir option -if [ -n "${ONEDRIVE_SINGLE_DIRECTORY:=""}" ]; then - echo "# We are synchronizing in single-directory mode" - echo "# Adding --single-directory ARG" - ARGS=(--single-directory \"${ONEDRIVE_SINGLE_DIRECTORY}\" ${ARGS[@]}) -fi - -if [ ${#} -gt 0 ]; then - ARGS=("${@}") -fi - -echo "# Launching onedrive" -# Only switch user if not running as target uid (ie. Docker) -if [ "$ONEDRIVE_UID" = "$(id -u)" ]; then - /usr/local/bin/onedrive "${ARGS[@]}" -else - chown "${oduser}:${odgroup}" /onedrive/data /onedrive/conf - exec gosu "${oduser}" /usr/local/bin/onedrive "${ARGS[@]}" -fi diff --git a/contrib/docker/hooks/post_push b/contrib/docker/hooks/post_push deleted file mode 100755 index 53289f1a..00000000 --- a/contrib/docker/hooks/post_push +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -BUILD_DATE=`date "+%Y%m%d%H%M"` - -docker tag ${IMAGE_NAME} "${IMAGE_NAME}-${BUILD_DATE}" -docker push "${IMAGE_NAME}-${BUILD_DATE}" diff --git a/contrib/gentoo/onedrive-2.4.25.ebuild b/contrib/gentoo/onedrive-2.4.25.ebuild deleted file mode 100644 index da9b9407..00000000 --- a/contrib/gentoo/onedrive-2.4.25.ebuild +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 1999-2018 Gentoo Foundation -# Distributed under the terms of the GNU General Public License v2 - -EAPI=6 - -DESCRIPTION="Onedrive sync client for Linux" -HOMEPAGE="https://github.com/abraunegg/onedrive" -SRC_URI="https://github.com/abraunegg/onedrive/archive/v${PV}.tar.gz -> ${P}.tar.gz" - -LICENSE="GPL-3" -SLOT="0" -KEYWORDS="~amd64 ~x86" -IUSE="" - -DEPEND=" - >=dev-lang/dmd-2.081.1 - dev-db/sqlite -" - -RDEPEND="${DEPEND} - net-misc/curl - " -src_prepare() { - default - # Copy line 38 to 44 as systemd path needs to be created in portage sandbox - # Update the makefile so that it doesnt use git commands to get the version during build. - sed -i -e "38h; 44p; 44x" \ - -e "s/version:.*/version:/" \ - -e "\$s/.*/\techo v${PV} > version/" \ - Makefile -} diff --git a/contrib/init.d/onedrive.init b/contrib/init.d/onedrive.init deleted file mode 100644 index a249954e..00000000 --- a/contrib/init.d/onedrive.init +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/sh -# -# chkconfig: 2345 20 80 -# description: Starts and stops OneDrive Free Client -# - -# Source function library. -if [ -f /etc/init.d/functions ] ; then - . /etc/init.d/functions -elif [ -f /etc/rc.d/init.d/functions ] ; then - . /etc/rc.d/init.d/functions -else - exit 1 -fi - -# Source networking configuration. -. /etc/sysconfig/network - -# Check that networking is up. -[ ${NETWORKING} = "no" ] && exit 1 - -APP_NAME="OneDrive Free Client" -STOP_TIMEOUT=${STOP_TIMEOUT-5} -RETVAL=0 - -start() { - export PATH=/usr/local/bin/:$PATH - echo -n "Starting $APP_NAME: " - daemon --user root onedrive_service.sh - RETVAL=$? - echo - [ $RETVAL -eq 0 ] && touch /var/lock/subsys/onedrive || \ - RETVAL=1 - return $RETVAL -} - -stop() { - echo -n "Shutting down $APP_NAME: " - killproc onedrive - RETVAL=$? - echo - [ $RETVAL = 0 ] && rm -f /var/lock/subsys/onedrive ${pidfile} -} - -restart() { - stop - start -} - -rhstatus() { - status onedrive - return $? -} - -# Allow status as non-root. -if [ "$1" = status ]; then - rhstatus - exit $? -fi - -case "$1" in - start) - start - ;; - stop) - stop - ;; - restart) - restart - ;; - reload) - reload - ;; - status) - rhstatus - ;; - *) - echo "Usage: $0 {start|stop|restart|reload|status}" - exit 2 -esac - -exit $? diff --git a/contrib/init.d/onedrive_service.sh b/contrib/init.d/onedrive_service.sh deleted file mode 100644 index 4d03ff03..00000000 --- a/contrib/init.d/onedrive_service.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -# This script is to assist in starting the onedrive client when using init.d -APP_OPTIONS="--monitor --verbose --enable-logging" -onedrive "$APP_OPTIONS" > /dev/null 2>&1 & -exit 0 diff --git a/contrib/logrotate/onedrive.logrotate b/contrib/logrotate/onedrive.logrotate deleted file mode 100644 index 73a88d74..00000000 --- a/contrib/logrotate/onedrive.logrotate +++ /dev/null @@ -1,26 +0,0 @@ -# Any OneDrive Client logs configured for here - -/var/log/onedrive/*log { - # What user / group should logrotate use? - # Logrotate 3.8.9 or greater required otherwise: - # "unknown option 'su' -- ignoring line" is generated - su root users - - # rotate log files weekly - weekly - - # keep 4 weeks worth of backlogs - rotate 4 - - # create new (empty) log files after rotating old ones - create - - # use date as a suffix of the rotated file - dateext - - # compress the log files - compress - - # missing files OK - missingok -} diff --git a/contrib/pacman/PKGBUILD.in b/contrib/pacman/PKGBUILD.in deleted file mode 100644 index 98ad4901..00000000 --- a/contrib/pacman/PKGBUILD.in +++ /dev/null @@ -1,30 +0,0 @@ -pkgname=onedrive -pkgver=@PACKAGE_VERSION@ -pkgrel=1 #patch-level (Increment this when patch is applied) -pkgdesc="A free OneDrive Client for Linux. This is a fork of the https://github.com/skilion/onedrive repository" -license=("unknown") -url="https://github.com/abraunegg/onedrive/" -arch=("i686" "x86_64") - -depends=("curl" "gcc-libs" "glibc" "sqlite") -makedepends=("dmd" "git" "tar") - -prepare() { - cd "$srcdir" - wget "https://github.com/abraunegg/onedrive/archive/v$pkgver.tar.gz" -O "$pkgname-$pkgver-patch-$pkgrel.tar.gz" #Pull last commit release - tar -xzf "$pkgname-$pkgver-patch-$pkgrel.tar.gz" --one-top-level="$pkgname-$pkgver-patch-$pkgrel" --strip-components 1 -} - -build() { - cd "$pkgname-$pkgver-patch-$pkgrel" - git init #Create .git folder required from Makefile - git add * #Create .git/index - git commit --allow-empty-message -m "" #Create .git/refs/heads/master - git tag v$pkgver #Add version tag - make PREFIX=/usr onedrive -} - -package() { - cd "$pkgname-$pkgver-patch-$pkgrel" - make PREFIX=/usr DESTDIR="$pkgdir" install -} diff --git a/contrib/spec/onedrive.spec.in b/contrib/spec/onedrive.spec.in deleted file mode 100644 index 2b4a22e9..00000000 --- a/contrib/spec/onedrive.spec.in +++ /dev/null @@ -1,80 +0,0 @@ -# Determine based on distribution & version what options & packages to include -%if 0%{?fedora} || 0%{?rhel} >= 7 -%global with_systemd 1 -%else -%global with_systemd 0 -%endif - -%if 0%{?rhel} >= 7 -%global rhel_unitdir 1 -%else -%global rhel_unitdir 0 -%endif - -Name: onedrive -Version: 2.4.25 -Release: 1%{?dist} -Summary: Microsoft OneDrive Client -Group: System Environment/Network -License: GPLv3 -URL: https://github.com/abraunegg/onedrive -Source0: v%{version}.tar.gz -BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) - -BuildRequires: dmd >= 2.088.0 -BuildRequires: sqlite-devel >= 3.7.15 -BuildRequires: libcurl-devel -Requires: sqlite >= 3.7.15 -Requires: libcurl - -%if 0%{?with_systemd} -Requires(post): systemd -Requires(preun): systemd -Requires(postun): systemd -%else -Requires(post): chkconfig -Requires(preun): chkconfig -Requires(preun): initscripts -Requires(postun): initscripts -%endif - -%define debug_package %{nil} - -%description -Microsoft OneDrive Client for Linux - -%prep - -%setup -q -# This creates the directory %{_builddir}/%{name}-%{version}/ - -%build -# cd %{_builddir}/%{name}-%{version} -%configure -make - -%install -%make_install PREFIX="%{buildroot}" - -%clean - -%files -%doc README.md LICENSE CHANGELOG.md -%config %{_sysconfdir}/logrotate.d/onedrive -%{_mandir}/man1/%{name}.1.gz -%{_docdir}/%{name} -%{_bindir}/%{name} -%if 0%{?with_systemd} -%if 0%{?rhel_unitdir} -%{_unitdir}/%{name}.service -%{_unitdir}/%{name}@.service -%else -%{_userunitdir}/%{name}.service -%{_unitdir}/%{name}@.service -%endif -%else -%{_bindir}/onedrive_service.sh -/etc/init.d/onedrive -%endif - -%changelog diff --git a/contrib/systemd/onedrive.service.in b/contrib/systemd/onedrive.service.in deleted file mode 100644 index d1e5607c..00000000 --- a/contrib/systemd/onedrive.service.in +++ /dev/null @@ -1,27 +0,0 @@ -[Unit] -Description=OneDrive Free Client -Documentation=https://github.com/abraunegg/onedrive -After=network-online.target -Wants=network-online.target - -[Service] -# Commented out hardenings are disabled because they may not work out of the box on your distribution -# If you know what you are doing please try to enable them. - -ProtectSystem=full -#PrivateUsers=true -#PrivateDevices=true -ProtectHostname=true -#ProtectClock=true -ProtectKernelTunables=true -#ProtectKernelModules=true -#ProtectKernelLogs=true -ProtectControlGroups=true -RestrictRealtime=true -ExecStart=@prefix@/bin/onedrive --monitor -Restart=on-failure -RestartSec=3 -RestartPreventExitStatus=3 - -[Install] -WantedBy=default.target \ No newline at end of file diff --git a/contrib/systemd/onedrive@.service.in b/contrib/systemd/onedrive@.service.in deleted file mode 100644 index 4d32859b..00000000 --- a/contrib/systemd/onedrive@.service.in +++ /dev/null @@ -1,27 +0,0 @@ -[Unit] -Description=OneDrive Free Client for %i -Documentation=https://github.com/abraunegg/onedrive -After=network-online.target -Wants=network-online.target - -[Service] -# Commented out hardenings are disabled because they don't work out of the box. -# If you know what you are doing please try to enable them. -ProtectSystem=full -#PrivateDevices=true -ProtectHostname=true -#ProtectClock=true -ProtectKernelTunables=true -#ProtectKernelModules=true -#ProtectKernelLogs=true -ProtectControlGroups=true -RestrictRealtime=true -ExecStart=@prefix@/bin/onedrive --monitor --confdir=/home/%i/.config/onedrive -User=%i -Group=users -Restart=on-failure -RestartSec=3 -RestartPreventExitStatus=3 - -[Install] -WantedBy=multi-user.target diff --git a/docs/BusinessSharedFolders.md b/docs/BusinessSharedFolders.md deleted file mode 100644 index 3f042943..00000000 --- a/docs/BusinessSharedFolders.md +++ /dev/null @@ -1,192 +0,0 @@ -# How to configure OneDrive Business Shared Folder Sync -## Application Version -Before reading this document, please ensure you are running application version [![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) or greater. Use `onedrive --version` to determine what application version you are using and upgrade your client if required. - -## Process Overview -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. 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: -```text -onedrive --list-shared-folders -``` - This will return a listing of all OneDrive Business Shared folders which have been shared with you and by whom. This is important for conflict resolution: -```text -Initializing the Synchronization Engine ... - -Listing available OneDrive Business Shared Folders: ---------------------------------------- -Shared Folder: SharedFolder0 -Shared By: Firstname Lastname ---------------------------------------- -Shared Folder: SharedFolder1 -Shared By: Firstname Lastname ---------------------------------------- -Shared Folder: SharedFolder2 -Shared By: Firstname Lastname ---------------------------------------- -Shared Folder: SharedFolder0 -Shared By: Firstname Lastname (user@domain) ---------------------------------------- -Shared Folder: SharedFolder1 -Shared By: Firstname Lastname (user@domain) ---------------------------------------- -Shared Folder: SharedFolder2 -Shared By: Firstname Lastname (user@domain) -... -``` - -## Configuring OneDrive Business Shared Folders -1. Create a new file called 'business_shared_folders' in your config directory -2. On each new line, list the OneDrive Business Shared Folder you wish to sync -```text -[alex@centos7full onedrive]$ cat ~/.config/onedrive/business_shared_folders -# comment -Child Shared Folder -# Another comment -Top Level to Share -[alex@centos7full onedrive]$ -``` -3. Validate your configuration with `onedrive --display-config`: -```text -Configuration file successfully loaded -onedrive version = v2.4.3 -Config path = /home/alex/.config/onedrive-business/ -Config file found in config path = true -Config option 'check_nosync' = false -Config option 'sync_dir' = /home/alex/OneDriveBusiness -Config option 'skip_dir' = -Config option 'skip_file' = ~*|.~*|*.tmp -Config option 'skip_dotfiles' = false -Config option 'skip_symlinks' = false -Config option 'monitor_interval' = 300 -Config option 'min_notify_changes' = 5 -Config option 'log_dir' = /var/log/onedrive/ -Config option 'classify_as_big_delete' = 1000 -Config option 'sync_root_files' = false -Selective sync 'sync_list' configured = false -Business Shared Folders configured = true -business_shared_folders contents: -# comment -Child Shared Folder -# Another comment -Top Level to Share -``` - -## Performing a sync of OneDrive Business Shared Folders -Perform a standalone sync using the following command: `onedrive --synchronize --sync-shared-folders --verbose`: -```text -onedrive --synchronize --sync-shared-folders --verbose -Using 'user' Config Dir: /home/alex/.config/onedrive-business/ -Using 'system' Config Dir: -Configuration file successfully loaded -Initializing the OneDrive API ... -Configuring Global Azure AD Endpoints -Opening the item database ... -All operations will be performed in: /home/alex/OneDriveBusiness -Application version: v2.4.3 -Account Type: business -Default Drive ID: b!bO8V7s9SSk6r7mWHpIjURotN33W1W2tEv3OXV_oFIdQimEdOHR-1So7CqeT1MfHA -Default Root ID: 01WIXGO5V6Y2GOVW7725BZO354PWSELRRZ -Remaining Free Space: 1098316220277 -Fetching details for OneDrive Root -OneDrive Root exists in the database -Initializing the Synchronization Engine ... -Syncing changes from OneDrive ... -Applying changes of Path ID: 01WIXGO5V6Y2GOVW7725BZO354PWSELRRZ -Number of items from OneDrive to process: 0 -Attempting to sync OneDrive Business Shared Folders -Syncing this OneDrive Business Shared Folder: Child Shared Folder -OneDrive Business Shared Folder - Shared By: test user -Applying changes of Path ID: 01JRXHEZMREEB3EJVHNVHKNN454Q7DFXPR -Adding OneDrive root details for processing -Adding OneDrive folder details for processing -Adding 4 OneDrive items for processing from OneDrive folder -Adding 2 OneDrive items for processing from /Child Shared Folder/Cisco VDI Whitepaper -Adding 2 OneDrive items for processing from /Child Shared Folder/SMPP_Shared -Processing 11 OneDrive items to ensure consistent local state -Syncing this OneDrive Business Shared Folder: Top Level to Share -OneDrive Business Shared Folder - Shared By: test user (testuser@mynasau3.onmicrosoft.com) -Applying changes of Path ID: 01JRXHEZLRMXHKBYZNOBF3TQOPBXS3VZMA -Adding OneDrive root details for processing -Adding OneDrive folder details for processing -Adding 4 OneDrive items for processing from OneDrive folder -Adding 3 OneDrive items for processing from /Top Level to Share/10-Files -Adding 2 OneDrive items for processing from /Top Level to Share/10-Files/Cisco VDI Whitepaper -Adding 2 OneDrive items for processing from /Top Level to Share/10-Files/Images -Adding 8 OneDrive items for processing from /Top Level to Share/10-Files/Images/JPG -Adding 8 OneDrive items for processing from /Top Level to Share/10-Files/Images/PNG -Adding 2 OneDrive items for processing from /Top Level to Share/10-Files/SMPP -Processing 31 OneDrive items to ensure consistent local state -Uploading differences of ~/OneDriveBusiness -Processing root -The directory has not changed -Processing SMPP_Local -The directory has not changed -Processing SMPP-IF-SPEC_v3_3-24858.pdf -The file has not changed -Processing SMPP_v3_4_Issue1_2-24857.pdf -The file has not changed -Processing new_local_file.txt -The file has not changed -Processing root -The directory has not changed -... -The directory has not changed -Processing week02-03-Combinational_Logic-v1.pptx -The file has not changed -Uploading new items of ~/OneDriveBusiness -Applying changes of Path ID: 01WIXGO5V6Y2GOVW7725BZO354PWSELRRZ -Number of items from OneDrive to process: 0 -Attempting to sync OneDrive Business Shared Folders -Syncing this OneDrive Business Shared Folder: Child Shared Folder -OneDrive Business Shared Folder - Shared By: test user -Applying changes of Path ID: 01JRXHEZMREEB3EJVHNVHKNN454Q7DFXPR -Adding OneDrive root details for processing -Adding OneDrive folder details for processing -Adding 4 OneDrive items for processing from OneDrive folder -Adding 2 OneDrive items for processing from /Child Shared Folder/Cisco VDI Whitepaper -Adding 2 OneDrive items for processing from /Child Shared Folder/SMPP_Shared -Processing 11 OneDrive items to ensure consistent local state -Syncing this OneDrive Business Shared Folder: Top Level to Share -OneDrive Business Shared Folder - Shared By: test user (testuser@mynasau3.onmicrosoft.com) -Applying changes of Path ID: 01JRXHEZLRMXHKBYZNOBF3TQOPBXS3VZMA -Adding OneDrive root details for processing -Adding OneDrive folder details for processing -Adding 4 OneDrive items for processing from OneDrive folder -Adding 3 OneDrive items for processing from /Top Level to Share/10-Files -Adding 2 OneDrive items for processing from /Top Level to Share/10-Files/Cisco VDI Whitepaper -Adding 2 OneDrive items for processing from /Top Level to Share/10-Files/Images -Adding 8 OneDrive items for processing from /Top Level to Share/10-Files/Images/JPG -Adding 8 OneDrive items for processing from /Top Level to Share/10-Files/Images/PNG -Adding 2 OneDrive items for processing from /Top Level to Share/10-Files/SMPP -Processing 31 OneDrive items to ensure consistent local state -``` - -**Note:** Whenever you modify the `business_shared_folders` file you must perform a `--resync` of your database to clean up stale entries due to changes in your configuration. - -## Enable / Disable syncing of OneDrive Business Shared Folders -Performing a sync of the configured OneDrive Business Shared Folders can be enabled / disabled via adding the following to your configuration file. - -### Enable syncing of OneDrive Business Shared Folders via config file -```text -sync_business_shared_folders = "true" -``` - -### Disable syncing of OneDrive Business Shared Folders via config file -```text -sync_business_shared_folders = "false" -``` - -## Known Issues -Shared folders, shared with you from people outside of your 'organisation' are unable to be synced. This is due to the Microsoft Graph API not presenting these folders. - -Shared folders that match this scenario, when you view 'Shared' via OneDrive online, will have a 'world' symbol as per below: - -![shared_with_me](./images/shared_with_me.JPG) - -This issue is being tracked by: [#966](https://github.com/abraunegg/onedrive/issues/966) diff --git a/docs/Docker.md b/docs/Docker.md deleted file mode 100644 index 1f0050fd..00000000 --- a/docs/Docker.md +++ /dev/null @@ -1,396 +0,0 @@ -# Run the OneDrive Client for Linux under Docker -This client can be run as a Docker container, with 3 available container base options for you to choose from: - -| Container Base | Docker Tag | Description | i686 | x86_64 | ARMHF | AARCH64 | -|----------------|-------------|----------------------------------------------------------------|:------:|:------:|:-----:|:-------:| -| Alpine Linux | edge-alpine | Docker container based on Alpine 3.18 using 'master' |❌|✔|❌|✔| -| Alpine Linux | alpine | Docker container based on Alpine 3.18 using latest release |❌|✔|❌|✔| -| Debian | debian | Docker container based on Debian Stable using latest release |✔|✔|✔|✔| -| Debian | edge | Docker container based on Debian Stable using 'master' |✔|✔|✔|✔| -| Debian | edge-debian | Docker container based on Debian Stable using 'master' |✔|✔|✔|✔| -| Debian | latest | Docker container based on Debian Stable using latest release |✔|✔|✔|✔| -| Fedora | edge-fedora | Docker container based on Fedora 38 using 'master' |❌|✔|❌|✔| -| Fedora | fedora | Docker container based on Fedora 38 using latest release |❌|✔|❌|✔| - -These containers offer a simple monitoring-mode service for the OneDrive Client for Linux. - -The instructions below have been validated on: -* Fedora 38 - -The instructions below will utilise the 'edge' tag, however this can be substituted for any of the other docker tags such as 'latest' from the table above if desired. - -The 'edge' Docker Container will align closer to all documentation and features, where as 'latest' is the release version from a static point in time. The 'latest' tag however may contain bugs and/or issues that will have been fixed, and those fixes are contained in 'edge'. - -Additionally there are specific version release tags for each release. Refer to https://hub.docker.com/r/driveone/onedrive/tags for any other Docker tags you may be interested in. - -**Note:** The below instructions for docker has been tested and validated when logging into the system as an unprivileged user (non 'root' user). - -## High Level Configuration Steps -1. Install 'docker' as per your distribution platform's instructions if not already installed. -2. Configure 'docker' to allow non-privileged users to run Docker commands -3. Disable 'SELinux' as per your distribution platform's instructions -4. Test 'docker' by running a test container without using `sudo` -5. Prepare the required docker volumes to store the configuration and data -6. Run the 'onedrive' container and perform authorisation -7. Running the 'onedrive' container under 'docker' - -## Configuration Steps - -### 1. Install 'docker' on your platform -Install 'docker' as per your distribution platform's instructions if not already installed. - -### 2. Configure 'docker' to allow non-privileged users to run Docker commands -Read https://docs.docker.com/engine/install/linux-postinstall/ to configure the 'docker' user group with your user account to allow your non 'root' user to run 'docker' commands. - -### 3. Disable SELinux on your platform -In order to run the Docker container, SELinux must be disabled. Without doing this, when the application is authenticated in the steps below, the following error will be presented: -```text -ERROR: The local file system returned an error with the following message: - Error Message: /onedrive/conf/refresh_token: Permission denied - -The database cannot be opened. Please check the permissions of ~/.config/onedrive/items.sqlite3 -``` -The only known work-around for the above problem at present is to disable SELinux. Please refer to your distribution platform's instructions on how to perform this step. - -* Fedora: https://docs.fedoraproject.org/en-US/quick-docs/selinux-changing-states-and-modes/#_disabling_selinux -* Red Hat Enterprise Linux: https://access.redhat.com/solutions/3176 - -Post disabling SELinux and reboot your system, confirm that `getenforce` returns `Disabled`: -```text -$ getenforce -Disabled -``` - -If you are still experiencing permission issues despite disabling SELinux, please read https://www.redhat.com/sysadmin/container-permission-denied-errors - -### 4. Test 'docker' on your platform -Ensure that 'docker' is running as a system service, and is enabled to be activated on system reboot: -```bash -sudo systemctl enable --now docker -``` - -Test that 'docker' is operational for your 'non-root' user, as per below: -```bash -[alex@fedora-38-docker-host ~]$ docker run hello-world -Unable to find image 'hello-world:latest' locally -latest: Pulling from library/hello-world -719385e32844: Pull complete -Digest: sha256:88ec0acaa3ec199d3b7eaf73588f4518c25f9d34f58ce9a0df68429c5af48e8d -Status: Downloaded newer image for hello-world:latest - -Hello from Docker! -This message shows that your installation appears to be working correctly. - -To generate this message, Docker took the following steps: - 1. The Docker client contacted the Docker daemon. - 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. - (amd64) - 3. The Docker daemon created a new container from that image which runs the - executable that produces the output you are currently reading. - 4. The Docker daemon streamed that output to the Docker client, which sent it - to your terminal. - -To try something more ambitious, you can run an Ubuntu container with: - $ docker run -it ubuntu bash - -Share images, automate workflows, and more with a free Docker ID: - https://hub.docker.com/ - -For more examples and ideas, visit: - https://docs.docker.com/get-started/ - -[alex@fedora-38-docker-host ~]$ -``` - -### 5. Configure the required docker volumes -The 'onedrive' Docker container requires 2 docker volumes to operate: -* Config Volume -* Data Volume - -The first volume is the configuration volume that stores all the applicable application configuration + current runtime state. In a non-containerised environment, this normally resides in `~/.config/onedrive` - in a containerised environment this is stored in the volume tagged as `/onedrive/conf` - -The second volume is the data volume, where all your data from Microsoft OneDrive is stored locally. This volume is mapped to an actual directory point on your local filesystem and this is stored in the volume tagged as `/onedrive/data` - -#### 5.1 Prepare the 'config' volume -Create the 'config' volume with the following command: -```bash -docker volume create onedrive_conf -``` - -This will create a docker volume labeled `onedrive_conf`, where all configuration of your onedrive account will be stored. You can add a custom config file in this location at a later point in time if required. - -#### 5.2 Prepare the 'data' volume -Create the 'data' volume with the following command: -```bash -docker volume create onedrive_data -``` - -This will create a docker volume labeled `onedrive_data` and will map to a path on your local filesystem. This is where your data from Microsoft OneDrive will be stored. Keep in mind that: - -* The owner of this specified folder must not be root -* The owner of this specified folder must have permissions for its parent directory -* Docker will attempt to change the permissions of the volume to the user the container is configured to run as - -**NOTE:** Issues occur when this target folder is a mounted folder of an external system (NAS, SMB mount, USB Drive etc) as the 'mount' itself is owed by 'root'. If this is your use case, you *must* ensure your normal user can mount your desired target without having the target mounted by 'root'. If you do not fix this, your Docker container will fail to start with the following error message: -```bash -ROOT level privileges prohibited! -``` - -### 6. First run of Docker container under docker and performing authorisation -The 'onedrive' client within the container first needs to be authorised with your Microsoft account. This is achieved by initially running docker in interactive mode. - -Run the docker image with the commands below and make sure to change the value of `ONEDRIVE_DATA_DIR` to the actual onedrive data directory on your filesystem that you wish to use (e.g. `export ONEDRIVE_DATA_DIR="/home/abraunegg/OneDrive"`). - -**Important:** The 'target' folder of `ONEDRIVE_DATA_DIR` must exist before running the docker container. The script below will create 'ONEDRIVE_DATA_DIR' so that it exists locally for the docker volume mapping to occur. - -It is also a requirement that the container be run using a non-root uid and gid, you must insert a non-root UID and GID (e.g.` export ONEDRIVE_UID=1000` and export `ONEDRIVE_GID=1000`). The script below will use `id` to evaluate your system environment to use the correct values. -```bash -export ONEDRIVE_DATA_DIR="${HOME}/OneDrive" -export ONEDRIVE_UID=`id -u` -export ONEDRIVE_GID=`id -g` -mkdir -p ${ONEDRIVE_DATA_DIR} -docker run -it --name onedrive -v onedrive_conf:/onedrive/conf \ - -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" \ - -e "ONEDRIVE_UID=${ONEDRIVE_UID}" \ - -e "ONEDRIVE_GID=${ONEDRIVE_GID}" \ - driveone/onedrive:edge -``` - -When the Docker container successfully starts: -* You will be asked to open a specific link using your web browser -* Login to your Microsoft Account and give the application the permission -* After giving the permission, you will be redirected to a blank page -* Copy the URI of the blank page into the application prompt to authorise the application - -Once the 'onedrive' application is authorised, the client will automatically start monitoring your `ONEDRIVE_DATA_DIR` for data changes to be uploaded to OneDrive. Files stored on OneDrive will be downloaded to this location. - -If the client is working as expected, you can detach from the container with Ctrl+p, Ctrl+q. - -### 7. Running the 'onedrive' container under 'docker' - -#### 7.1 Check if the monitor service is running -```bash -docker ps -f name=onedrive -``` - -#### 7.2 Show 'onedrive' runtime logs -```bash -docker logs onedrive -``` - -#### 7.3 Stop running 'onedrive' container -```bash -docker stop onedrive -``` - -#### 7.4 Start 'onedrive' container -```bash -docker start onedrive -``` - -#### 7.5 Remove 'onedrive' container -```bash -docker rm -f onedrive -``` - -## Advanced Usage - -### How to use Docker-compose -You can utilise `docker-compose` if available on your platform if you are able to use docker compose schemas > 3. - -In the following example it is assumed you have a `ONEDRIVE_DATA_DIR` environment variable and have already created the `onedrive_conf` volume. - -You can also use docker bind mounts for the configuration folder, e.g. `export ONEDRIVE_CONF="${HOME}/OneDriveConfig"`. - -``` -version: "3" -services: - onedrive: - image: driveone/onedrive:edge - restart: unless-stopped - environment: - - ONEDRIVE_UID=${PUID} - - ONEDRIVE_GID=${PGID} - volumes: - - onedrive_conf:/onedrive/conf - - ${ONEDRIVE_DATA_DIR}:/onedrive/data -``` - -Note that you still have to perform step 3: First Run. - -### Editing the running configuration and using a 'config' file -The 'onedrive' client should run in default configuration, however you can change this default configuration by placing a custom config file in the `onedrive_conf` docker volume. First download the default config from [here](https://raw.githubusercontent.com/abraunegg/onedrive/master/config) -Then put it into your onedrive_conf volume path, which can be found with: - -```bash -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: [Configuration](https://github.com/abraunegg/onedrive/blob/master/docs/USAGE.md#configuration) - -### Syncing multiple accounts -There are many ways to do this, the easiest is probably to do the following: -1. Create a second docker config volume (replace `Work` with your desired name): `docker volume create onedrive_conf_Work` -2. And start a second docker monitor container (again replace `Work` with your desired name): -``` -export ONEDRIVE_DATA_DIR_WORK="/home/abraunegg/OneDriveWork" -mkdir -p ${ONEDRIVE_DATA_DIR_WORK} -docker run -it --restart unless-stopped --name onedrive_Work -v onedrive_conf_Work:/onedrive/conf -v "${ONEDRIVE_DATA_DIR_WORK}:/onedrive/data" driveone/onedrive:edge -``` - -### Run or update the Docker container with one script -If you are experienced with docker and onedrive, you can use the following script: - -```bash -# Update ONEDRIVE_DATA_DIR with correct OneDrive directory path -ONEDRIVE_DATA_DIR="${HOME}/OneDrive" -# Create directory if non-existant -mkdir -p ${ONEDRIVE_DATA_DIR} - -firstRun='-d' -docker pull driveone/onedrive:edge -docker inspect onedrive_conf > /dev/null 2>&1 || { docker volume create onedrive_conf; firstRun='-it'; } -docker inspect onedrive > /dev/null 2>&1 && docker rm -f onedrive -docker run $firstRun --restart unless-stopped --name onedrive -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" driveone/onedrive:edge -``` - -## Supported Docker Environment Variables -| Variable | Purpose | Sample Value | -| ---------------- | --------------------------------------------------- |:--------------------------------------------------------------------------------------------------------------------------------:| -| ONEDRIVE_UID | UserID (UID) to run as | 1000 | -| ONEDRIVE_GID | GroupID (GID) to run as | 1000 | -| ONEDRIVE_VERBOSE | Controls "--verbose" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_DEBUG | Controls "--verbose --verbose" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_DEBUG_HTTPS | Controls "--debug-https" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_RESYNC | Controls "--resync" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_DOWNLOADONLY | Controls "--download-only" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_UPLOADONLY | Controls "--upload-only" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_NOREMOTEDELETE | Controls "--no-remote-delete" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_LOGOUT | Controls "--logout" switch. Default is 0 | 1 | -| ONEDRIVE_REAUTH | Controls "--reauth" switch. Default is 0 | 1 | -| ONEDRIVE_AUTHFILES | Controls "--auth-files" option. Default is "" | "authUrl:responseUrl" | -| ONEDRIVE_AUTHRESPONSE | Controls "--auth-response" option. Default is "" | See [here](https://github.com/abraunegg/onedrive/blob/master/docs/USAGE.md#authorize-the-application-with-your-onedrive-account) | -| ONEDRIVE_DISPLAY_CONFIG | Controls "--display-running-config" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_SINGLE_DIRECTORY | Controls "--single-directory" option. Default = "" | "mydir" | - -### Environment Variables Usage Examples -**Verbose Output:** -```bash -docker container run -e ONEDRIVE_VERBOSE=1 -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" driveone/onedrive:edge -``` -**Debug Output:** -```bash -docker container run -e ONEDRIVE_DEBUG=1 -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" driveone/onedrive:edge -``` -**Perform a --resync:** -```bash -docker container run -e ONEDRIVE_RESYNC=1 -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" driveone/onedrive:edge -``` -**Perform a --resync and --verbose:** -```bash -docker container run -e ONEDRIVE_RESYNC=1 -e ONEDRIVE_VERBOSE=1 -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" driveone/onedrive:edge -``` -**Perform a --logout and re-authenticate:** -```bash -docker container run -it -e ONEDRIVE_LOGOUT=1 -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" driveone/onedrive:edge -``` - -## Building a custom Docker image - -### Build Environment Requirements -* Build environment must have at least 1GB of memory & 2GB swap space - -You can validate your build environment memory status with the following command: -```text -cat /proc/meminfo | grep -E 'MemFree|Swap' -``` -This should result in the following similar output: -```text -MemFree: 3704644 kB -SwapCached: 0 kB -SwapTotal: 8117244 kB -SwapFree: 8117244 kB -``` - -If you do not have enough swap space, you can use the following script to dynamically allocate a swapfile for building the Docker container: - -```bash -cd /var -sudo fallocate -l 1.5G swapfile -sudo chmod 600 swapfile -sudo mkswap swapfile -sudo swapon swapfile -# make swap permanent -sudo nano /etc/fstab -# add "/swapfile swap swap defaults 0 0" at the end of file -# check it has been assigned -swapon -s -free -h -``` - -If you are running a Raspberry Pi, you will need to edit your system configuration to increase your swapfile: - -* Modify the file `/etc/dphys-swapfile` and edit the `CONF_SWAPSIZE`, for example: `CONF_SWAPSIZE=2048`. - -A reboot of your Raspberry Pi is required to make this change effective. - -### Building and running a custom Docker image -You can also build your own image instead of pulling the one from [hub.docker.com](https://hub.docker.com/r/driveone/onedrive): -```bash -git clone https://github.com/abraunegg/onedrive -cd onedrive -docker build . -t local-onedrive -f contrib/docker/Dockerfile -docker container run -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" local-onedrive:latest -``` - -There are alternate, smaller images available by using `Dockerfile-debian` or `Dockerfile-alpine`. These [multi-stage builder pattern](https://docs.docker.com/develop/develop-images/multistage-build/) Dockerfiles require Docker version at least 17.05. - -### How to build and run a custom Docker image based on Debian -``` bash -docker build . -t local-ondrive-debian -f contrib/docker/Dockerfile-debian -docker container run -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" local-ondrive-debian:latest -``` - -### How to build and run a custom Docker image based on Alpine Linux -``` bash -docker build . -t local-ondrive-alpine -f contrib/docker/Dockerfile-alpine -docker container run -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" local-ondrive-alpine:latest -``` - -### How to build and run a custom Docker image for ARMHF (Raspberry Pi) -Compatible with: -* Raspberry Pi -* Raspberry Pi 2 -* Raspberry Pi Zero -* Raspberry Pi 3 -* Raspberry Pi 4 -``` bash -docker build . -t local-onedrive-armhf -f contrib/docker/Dockerfile-debian -docker container run -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" local-onedrive-armhf:latest -``` - -### How to build and run a custom Docker image for AARCH64 Platforms -``` bash -docker build . -t local-onedrive-aarch64 -f contrib/docker/Dockerfile-debian -docker container run -v onedrive_conf:/onedrive/conf -v "${ONEDRIVE_DATA_DIR}:/onedrive/data" local-onedrive-aarch64:latest -``` -### How to support double-byte languages -In some geographic regions, you may need to change and/or update the locale specification of the Docker container to better support the local language used for your local filesystem. To do this, follow the example below: -``` -FROM driveone/onedrive - -ENV DEBIAN_FRONTEND noninteractive - -RUN apt-get update -RUN apt-get install -y locales - -RUN echo "ja_JP.UTF-8 UTF-8" > /etc/locale.gen && \ - locale-gen ja_JP.UTF-8 && \ - dpkg-reconfigure locales && \ - /usr/sbin/update-locale LANG=ja_JP.UTF-8 - -ENV LC_ALL ja_JP.UTF-8 -``` -The above example changes the Docker container to support Japanese. To support your local language, change `ja_JP.UTF-8` to the required entry. \ No newline at end of file diff --git a/docs/INSTALL.md b/docs/INSTALL.md deleted file mode 100644 index 3f00ae21..00000000 --- a/docs/INSTALL.md +++ /dev/null @@ -1,277 +0,0 @@ -# Installing or Upgrading using Distribution Packages or Building the OneDrive Client for Linux from source - -## Installing or Upgrading using Distribution Packages -This project has been packaged for the following Linux distributions as per below. The current client release is: [![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) - -Only the current release version or greater is supported. Earlier versions are not supported and should not be installed or used. - -#### Important Note: -Distribution packages may be of an older release when compared to the latest release that is [available](https://github.com/abraunegg/onedrive/releases). If any package version indicator below is 'red' for your distribution, it is recommended that you build from source. Do not install the software from the available distribution package. If a package is out of date, please contact the package maintainer for resolution. - -| Distribution | Package Name & Package Link |   PKG_Version   |  i686  | x86_64 | ARMHF | AARCH64 | Extra Details | -|---------------------------------|------------------------------------------------------------------------------|:---------------:|:----:|:------:|:-----:|:-------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Alpine Linux | [onedrive](https://pkgs.alpinelinux.org/packages?name=onedrive&branch=edge) |Alpine Linux Edge package|❌|✔|❌|✔ | | -| Arch Linux

Manjaro Linux | [onedrive-abraunegg](https://aur.archlinux.org/packages/onedrive-abraunegg/) |AUR package|✔|✔|✔|✔ | Install via: `pamac build onedrive-abraunegg` from the Arch Linux User Repository (AUR)

**Note:** If asked regarding a provider for 'd-runtime' and 'd-compiler', select 'liblphobos' and 'ldc'

**Note:** System must have at least 1GB of memory & 1GB swap space -| Debian 11 | [onedrive](https://packages.debian.org/bullseye/source/onedrive) |Debian 11 package|✔|✔|✔|✔| **Note:** Do not install from Debian Package Repositories

It is recommended that for Debian 11 that you install from OpenSuSE Build Service using the Debian Package Install [Instructions](ubuntu-package-install.md) | -| Debian 12 | [onedrive](https://packages.debian.org/bookworm/source/onedrive) |Debian 12 package|✔|✔|✔|✔| **Note:** Do not install from Debian Package Repositories

It is recommended that for Debian 12 that you install from OpenSuSE Build Service using the Debian Package Install [Instructions](ubuntu-package-install.md) | -| Fedora | [onedrive](https://koji.fedoraproject.org/koji/packageinfo?packageID=26044) |Fedora Rawhide package|✔|✔|✔|✔| | -| Gentoo | [onedrive](https://gpo.zugaina.org/net-misc/onedrive) | No API Available |✔|✔|❌|❌| | -| Homebrew | [onedrive](https://formulae.brew.sh/formula/onedrive) | Homebrew package |❌|✔|❌|❌| | -| Linux Mint 20.x | [onedrive](https://community.linuxmint.com/software/view/onedrive) |Ubuntu 20.04 package |❌|✔|✔|✔| **Note:** Do not install from Linux Mint Repositories

It is recommended that for Linux Mint that you install from OpenSuSE Build Service using the Ubuntu Package Install [Instructions](ubuntu-package-install.md) | -| Linux Mint 21.x | [onedrive](https://community.linuxmint.com/software/view/onedrive) |Ubuntu 22.04 package |❌|✔|✔|✔| **Note:** Do not install from Linux Mint Repositories

It is recommended that for Linux Mint that you install from OpenSuSE Build Service using the Ubuntu Package Install [Instructions](ubuntu-package-install.md) | -| NixOS | [onedrive](https://search.nixos.org/packages?channel=20.09&from=0&size=50&sort=relevance&query=onedrive)|nixpkgs unstable package|❌|✔|❌|❌| Use package `onedrive` either by adding it to `configuration.nix` or by using the command `nix-env -iA .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 | [onedrive](https://software.opensuse.org/package/onedrive) |openSUSE Tumbleweed package|✔|✔|❌|❌| | -| OpenSuSE Build Service | [onedrive](https://build.opensuse.org/package/show/home:npreining:debian-ubuntu-onedrive/onedrive) | No API Available |✔|✔|✔|✔| Package Build Service for Debian and Ubuntu | -| Raspbian | [onedrive](https://archive.raspbian.org/raspbian/pool/main/o/onedrive/) |Raspbian Stable package |❌|❌|✔|✔| **Note:** Do not install from Raspbian Package Repositories

It is recommended that for Raspbian that you install from OpenSuSE Build Service using the Debian Package Install [Instructions](ubuntu-package-install.md) | -| Slackware | [onedrive](https://slackbuilds.org/result/?search=onedrive&sv=) |SlackBuilds package|✔|✔|❌|❌| | -| Solus | [onedrive](https://dev.getsol.us/search/query/FB7PIf1jG9Z9/#R) |Solus package|✔|✔|❌|❌| | -| Ubuntu 20.04 | [onedrive](https://packages.ubuntu.com/focal/onedrive) |Ubuntu 20.04 package |❌|✔|✔|✔| **Note:** Do not install from Ubuntu Universe

It is recommended that for Ubuntu that you install from OpenSuSE Build Service using the Ubuntu Package Install [Instructions](ubuntu-package-install.md) | -| Ubuntu 22.04 | [onedrive](https://packages.ubuntu.com/jammy/onedrive) |Ubuntu 22.04 package |❌|✔|✔|✔| **Note:** Do not install from Ubuntu Universe

It is recommended that for Ubuntu that you install from OpenSuSE Build Service using the Ubuntu Package Install [Instructions](ubuntu-package-install.md) | -| Ubuntu 23.04 | [onedrive](https://packages.ubuntu.com/lunar/onedrive) |Ubuntu 23.04 package |❌|✔|✔|✔| **Note:** Do not install from Ubuntu Universe

It is recommended that for Ubuntu that you install from OpenSuSE Build Service using the Ubuntu Package Install [Instructions](ubuntu-package-install.md) | -| Void Linux | [onedrive](https://voidlinux.org/packages/?arch=x86_64&q=onedrive) |Void Linux x86_64 package|✔|✔|❌|❌| | - -#### Important information for all Ubuntu and Ubuntu based distribution users: -This information is specifically for the following platforms and distributions: -* Ubuntu -* Lubuntu -* Linux Mint -* POP OS -* Peppermint OS - -Whilst there are [onedrive](https://packages.ubuntu.com/search?keywords=onedrive&searchon=names&suite=all§ion=all) Universe packages available for Ubuntu, do not install 'onedrive' from these Universe packages. The default Universe packages are out-of-date and are not supported and should not be used. If you wish to use a package, it is highly recommended that you utilise the [OpenSuSE Build Service](ubuntu-package-install.md) to install packages for these platforms. If the OpenSuSE Build Service does not cater for your version, your only option is to build from source. - -If you wish to change this situation so that you can just use the Universe packages via 'apt install onedrive', consider becoming the Ubuntu package maintainer and contribute back to your community. - -## Building from Source - High Level Requirements -* Build environment must have at least 1GB of memory & 1GB swap space -* Install the required distribution package dependencies -* [libcurl](http://curl.haxx.se/libcurl/) -* [SQLite 3](https://www.sqlite.org/) >= 3.7.15 -* [Digital Mars D Compiler (DMD)](http://dlang.org/download.html) or [LDC – the LLVM-based D Compiler](https://github.com/ldc-developers/ldc) - -**Note:** DMD version >= 2.088.0 or LDC version >= 1.18.0 is required to compile this application - -### Example for installing DMD Compiler -```text -curl -fsS https://dlang.org/install.sh | bash -s dmd -``` - -### Example for installing LDC Compiler -```text -curl -fsS https://dlang.org/install.sh | bash -s ldc -``` - -## Distribution Package Dependencies -### Dependencies: Ubuntu 16.x -Ubuntu Linux 16.x LTS reached the end of its five-year LTS window on April 30th 2021 and is no longer supported. - -### Dependencies: Ubuntu 18.x / Lubuntu 18.x -Ubuntu Linux 18.x LTS reached the end of its five-year LTS window on May 31th 2023 and is no longer supported. - -### Dependencies: Debian 9 -Debian 9 reached the end of its five-year support window on June 30th 2022 and is no longer supported. - -### Dependencies: Ubuntu 20.x -> Ubuntu 23.x / Debian 10 -> Debian 12 - x86_64 -These dependencies are also applicable for all Ubuntu based distributions such as: -* Lubuntu -* Linux Mint -* POP OS -* Peppermint OS -```text -sudo apt install build-essential -sudo apt install libcurl4-openssl-dev libsqlite3-dev pkg-config git curl -curl -fsS https://dlang.org/install.sh | bash -s dmd -``` -For notifications the following is also necessary: -```text -sudo apt install libnotify-dev -``` - -### Dependencies: CentOS 6.x / RHEL 6.x -CentOS 6.x and RHEL 6.x reached End of Life status on November 30th 2020 and is no longer supported. - -### Dependencies: Fedora < Version 18 / CentOS 7.x / RHEL 7.x -```text -sudo yum groupinstall 'Development Tools' -sudo yum install libcurl-devel sqlite-devel -curl -fsS https://dlang.org/install.sh | bash -s dmd-2.099.0 -``` -For notifications the following is also necessary: -```text -sudo yum install libnotify-devel -``` - -### Dependencies: Fedora > Version 18 / CentOS 8.x / RHEL 8.x / RHEL 9.x -```text -sudo dnf groupinstall 'Development Tools' -sudo dnf install libcurl-devel sqlite-devel -curl -fsS https://dlang.org/install.sh | bash -s dmd -``` -For notifications the following is also necessary: -```text -sudo dnf install libnotify-devel -``` - -### Dependencies: Arch Linux & Manjaro Linux -```text -sudo pacman -S make pkg-config curl sqlite ldc -``` -For notifications the following is also necessary: -```text -sudo pacman -S libnotify -``` - -### Dependencies: Raspbian (ARMHF) and Ubuntu 22.x / Debian 11 / Debian 12 / Raspbian (ARM64) -**Note:** The minimum LDC compiler version required to compile this application is now 1.18.0, which is not available for Debian Buster or distributions based on Debian Buster. You are advised to first upgrade your platform distribution to one that is based on Debian Bullseye (Debian 11) or later. - -These instructions were validated using: -* `Linux raspberrypi 5.10.92-v8+ #1514 SMP PREEMPT Mon Jan 17 17:39:38 GMT 2022 aarch64` (2022-01-28-raspios-bullseye-armhf-lite) using Raspberry Pi 3B (revision 1.2) -* `Linux raspberrypi 5.10.92-v8+ #1514 SMP PREEMPT Mon Jan 17 17:39:38 GMT 2022 aarch64` (2022-01-28-raspios-bullseye-arm64-lite) using Raspberry Pi 3B (revision 1.2) -* `Linux ubuntu 5.15.0-1005-raspi #5-Ubuntu SMP PREEMPT Mon Apr 4 12:21:48 UTC 2022 aarch64 aarch64 aarch64 GNU/Linux` (ubuntu-22.04-preinstalled-server-arm64+raspi) using Raspberry Pi 3B (revision 1.2) - -**Note:** Build environment must have at least 1GB of memory & 1GB swap space. Check with `swapon`. - -```text -sudo apt install build-essential -sudo apt install libcurl4-openssl-dev libsqlite3-dev pkg-config git curl ldc -``` -For notifications the following is also necessary: -```text -sudo apt install libnotify-dev -``` - -### Dependencies: Gentoo -```text -sudo emerge app-portage/layman -sudo layman -a dlang -``` -Add ebuild from contrib/gentoo to a local overlay to use. - -For notifications the following is also necessary: -```text -sudo emerge x11-libs/libnotify -``` - -### Dependencies: OpenSuSE Leap 15.0 -```text -sudo zypper addrepo https://download.opensuse.org/repositories/devel:languages:D/openSUSE_Leap_15.0/devel:languages:D.repo -sudo zypper refresh -sudo zypper install gcc git libcurl-devel sqlite3-devel dmd phobos-devel phobos-devel-static -``` -For notifications the following is also necessary: -```text -sudo zypper install libnotify-devel -``` - -### Dependencies: OpenSuSE Leap 15.1 -```text -sudo zypper addrepo https://download.opensuse.org/repositories/devel:languages:D/openSUSE_Leap_15.1/devel:languages:D.repo -sudo zypper refresh -sudo zypper install gcc git libcurl-devel sqlite3-devel dmd phobos-devel phobos-devel-static -``` -For notifications the following is also necessary: -```text -sudo zypper install libnotify-devel -``` - -### Dependencies: OpenSuSE Leap 15.2 -```text -sudo zypper refresh -sudo zypper install gcc git libcurl-devel sqlite3-devel dmd phobos-devel phobos-devel-static -``` -For notifications the following is also necessary: -```text -sudo zypper install libnotify-devel -``` - -## Compilation & Installation -### High Level Steps -1. Install the platform dependencies for your Linux OS -2. Activate your DMD or LDC compiler -3. Clone the GitHub repository, run configure and make, then install -4. Deactivate your DMD or LDC compiler - -### Building using DMD Reference Compiler -Before cloning and compiling, if you have installed DMD via curl for your OS, you will need to activate DMD as per example below: -```text -Run `source ~/dlang/dmd-2.088.0/activate` in your shell to use dmd-2.088.0. -This will setup PATH, LIBRARY_PATH, LD_LIBRARY_PATH, DMD, DC, and PS1. -Run `deactivate` later on to restore your environment. -``` -Without performing this step, the compilation process will fail. - -**Note:** Depending on your DMD version, substitute `2.088.0` above with your DMD version that is installed. - -```text -git clone https://github.com/abraunegg/onedrive.git -cd onedrive -./configure -make clean; make; -sudo make install -``` - -### Build options -Notifications can be enabled using the `configure` switch `--enable-notifications`. - -Systemd service files are installed in the appropriate directories on the system, -as provided by `pkg-config systemd` settings. If the need for overriding the -deduced path are necessary, the two options `--with-systemdsystemunitdir` (for -the Systemd system unit location), and `--with-systemduserunitdir` (for the -Systemd user unit location) can be specified. Passing in `no` to one of these -options disabled service file installation. - -By passing `--enable-debug` to the `configure` call, `onedrive` gets built with additional debug -information, useful (for example) to get `perf`-issued figures. - -By passing `--enable-completions` to the `configure` call, shell completion functions are -installed for `bash`, `zsh` and `fish`. The installation directories are determined -as far as possible automatically, but can be overridden by passing -`--with-bash-completion-dir=`, `--with-zsh-completion-dir=`, and -`--with-fish-completion-dir=` to `configure`. - -### Building using a different compiler (for example [LDC](https://wiki.dlang.org/LDC)) -#### ARMHF Architecture (Raspbian) and ARM64 Architecture (Ubuntu 22.x / Debian 11 / Raspbian) -**Note:** The minimum LDC compiler version required to compile this application is now 1.18.0, which is not available for Debian Buster or distributions based on Debian Buster. You are advised to first upgrade your platform distribution to one that is based on Debian Bullseye (Debian 11) or later. - -**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 -./configure DC=/usr/bin/ldmd2 -make clean; make -sudo make install -``` - -## Upgrading the client -If you have installed the client from a distribution package, the client will be updated when the distribution package is updated by the package maintainer and will be updated to the new application version when you perform your package update. - -If you have built the client from source, to upgrade your client, it is recommended that you first uninstall your existing 'onedrive' binary (see below), then re-install the client by re-cloning, re-compiling and re-installing the client again to install the new version. - -**Note:** Following the uninstall process will remove all client components including *all* systemd files, including any custom files created for specific access such as SharePoint Libraries. - -You can optionally choose to not perform this uninstallation step, and simply re-install the client by re-cloning, re-compiling and re-installing the client again - however the risk here is that you end up with two onedrive client binaries on your system, and depending on your system search path preferences, this will determine which binary is used. - -**Important:** Before performing any upgrade, it is highly recommended for you to stop any running systemd service if applicable to ensure that these services are restarted using the updated client version. - -Post re-install, to confirm that you have the new version of the client installed, use `onedrive --version` to determine the client version that is now installed. - -## Uninstalling the client -### Uninstalling the client if installed from distribution package -Follow your distribution documentation to uninstall the package that you installed - -### Uninstalling the client if installed and built from source -From within your GitHub repository clone, perform the following to remove the 'onedrive' binary: -```text -sudo make uninstall -``` - -If you are not upgrading your client, to remove your application state and configuration, perform the following additional step: -``` -rm -rf ~/.config/onedrive -``` -**Note:** If you are using the `--confdir option`, substitute `~/.config/onedrive` for the correct directory storing your client configuration. - -If you want to just delete the application key, but keep the items database: -```text -rm -f ~/.config/onedrive/refresh_token -``` diff --git a/docs/Podman.md b/docs/Podman.md deleted file mode 100644 index 35e69d26..00000000 --- a/docs/Podman.md +++ /dev/null @@ -1,360 +0,0 @@ -# Run the OneDrive Client for Linux under Podman -This client can be run as a Podman container, with 3 available container base options for you to choose from: - -| Container Base | Docker Tag | Description | i686 | x86_64 | ARMHF | AARCH64 | -|----------------|-------------|----------------------------------------------------------------|:------:|:------:|:-----:|:-------:| -| Alpine Linux | edge-alpine | Podman container based on Alpine 3.18 using 'master' |❌|✔|❌|✔| -| Alpine Linux | alpine | Podman container based on Alpine 3.18 using latest release |❌|✔|❌|✔| -| Debian | debian | Podman container based on Debian Stable using latest release |✔|✔|✔|✔| -| Debian | edge | Podman container based on Debian Stable using 'master' |✔|✔|✔|✔| -| Debian | edge-debian | Podman container based on Debian Stable using 'master' |✔|✔|✔|✔| -| Debian | latest | Podman container based on Debian Stable using latest release |✔|✔|✔|✔| -| Fedora | edge-fedora | Podman container based on Fedora 38 using 'master' |❌|✔|❌|✔| -| Fedora | fedora | Podman container based on Fedora 38 using latest release |❌|✔|❌|✔| - -These containers offer a simple monitoring-mode service for the OneDrive Client for Linux. - -The instructions below have been validated on: -* Fedora 38 - -The instructions below will utilise the 'edge' tag, however this can be substituted for any of the other docker tags such as 'latest' from the table above if desired. - -The 'edge' Docker Container will align closer to all documentation and features, where as 'latest' is the release version from a static point in time. The 'latest' tag however may contain bugs and/or issues that will have been fixed, and those fixes are contained in 'edge'. - -Additionally there are specific version release tags for each release. Refer to https://hub.docker.com/r/driveone/onedrive/tags for any other Docker tags you may be interested in. - -**Note:** The below instructions for podman has been tested and validated when logging into the system as an unprivileged user (non 'root' user). - -## High Level Configuration Steps -1. Install 'podman' as per your distribution platform's instructions if not already installed. -2. Disable 'SELinux' as per your distribution platform's instructions -3. Test 'podman' by running a test container -4. Prepare the required podman volumes to store the configuration and data -5. Run the 'onedrive' container and perform authorisation -6. Running the 'onedrive' container under 'podman' - -## Configuration Steps - -### 1. Install 'podman' on your platform -Install 'podman' as per your distribution platform's instructions if not already installed. - -### 2. Disable SELinux on your platform -In order to run the Docker container under 'podman', SELinux must be disabled. Without doing this, when the application is authenticated in the steps below, the following error will be presented: -```text -ERROR: The local file system returned an error with the following message: - Error Message: /onedrive/conf/refresh_token: Permission denied - -The database cannot be opened. Please check the permissions of ~/.config/onedrive/items.sqlite3 -``` -The only known work-around for the above problem at present is to disable SELinux. Please refer to your distribution platform's instructions on how to perform this step. - -* Fedora: https://docs.fedoraproject.org/en-US/quick-docs/selinux-changing-states-and-modes/#_disabling_selinux -* Red Hat Enterprise Linux: https://access.redhat.com/solutions/3176 - -Post disabling SELinux and reboot your system, confirm that `getenforce` returns `Disabled`: -```text -$ getenforce -Disabled -``` - -If you are still experiencing permission issues despite disabling SELinux, please read https://www.redhat.com/sysadmin/container-permission-denied-errors - -### 3. Test 'podman' on your platform -Test that 'podman' is operational for your 'non-root' user, as per below: -```bash -[alex@fedora38-podman ~]$ podman pull fedora -Resolved "fedora" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf) -Trying to pull registry.fedoraproject.org/fedora:latest... -Getting image source signatures -Copying blob b30887322388 done | -Copying config a1cd3cbf8a done | -Writing manifest to image destination -a1cd3cbf8adaa422629f2fcdc629fd9297138910a467b11c66e5ddb2c2753dff -[alex@fedora38-podman ~]$ podman run fedora /bin/echo "Welcome to the Podman World" -Welcome to the Podman World -[alex@fedora38-podman ~]$ -``` - -### 4. Configure the required podman volumes -The 'onedrive' Docker container requires 2 podman volumes to operate: -* Config Volume -* Data Volume - -The first volume is the configuration volume that stores all the applicable application configuration + current runtime state. In a non-containerised environment, this normally resides in `~/.config/onedrive` - in a containerised environment this is stored in the volume tagged as `/onedrive/conf` - -The second volume is the data volume, where all your data from Microsoft OneDrive is stored locally. This volume is mapped to an actual directory point on your local filesystem and this is stored in the volume tagged as `/onedrive/data` - -#### 4.1 Prepare the 'config' volume -Create the 'config' volume with the following command: -```bash -podman volume create onedrive_conf -``` - -This will create a podman volume labeled `onedrive_conf`, where all configuration of your onedrive account will be stored. You can add a custom config file in this location at a later point in time if required. - -#### 4.2 Prepare the 'data' volume -Create the 'data' volume with the following command: -```bash -podman volume create onedrive_data -``` - -This will create a podman volume labeled `onedrive_data` and will map to a path on your local filesystem. This is where your data from Microsoft OneDrive will be stored. Keep in mind that: - -* The owner of this specified folder must not be root -* Podman will attempt to change the permissions of the volume to the user the container is configured to run as - -**NOTE:** Issues occur when this target folder is a mounted folder of an external system (NAS, SMB mount, USB Drive etc) as the 'mount' itself is owed by 'root'. If this is your use case, you *must* ensure your normal user can mount your desired target without having the target mounted by 'root'. If you do not fix this, your Podman container will fail to start with the following error message: -```bash -ROOT level privileges prohibited! -``` - -### 5. First run of Docker container under podman and performing authorisation -The 'onedrive' client within the container first needs to be authorised with your Microsoft account. This is achieved by initially running podman in interactive mode. - -Run the podman image with the commands below and make sure to change the value of `ONEDRIVE_DATA_DIR` to the actual onedrive data directory on your filesystem that you wish to use (e.g. `export ONEDRIVE_DATA_DIR="/home/abraunegg/OneDrive"`). - -**Important:** The 'target' folder of `ONEDRIVE_DATA_DIR` must exist before running the podman container. The script below will create 'ONEDRIVE_DATA_DIR' so that it exists locally for the podman volume mapping to occur. - -It is also a requirement that the container be run using a non-root uid and gid, you must insert a non-root UID and GID (e.g.` export ONEDRIVE_UID=1000` and export `ONEDRIVE_GID=1000`). The script below will use `id` to evaluate your system environment to use the correct values. -```bash -export ONEDRIVE_DATA_DIR="${HOME}/OneDrive" -export ONEDRIVE_UID=`id -u` -export ONEDRIVE_GID=`id -g` -mkdir -p ${ONEDRIVE_DATA_DIR} -podman run -it --name onedrive --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" \ - -v onedrive_conf:/onedrive/conf:U,Z \ - -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" \ - driveone/onedrive:edge -``` - -**Important:** In some scenarios, 'podman' sets the configuration and data directories to a different UID & GID as specified. To resolve this situation, you must run 'podman' with the `--userns=keep-id` flag to ensure 'podman' uses the UID and GID as specified. The updated script example when using `--userns=keep-id` is below: - -```bash -export ONEDRIVE_DATA_DIR="${HOME}/OneDrive" -export ONEDRIVE_UID=`id -u` -export ONEDRIVE_GID=`id -g` -mkdir -p ${ONEDRIVE_DATA_DIR} -podman run -it --name onedrive --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" \ - --userns=keep-id \ - -v onedrive_conf:/onedrive/conf:U,Z \ - -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" \ - driveone/onedrive:edge -``` - - -**Important:** If you plan to use the 'podman' built in auto-updating of container images described in 'Systemd Service & Auto Updating' below, you must pass an additional argument to set a label during the first run. The updated script example to support auto-updating of container images is below: - -```bash -export ONEDRIVE_DATA_DIR="${HOME}/OneDrive" -export ONEDRIVE_UID=`id -u` -export ONEDRIVE_GID=`id -g` -mkdir -p ${ONEDRIVE_DATA_DIR} -podman run -it --name onedrive --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" \ - --userns=keep-id \ - -v onedrive_conf:/onedrive/conf:U,Z \ - -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" \ - -e PODMAN=1 \ - --label "io.containers.autoupdate=image" \ - driveone/onedrive:edge -``` - -When the Podman container successfully starts: -* You will be asked to open a specific link using your web browser -* Login to your Microsoft Account and give the application the permission -* After giving the permission, you will be redirected to a blank page -* Copy the URI of the blank page into the application prompt to authorise the application - -Once the 'onedrive' application is authorised, the client will automatically start monitoring your `ONEDRIVE_DATA_DIR` for data changes to be uploaded to OneDrive. Files stored on OneDrive will be downloaded to this location. - -If the client is working as expected, you can detach from the container with Ctrl+p, Ctrl+q. - -### 6. Running the 'onedrive' container under 'podman' - -#### 6.1 Check if the monitor service is running -```bash -podman ps -f name=onedrive -``` - -#### 6.2 Show 'onedrive' runtime logs -```bash -podman logs onedrive -``` - -#### 6.3 Stop running 'onedrive' container -```bash -podman stop onedrive -``` - -#### 6.4 Start 'onedrive' container -```bash -podman start onedrive -``` - -#### 6.5 Remove 'onedrive' container -```bash -podman rm -f onedrive -``` - - -## Advanced Usage - -### Systemd Service & Auto Updating - -Podman supports running containers as a systemd service and also auto updating of the container images. Using the existing running container you can generate a systemd unit file to be installed by the **root** user. To have your container image auto-update with podman, it must first be created with the label `"io.containers.autoupdate=image"` mentioned in step 5 above. - -``` -cd /tmp -podman generate systemd --new --restart-policy on-failure --name -f onedrive -/tmp/container-onedrive.service - -# copy the generated systemd unit file to the systemd path and reload the daemon - -cp -Z ~/container-onedrive.service /usr/lib/systemd/system -systemctl daemon-reload - -#optionally enable it to startup on boot - -systemctl enable container-onedrive.service - -#check status - -systemctl status container-onedrive - -#start/stop/restart container as a systemd service - -systemctl stop container-onedrive -systemctl start container-onedrive -``` - -To update the image using podman (Ad-hoc) -``` -podman auto-update -``` - -To update the image using systemd (Automatic/Scheduled) -``` -# Enable the podman-auto-update.timer service at system start: - -systemctl enable podman-auto-update.timer - -# Start the service - -systemctl start podman-auto-update.timer - -# Containers with the autoupdate label will be updated on the next scheduled timer - -systemctl list-timers --all -``` - -### Editing the running configuration and using a 'config' file -The 'onedrive' client should run in default configuration, however you can change this default configuration by placing a custom config file in the `onedrive_conf` podman volume. First download the default config from [here](https://raw.githubusercontent.com/abraunegg/onedrive/master/config) -Then put it into your onedrive_conf volume path, which can be found with: - -```bash -podman volume inspect onedrive_conf -``` -Or you can map your own config folder to the config volume. Make sure to copy all files from the volume into your mapped folder first. - -The detailed document for the config can be found here: [Configuration](https://github.com/abraunegg/onedrive/blob/master/docs/USAGE.md#configuration) - -### Syncing multiple accounts -There are many ways to do this, the easiest is probably to do the following: -1. Create a second podman config volume (replace `work` with your desired name): `podman volume create onedrive_conf_work` -2. And start a second podman monitor container (again replace `work` with your desired name): - -```bash -export ONEDRIVE_DATA_DIR_WORK="/home/abraunegg/OneDriveWork" -export ONEDRIVE_UID=`id -u` -export ONEDRIVE_GID=`id -g` -mkdir -p ${ONEDRIVE_DATA_DIR_WORK} -podman run -it --name onedrive_work --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" \ - --userns=keep-id \ - -v onedrive_conf_work:/onedrive/conf:U,Z \ - -v "${ONEDRIVE_DATA_DIR_WORK}:/onedrive/data:U,Z" \ - -e PODMAN=1 \ - --label "io.containers.autoupdate=image" \ - driveone/onedrive:edge -``` - -## Supported Podman Environment Variables -| Variable | Purpose | Sample Value | -| ---------------- | --------------------------------------------------- |:-------------:| -| ONEDRIVE_UID | UserID (UID) to run as | 1000 | -| ONEDRIVE_GID | GroupID (GID) to run as | 1000 | -| ONEDRIVE_VERBOSE | Controls "--verbose" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_DEBUG | Controls "--verbose --verbose" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_DEBUG_HTTPS | Controls "--debug-https" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_RESYNC | Controls "--resync" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_DOWNLOADONLY | Controls "--download-only" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_UPLOADONLY | Controls "--upload-only" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_NOREMOTEDELETE | Controls "--no-remote-delete" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_LOGOUT | Controls "--logout" switch. Default is 0 | 1 | -| ONEDRIVE_REAUTH | Controls "--reauth" switch. Default is 0 | 1 | -| ONEDRIVE_AUTHFILES | Controls "--auth-files" option. Default is "" | "authUrl:responseUrl" | -| ONEDRIVE_AUTHRESPONSE | Controls "--auth-response" option. Default is "" | See [here](https://github.com/abraunegg/onedrive/blob/master/docs/USAGE.md#authorize-the-application-with-your-onedrive-account) | -| ONEDRIVE_DISPLAY_CONFIG | Controls "--display-running-config" switch on onedrive sync. Default is 0 | 1 | -| ONEDRIVE_SINGLE_DIRECTORY | Controls "--single-directory" option. Default = "" | "mydir" | - -### Environment Variables Usage Examples -**Verbose Output:** -```bash -podman run -e ONEDRIVE_VERBOSE=1 -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" driveone/onedrive:edge -``` -**Debug Output:** -```bash -podman run -e ONEDRIVE_DEBUG=1 -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" driveone/onedrive:edge -``` -**Perform a --resync:** -```bash -podman run -e ONEDRIVE_RESYNC=1 -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" driveone/onedrive:edge -``` -**Perform a --resync and --verbose:** -```bash -podman run -e ONEDRIVE_RESYNC=1 -e ONEDRIVE_VERBOSE=1 -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" driveone/onedrive:edge -``` -**Perform a --logout and re-authenticate:** -```bash -podman run -it -e ONEDRIVE_LOGOUT=1 -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" driveone/onedrive:edge -``` - -## Building a custom Podman image -You can also build your own image instead of pulling the one from [hub.docker.com](https://hub.docker.com/r/driveone/onedrive): -```bash -git clone https://github.com/abraunegg/onedrive -cd onedrive -podman build . -t local-onedrive -f contrib/docker/Dockerfile -``` - -There are alternate, smaller images available by building -Dockerfile-debian or Dockerfile-alpine. These [multi-stage builder pattern](https://docs.docker.com/develop/develop-images/multistage-build/) -Dockerfiles require Docker version at least 17.05. - -### How to build and run a custom Podman image based on Debian -``` bash -podman build . -t local-ondrive-debian -f contrib/docker/Dockerfile-debian -podman run -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" --userns=keep-id local-ondrive-debian:latest -``` - -### How to build and run a custom Podman image based on Alpine Linux -``` bash -podman build . -t local-ondrive-alpine -f contrib/docker/Dockerfile-alpine -podman run -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" --userns=keep-id local-ondrive-alpine:latest -``` - -### How to build and run a custom Podman image for ARMHF (Raspberry Pi) -Compatible with: -* Raspberry Pi -* Raspberry Pi 2 -* Raspberry Pi Zero -* Raspberry Pi 3 -* Raspberry Pi 4 -``` bash -podman build . -t local-onedrive-armhf -f contrib/docker/Dockerfile-debian -podman run -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" --userns=keep-id local-onedrive-armhf:latest -``` - -### How to build and run a custom Podman image for AARCH64 Platforms -``` bash -podman build . -t local-onedrive-aarch64 -f contrib/docker/Dockerfile-debian -podman run -v onedrive_conf:/onedrive/conf:U,Z -v "${ONEDRIVE_DATA_DIR}:/onedrive/data:U,Z" --user "${ONEDRIVE_UID}:${ONEDRIVE_GID}" --userns=keep-id local-onedrive-aarch64:latest -``` diff --git a/docs/SharePoint-Shared-Libraries.md b/docs/SharePoint-Shared-Libraries.md deleted file mode 100644 index d1714d4e..00000000 --- a/docs/SharePoint-Shared-Libraries.md +++ /dev/null @@ -1,228 +0,0 @@ -# How to configure OneDrive SharePoint Shared Library sync -**WARNING:** Several users have reported files being overwritten causing data loss as a result of using this client with SharePoint Libraries when running as a systemd service. - -When this has been investigated, the following has been noted as potential root causes: -* File indexing application such as Baloo File Indexer or Tracker3 constantly indexing your OneDrive data -* The use of WPS Office and how it 'saves' files by deleting the existing item and replaces it with the saved data - -Additionally there could be a yet unknown bug with the client, however all debugging and data provided previously shows that an 'external' process to the 'onedrive' application modifies the files triggering the undesirable upload to occur. - -**Possible Preventative Actions:** -* Disable all File Indexing for your SharePoint Library data. It is out of scope to detail on how you should do this. -* Disable using a systemd service for syncing your SharePoint Library data. -* Do not use WPS Office to edit your documents. Use OpenOffice or LibreOffice as these do not exhibit the same 'delete to save' action that WPS Office has. - -Additionally, please use caution when using this client with SharePoint. - -## Application Version -Before reading this document, please ensure you are running application version [![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) or greater. Use `onedrive --version` to determine what application version you are using and upgrade your client if required. - -## Process Overview -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. Create a unique local folder which will be the SharePoint Library 'root' -4. Configure the client's config file with the required 'drive_id' -5. Test the configuration using '--dry-run' -6. Sync the SharePoint Library as required - -**Note:** The `--get-O365-drive-id` process below requires a fully configured 'onedrive' configuration so that the applicable Drive ID for the given Office 365 SharePoint Shared Library can be determined. It is highly recommended that you do not use the application 'default' configuration directory for any SharePoint Site, and configure separate items for each site you wish to use. - -## 1. Listing available OneDrive SharePoint Libraries -Login to the OneDrive web interface and determine which shared library you wish to configure the client for: -![shared_libraries](./images/SharedLibraries.jpg) - -## 2. Query OneDrive API to obtain required configuration details -Run the following command using the 'onedrive' client to query the OneDrive API to obtain the required 'drive_id' of the SharePoint Library that you wish to sync: -```text -onedrive --get-O365-drive-id '' -``` -This will return something similar to the following: -```text -Configuration file successfully loaded -Configuring Global Azure AD Endpoints -Initializing the Synchronization Engine ... -Office 365 Library Name Query: ------------------------------------------------ -Site Name: -Library Name: -drive_id: b!6H_y8B...xU5 -Library URL: ------------------------------------------------ -``` -If there are no matches to the site you are attempting to search, the following will be displayed: -```text -Configuration file successfully loaded -Configuring Global Azure AD Endpoints -Initializing the Synchronization Engine ... -Office 365 Library Name Query: blah - -ERROR: The requested SharePoint site could not be found. Please check it's name and your permissions to access the site. - -The following SharePoint site names were returned: - * - * - ... - * -``` -This list of site names can be used as a basis to search for the correct site for which you are searching - -## 3. Create a new configuration directory and sync location for this SharePoint Library -Create a new configuration directory for this SharePoint Library in the following manner: -```text -mkdir ~/.config/SharePoint_My_Library_Name -``` - -Create a new local folder to store the SharePoint Library data in: -```text -mkdir ~/SharePoint_My_Library_Name -``` - -**Note:** Do not use spaces in the directory name, use '_' as a replacement - -## 4. Configure SharePoint Library config file with the required 'drive_id' & 'sync_dir' options -Download 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/SharePoint_My_Library_Name/config -``` - -Update your 'onedrive' configuration file (`~/.config/SharePoint_My_Library_Name/config`) with the local folder where you will store your data: -```text -sync_dir = "~/SharePoint_My_Library_Name" -``` - -Update your 'onedrive' configuration file(`~/.config/SharePoint_My_Library_Name/config`) with the 'drive_id' value obtained in the steps above: -```text -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 and the location you have configured. - -**Note:** After changing `drive_id`, you must perform a full re-synchronization by adding `--resync` to your existing command line. - -## 5. Validate and Test the configuration -Validate your new configuration using the `--display-config` option to validate you have configured the application correctly: -```text -onedrive --confdir="~/.config/SharePoint_My_Library_Name" --display-config -``` - -Test your new configuration using the `--dry-run` option to validate the application configuration: -```text -onedrive --confdir="~/.config/SharePoint_My_Library_Name" --synchronize --verbose --dry-run -``` - -**Note:** As this is a *new* configuration, the application will be required to be re-authorised the first time this command is run with the new configuration. - -## 6. Sync the SharePoint Library as required -Sync the SharePoint Library to your system with either `--synchronize` or `--monitor` operations: -```text -onedrive --confdir="~/.config/SharePoint_My_Library_Name" --synchronize --verbose -``` - -```text -onedrive --confdir="~/.config/SharePoint_My_Library_Name" --monitor --verbose -``` - -**Note:** As this is a *new* configuration, the application will be required to be re-authorised the first time this command is run with the new configuration. - -## 7. Enable custom systemd service for SharePoint Library -Systemd can be used to automatically run this configuration in the background, however, a unique systemd service will need to be setup for this SharePoint Library instance - -In order to automatically start syncing each SharePoint Library, you will need to create a service file for each SharePoint Library. 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` - -### Step1: Create a new systemd service file -#### Red Hat Enterprise Linux, CentOS Linux -Copy the required service file to a new name: -```text -sudo cp /usr/lib/systemd/system/onedrive.service /usr/lib/systemd/system/onedrive-SharePoint_My_Library_Name.service -``` -or -```text -sudo cp /usr/lib/systemd/system/onedrive@.service /usr/lib/systemd/system/onedrive-SharePoint_My_Library_Name@.service -``` - -#### Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora -Copy the required service file to a new name: -```text -sudo cp /usr/lib/systemd/user/onedrive.service /usr/lib/systemd/user/onedrive-SharePoint_My_Library_Name.service -``` -or -```text -sudo cp /lib/systemd/system/onedrive@.service /lib/systemd/system/onedrive-SharePoint_My_Library_Name@.service -``` - -### Step 2: Edit new systemd service file -Edit the new systemd file, updating 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/SharePoint_My_Library_Name" -``` - -**Note:** When running the client manually, `--confdir="~/.config/......` is acceptable. In a systemd configuration file, the full path must be used. The `~` must be expanded. - -### Step 3: Enable the new systemd service -Once the file is correctly editied, you can enable the new systemd service using the following commands. - -#### Red Hat Enterprise Linux, CentOS Linux -```text -systemctl enable onedrive-SharePoint_My_Library_Name -systemctl start onedrive-SharePoint_My_Library_Name -``` - -#### Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora -```text -systemctl --user enable onedrive-SharePoint_My_Library_Name -systemctl --user start onedrive-SharePoint_My_Library_Name -``` -or -```text -systemctl --user enable onedrive-SharePoint_My_Library_Name@myusername.service -systemctl --user start onedrive-SharePoint_My_Library_Name@myusername.service -``` - -### Step 4: Viewing systemd status and logs for the custom service -#### Viewing systemd service status - Red Hat Enterprise Linux, CentOS Linux -```text -systemctl status onedrive-SharePoint_My_Library_Name -``` - -#### Viewing systemd service status - Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora -```text -systemctl --user status onedrive-SharePoint_My_Library_Name -``` - -#### Viewing journalctl systemd logs - Red Hat Enterprise Linux, CentOS Linux -```text -journalctl --unit=onedrive-SharePoint_My_Library_Name -f -``` - -#### Viewing journalctl systemd logs - Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora -```text -journalctl --user --unit=onedrive-SharePoint_My_Library_Name -f -``` - -### Step 5: (Optional) Run custom systemd service at boot without user login -In some cases it may be desirable for the systemd service to start without having to login as your 'user' - -All the systemd steps above that utilise the `--user` option, will run the systemd service as your particular user. As such, the systemd service will not start unless you actually login to your system. - -To avoid this issue, you need to reconfigure your 'user' account so that the systemd services you have created will startup without you having to login to your system: -```text -loginctl enable-linger -``` - -Example: -```text -alex@ubuntu-headless:~$ loginctl enable-linger alex -``` - -## 8. Configuration for a SharePoint Library is complete -The 'onedrive' client configuration for this particular SharePoint Library is now complete. - -# How to configure multiple OneDrive SharePoint Shared Library sync -Create a new configuration as per the process above. Repeat these steps for each SharePoint Library that you wish to use. diff --git a/docs/USAGE.md b/docs/USAGE.md deleted file mode 100644 index 235b15d3..00000000 --- a/docs/USAGE.md +++ /dev/null @@ -1,1469 +0,0 @@ -# Configuration and Usage of the OneDrive Free Client -## Application Version -Before reading this document, please ensure you are running application version [![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) or greater. Use `onedrive --version` to determine what application version you are using and upgrade your client if required. - -## Table of Contents -- [Using the client](#using-the-client) - * [Upgrading from 'skilion' client](#upgrading-from-skilion-client) - * [Local File and Folder Naming Conventions](#local-file-and-folder-naming-conventions) - * [curl compatibility](#curl-compatibility) - * [Authorize the application with your OneDrive Account](#authorize-the-application-with-your-onedrive-account) - * [Show your configuration](#show-your-configuration) - * [Testing your configuration](#testing-your-configuration) - * [Performing a sync](#performing-a-sync) - * [Performing a single directory sync](#performing-a-single-directory-sync) - * [Performing a 'one-way' download sync](#performing-a-one-way-download-sync) - * [Performing a 'one-way' upload sync](#performing-a-one-way-upload-sync) - * [Performing a selective sync via 'sync_list' file](#performing-a-selective-sync-via-sync_list-file) - * [Performing a --resync](#performing-a---resync) - * [Performing a --force-sync without a --resync or changing your configuration](#performing-a---force-sync-without-a---resync-or-changing-your-configuration) - * [Increasing logging level](#increasing-logging-level) - * [Client Activity Log](#client-activity-log) - * [Notifications](#notifications) - * [Handling a OneDrive account password change](#handling-a-onedrive-account-password-change) -- [Configuration](#configuration) - * [The default configuration](#the-default-configuration-file-is-listed-below) - * ['config' file configuration examples](#config-file-configuration-examples) - + [sync_dir](#sync_dir) - + [sync_dir directory and file permissions](#sync_dir-directory-and-file-permissions) - + [skip_dir](#skip_dir) - + [skip_file](#skip_file) - + [skip_dotfiles](#skip_dotfiles) - + [monitor_interval](#monitor_interval) - + [monitor_fullscan_frequency](#monitor_fullscan_frequency) - + [monitor_log_frequency](#monitor_log_frequency) - + [min_notify_changes](#min_notify_changes) - + [operation_timeout](#operation_timeout) - + [ip_protocol_version](#ip_protocol_version) - + [classify_as_big_delete](#classify_as_big_delete) - * [Configuring the client for 'single tenant application' use](#configuring-the-client-for-single-tenant-application-use) - * [Configuring the client to use older 'skilion' application identifier](#configuring-the-client-to-use-older-skilion-application-identifier) -- [Frequently Asked Configuration Questions](#frequently-asked-configuration-questions) - * [How to sync only specific or single directory?](#how-to-sync-only-specific-or-single-directory) - * [How to 'skip' directories from syncing?](#how-to-skip-directories-from-syncing) - * [How to 'skip' files from syncing?](#how-to-skip-files-from-syncing) - * [How to 'skip' dot files and folders from syncing?](#how-to-skip-dot-files-and-folders-from-syncing) - * [How to 'skip' files larger than a certain size from syncing?](#how-to-skip-files-larger-than-a-certain-size-from-syncing) - * [How to 'rate limit' the application to control bandwidth consumed for upload & download operations?](#how-to-rate-limit-the-application-to-control-bandwidth-consumed-for-upload--download-operations) - * [How to prevent your local disk from filling up?](#how-to-prevent-your-local-disk-from-filling-up) - * [How are symbolic links handled by the client?](#how-are-symbolic-links-handled-by-the-client) - * [How to sync shared folders (OneDrive Personal)?](#how-to-sync-shared-folders-onedrive-personal) - * [How to sync shared folders (OneDrive Business or Office 365)?](#how-to-sync-shared-folders-onedrive-business-or-office-365) - * [How to sync sharePoint / Office 365 Shared Libraries?](#how-to-sync-sharepoint--office-365-shared-libraries) - * [How to run a user systemd service at boot without user login?](#how-to-run-a-user-systemd-service-at-boot-without-user-login) - * [How to create a shareable link?](#how-to-create-a-shareable-link) - * [How to sync both Personal and Business accounts at the same time?](#how-to-sync-both-personal-and-business-accounts-at-the-same-time) - * [How to sync multiple SharePoint Libraries at the same time?](#how-to-sync-multiple-sharepoint-libraries-at-the-same-time) -- [Running 'onedrive' in 'monitor' mode](#running-onedrive-in-monitor-mode) - * [Use webhook to subscribe to remote updates in 'monitor' mode](#use-webhook-to-subscribe-to-remote-updates-in-monitor-mode) - * [More webhook configuration options](#more-webhook-configuration-options) - + [webhook_listening_host and webhook_listening_port](#webhook_listening_host-and-webhook_listening_port) - + [webhook_expiration_interval and webhook_renewal_interval](#webhook_expiration_interval-and-webhook_renewal_interval) -- [Running 'onedrive' as a system service](#running-onedrive-as-a-system-service) - * [OneDrive service running as root user via init.d](#onedrive-service-running-as-root-user-via-initd) - * [OneDrive service running as root user via systemd (Arch, Ubuntu, Debian, OpenSuSE, Fedora)](#onedrive-service-running-as-root-user-via-systemd-arch-ubuntu-debian-opensuse-fedora) - * [OneDrive service running as root user via systemd (Red Hat Enterprise Linux, CentOS Linux)](#onedrive-service-running-as-root-user-via-systemd-red-hat-enterprise-linux-centos-linux) - * [OneDrive service running as a non-root user via systemd (All Linux Distributions)](#onedrive-service-running-as-a-non-root-user-via-systemd-all-linux-distributions) - * [OneDrive service running as a non-root user via systemd (with notifications enabled) (Arch, Ubuntu, Debian, OpenSuSE, Fedora)](#onedrive-service-running-as-a-non-root-user-via-systemd-with-notifications-enabled-arch-ubuntu-debian-opensuse-fedora) - * [OneDrive service running as a non-root user via runit (antiX, Devuan, Artix, Void)](#onedrive-service-running-as-a-non-root-user-via-runit-antix-devuan-artix-void) -- [Additional Configuration](#additional-configuration) - * [Advanced Configuration of the OneDrive Free Client](#advanced-configuration-of-the-onedrive-free-client) - * [Access OneDrive service through a proxy](#access-onedrive-service-through-a-proxy) - * [Setup selinux for a sync folder outside of the home folder](#setup-selinux-for-a-sync-folder-outside-of-the-home-folder) -- [All available commands](#all-available-commands) - -## Using the client -### Upgrading from 'skilion' client -The 'skilion' version contains a significant number of defects in how the local sync state is managed. When upgrading from the 'skilion' version to this version, it is advisable to stop any service / onedrive process from running and then remove any `items.sqlite3` file from your configuration directory (`~/.config/onedrive/`) as this will force the creation of a new local cache file. - -Additionally, if you are using a 'config' file within your configuration directory (`~/.config/onedrive/`), please ensure that you update the `skip_file = ` option as per below: - -**Invalid configuration:** -```text -skip_file = ".*|~*" -``` -**Minimum valid configuration:** -```text -skip_file = "~*" -``` -**Default valid configuration:** -```text -skip_file = "~*|.~*|*.tmp" -``` - -Do not use a skip_file entry of `.*` as this will prevent correct searching of local changes to process. - -### Local File and Folder Naming Conventions -The files and directories in the synchronization directory must follow the [Windows naming conventions](https://docs.microsoft.com/windows/win32/fileio/naming-a-file). -The application will attempt to handle instances where you have two files with the same names but with different capitalization. Where there is a namespace clash, the file name which clashes will not be synced. This is expected behavior and won't be fixed. - -### curl compatibility -If your system utilises curl < 7.47.0, curl defaults to HTTP/1.1 for HTTPS operations. The client will use HTTP/1.1. - -If your system utilises curl >= 7.47.0 and < 7.62.0, curl will prefer HTTP/2 for HTTPS but will stick to HTTP/1.1 by default. The client will use HTTP/1.1 for HTTPS operations. - -If your system utilises curl >= 7.62.0, curl defaults to prefer HTTP/2 over HTTP/1.1 by default. The client will utilse HTTP/2 for most HTTPS operations and HTTP/1.1 for others. This difference is governed by the OneDrive platform and not this client. - -If you wish to explicitly use HTTP/1.1 you will need to use the `--force-http-11` flag or set the config option `force_http_11 = "true"` to force the application to use HTTP/1.1 otherwise all client operations will use whatever is the curl default for your distribution. - -### Authorize the application with your OneDrive Account -After installing the application you must authorize the application with your OneDrive Account. This is done by running the application without any additional command switches. - -Note that some companies require to explicitly add this app in [Microsoft MyApps portal](https://myapps.microsoft.com/). To add an (approved) app to your apps, click on the ellipsis in the top-right corner and choose "Request new apps". On the next page you can add this app. If its not listed, you should request through your IT department. - -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 - -Authorize this app visiting: - -https://..... - -Enter the response uri: - -``` - -**Example:** -``` -[user@hostname ~]$ onedrive -Authorize this app visiting: - -https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=22c49a0d-d21c-4792-aed1-8f163c982546&scope=Files.ReadWrite%20Files.ReadWrite.all%20Sites.ReadWrite.All%20offline_access&response_type=code&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient - -Enter the response uri: https://login.microsoftonline.com/common/oauth2/nativeclient?code= - -Application has been successfully authorised, however no additional command switches were provided. - -Please use 'onedrive --help' for further assistance in regards to running this application. -``` - -### Show your configuration -To validate your configuration the application will use, utilize the following: -```text -onedrive --display-config -``` -This will display all the pertinent runtime interpretation of the options and configuration you are using. Example output is as follows: -```text -Configuration file successfully loaded -onedrive version = vX.Y.Z-A-bcdefghi -Config path = /home/alex/.config/onedrive -Config file found in config path = true -Config option 'sync_dir' = /home/alex/OneDrive -Config option 'enable_logging' = false -... -Selective sync 'sync_list' configured = false -Config option 'sync_business_shared_folders' = false -Business Shared Folders configured = false -Config option 'webhook_enabled' = false -``` - -### Testing your configuration -You are able to test your configuration by utilising the `--dry-run` CLI option. No files will be downloaded, uploaded or removed, however the application will display what 'would' have occurred. For example: -```text -onedrive --synchronize --verbose --dry-run -DRY-RUN Configured. Output below shows what 'would' have occurred. -Loading config ... -Using Config Dir: /home/user/.config/onedrive -Initializing the OneDrive API ... -Opening the item database ... -All operations will be performed in: /home/user/OneDrive -Initializing the Synchronization Engine ... -Account Type: personal -Default Drive ID: -Default Root ID: -Remaining Free Space: 5368709120 -Fetching details for OneDrive Root -OneDrive Root exists in the database -Syncing changes from OneDrive ... -Applying changes of Path ID: -Uploading differences of . -Processing root -The directory has not changed -Uploading new items of . -OneDrive Client requested to create remote path: ./newdir -The requested directory to create was not found on OneDrive - creating remote directory: ./newdir -Successfully created the remote directory ./newdir on OneDrive -Uploading new file ./newdir/newfile.txt ... done. -Remaining free space: 5368709076 -Applying changes of Path ID: -``` - -**Note:** `--dry-run` can only be used with `--synchronize`. It cannot be used with `--monitor` and will be ignored. - -### Performing a sync -By default all files are downloaded in `~/OneDrive`. After authorizing the application, a sync of your data can be performed by running: -```text -onedrive --synchronize -``` -This will synchronize files from your OneDrive account to your `~/OneDrive` local directory. - -If you prefer to use your local files as stored in `~/OneDrive` as the 'source of truth' use the following sync command: -```text -onedrive --synchronize --local-first -``` - -### Performing a single directory sync -In some cases it may be desirable to sync a single directory under ~/OneDrive without having to change your client configuration. To do this use the following command: -```text -onedrive --synchronize --single-directory '' -``` - -Example: If the full path is `~/OneDrive/mydir`, the command would be `onedrive --synchronize --single-directory 'mydir'` - -### Performing a 'one-way' download sync -In some cases it may be desirable to 'download only' from OneDrive. To do this use the following command: -```text -onedrive --synchronize --download-only -``` - -### Performing a 'one-way' upload sync -In some cases it may be desirable to 'upload only' to OneDrive. To do this use the following command: -```text -onedrive --synchronize --upload-only -``` -**Note:** If a file or folder is present on OneDrive, that was previously synced and now does not exist locally, that item it will be removed from OneDrive. If the data on OneDrive should be kept, the following should be used: -```text -onedrive --synchronize --upload-only --no-remote-delete -``` -**Note:** The operation of 'upload only' does not request data from OneDrive about what 'other' data exists online. The client only knows about the data that 'this' client uploaded, thus any files or folders created or uploaded outside of this client will remain untouched online. - -### Performing a selective sync via 'sync_list' file -Selective sync allows you to sync only specific files and directories. -To enable selective sync create a file named `sync_list` in your application configuration directory (default is `~/.config/onedrive`). - -Important points to understand before using 'sync_list'. -* 'sync_list' excludes _everything_ by default on onedrive. -* 'sync_list' follows an _"exclude overrides include"_ rule, and requires **explicit inclusion**. -* Order exclusions before inclusions, so that anything _specifically included_ is included. -* How and where you place your `/` matters for excludes and includes in sub directories. - -Each line of the file represents a relative path from your `sync_dir`. All files and directories not matching any line of the file will be skipped during all operations. - -Additionally, the use of `/` is critically important to determine how a rule is interpreted. It is very similar to `**` wildcards, for those that are familiar with globbing patterns. -Here is an example of `sync_list`: -```text -# sync_list supports comments -# -# The ordering of entries is highly recommended - exclusions before inclusions -# -# Exclude temp folder(s) or file(s) under Documents folder(s), anywhere in Onedrive -!Documents/temp* -# -# Exclude secret data folder in root directory only -!/Secret_data/* -# -# Include everything else in root directory -/* -# -# Include my Backup folder(s) or file(s) anywhere on Onedrive -Backup -# -# Include my Backup folder in root -/Backup/ -# -# Include Documents folder(s) anywhere in Onedrive -Documents/ -# -# Include all PDF files in Documents folder(s), anywhere in Onedrive -Documents/*.pdf -# -# Include this single document in Documents folder(s), anywhere in Onedrive -Documents/latest_report.docx -# -# Include all Work/Project directories or files, inside 'Work' folder(s), anywhere in Onedrive -Work/Project* -# -# Include all "notes.txt" files, anywhere in Onedrive -notes.txt -# -# Include /Blender in the ~Onedrive root but not if elsewhere in Onedrive -/Blender -# -# Include these directories(or files) in 'Pictures' folder(s), that have a space in their name -Pictures/Camera Roll -Pictures/Saved Pictures -# -# Include these names if they match any file or folder -Cinema Soc -Codes -Textbooks -Year 2 -``` -The following are supported for pattern matching and exclusion rules: -* Use the `*` to wildcard select any characters to match for the item to be included -* Use either `!` or `-` characters at the start of the line to exclude an otherwise included item - - -**Note:** When enabling the use of 'sync_list' utilise the `--display-config` option to validate that your configuration will be used by the application, and test your configuration by adding `--dry-run` to ensure the client will operate as per your requirement. - -**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` - -**Note:** In some circumstances, it may be required to sync all the individual files within the 'sync_dir', but due to frequent name change / addition / deletion of these files, it is not desirable to constantly change the 'sync_list' file to include / exclude these files and force a resync. To assist with this, enable the following in your configuration file: -```text -sync_root_files = "true" -``` -This will tell the application to sync any file that it finds in your 'sync_dir' root by default. - -### Performing a --resync -If you modify any of the following configuration items, you will be required to perform a `--resync` to ensure your client is syncing your data with the updated configuration: -* sync_dir -* skip_dir -* skip_file -* drive_id -* Modifying sync_list -* Modifying business_shared_folders - -Additionally, you may choose to perform a `--resync` if you feel that this action needs to be taken to ensure your data is in sync. If you are using this switch simply because you dont know the sync status, you can query the actual sync status using `--display-sync-status`. - -When using `--resync`, the following warning and advice will be presented: -```text -The use of --resync will remove your local 'onedrive' client state, thus no record will exist regarding your current 'sync status' -This has the potential to overwrite local versions of files with potentially older versions downloaded from OneDrive which can lead to data loss -If in-doubt, backup your local data first before proceeding with --resync - -Are you sure you wish to proceed with --resync? [Y/N] -``` - -To proceed with using `--resync`, you must type 'y' or 'Y' to allow the application to continue. - -**Note:** It is highly recommended to only use `--resync` if the application advises you to use it. Do not just blindly set the application to start with `--resync` as the default option. - -**Note:** In some automated environments (and it is 100% assumed you *know* what you are doing because of automation), in order to avoid this 'proceed with acknowledgement' requirement, add `--resync-auth` to automatically acknowledge the prompt. - -### Performing a --force-sync without a --resync or changing your configuration -In some cases and situations, you may have configured the application to skip certain files and folders using 'skip_file' and 'skip_dir' configuration. You then may have a requirement to actually sync one of these items, but do not wish to modify your configuration, nor perform an entire `--resync` twice. - -The `--force-sync` option allows you to sync a specific directory, ignoring your 'skip_file' and 'skip_dir' configuration and negating the requirement to perform a `--resync` - -In order to use this option, you must run the application manually in the following manner: -```text -onedrive --synchronize --single-directory '' --force-sync -``` - -When using `--force-sync`, the following warning and advice will be presented: -```text -WARNING: Overriding application configuration to use application defaults for skip_dir and skip_file due to --synchronize --single-directory --force-sync being used - -The use of --force-sync will reconfigure the application to use defaults. This may have untold and unknown future impacts. -By proceeding in using this option you accept any impacts including any data loss that may occur as a result of using --force-sync. - -Are you sure you wish to proceed with --force-sync [Y/N] -``` - -To proceed with using `--force-sync`, you must type 'y' or 'Y' to allow the application to continue. - -### Increasing logging level -When running a sync it may be desirable to see additional information as to the progress and operation of the client. To do this, use the following command: -```text -onedrive --synchronize --verbose -``` - -### Client Activity Log -When running onedrive all actions can be logged to a separate log file. This can be enabled by using the `--enable-logging` flag. By default, log files will be written to `/var/log/onedrive/` - -**Note:** You will need to ensure the existence of this directory, and that your user has the applicable permissions to write to this directory or the following warning will be printed: -```text -Unable to access /var/log/onedrive/ -Please manually create '/var/log/onedrive/' and set appropriate permissions to allow write access -The requested client activity log will instead be located in the users home directory -``` - -On many systems this can be achieved by -```text -sudo mkdir /var/log/onedrive -sudo chown root:users /var/log/onedrive -sudo chmod 0775 /var/log/onedrive -``` - -All log files will be in the format of `%username%.onedrive.log`, where `%username%` represents the user who ran the client. - -Additionally, you need to ensure that your user account is part of the 'users' group: -``` -cat /etc/group | grep users -``` - -If your user is not part of this group, then you need to add your user to this group: -``` -sudo usermod -a -G users -``` - -You then need to 'logout' of all sessions / SSH sessions to login again to have the new group access applied. - - -**Note:** -To use a different log directory rather than the default above, add the following as a configuration option to `~/.config/onedrive/config`: -```text -log_dir = "/path/to/location/" -``` -Trailing slash required - -An example of the log file is below: -```text -2018-Apr-07 17:09:32.1162837 Loading config ... -2018-Apr-07 17:09:32.1167908 No config file found, using defaults -2018-Apr-07 17:09:32.1170626 Initializing the OneDrive API ... -2018-Apr-07 17:09:32.5359143 Opening the item database ... -2018-Apr-07 17:09:32.5515295 All operations will be performed in: /root/OneDrive -2018-Apr-07 17:09:32.5518387 Initializing the Synchronization Engine ... -2018-Apr-07 17:09:36.6701351 Applying changes of Path ID: -2018-Apr-07 17:09:37.4434282 Adding OneDrive Root to the local database -2018-Apr-07 17:09:37.4478342 The item is already present -2018-Apr-07 17:09:37.4513752 The item is already present -2018-Apr-07 17:09:37.4550062 The item is already present -2018-Apr-07 17:09:37.4586444 The item is already present -2018-Apr-07 17:09:37.7663571 Adding OneDrive Root to the local database -2018-Apr-07 17:09:37.7739451 Fetching details for OneDrive Root -2018-Apr-07 17:09:38.0211861 OneDrive Root exists in the database -2018-Apr-07 17:09:38.0215375 Uploading differences of . -2018-Apr-07 17:09:38.0220464 Processing -2018-Apr-07 17:09:38.0224884 The directory has not changed -2018-Apr-07 17:09:38.0229369 Processing -2018-Apr-07 17:09:38.02338 The directory has not changed -2018-Apr-07 17:09:38.0237678 Processing -2018-Apr-07 17:09:38.0242285 The directory has not changed -2018-Apr-07 17:09:38.0245977 Processing -2018-Apr-07 17:09:38.0250788 The directory has not changed -2018-Apr-07 17:09:38.0254657 Processing -2018-Apr-07 17:09:38.0259923 The directory has not changed -2018-Apr-07 17:09:38.0263547 Uploading new items of . -2018-Apr-07 17:09:38.5708652 Applying changes of Path ID: -``` - -### Notifications -If notification support is compiled in, the following events will trigger a notification within the display manager session: -* Aborting a sync if .nosync file is found -* Cannot create remote directory -* Cannot upload file changes -* Cannot delete remote file / folder -* Cannot move remote file / folder - - -### Handling a OneDrive account password change -If you change your OneDrive account password, the client will no longer be authorised to sync, and will generate the following error: -```text -ERROR: OneDrive returned a 'HTTP 401 Unauthorized' - Cannot Initialize Sync Engine -``` -To re-authorise the client, follow the steps below: -1. If running the client as a service (init.d or systemd), stop the service -2. Run the command `onedrive --reauth`. This will clean up the previous authorisation, and will prompt you to re-authorise the client as per initial configuration. -3. Restart the client if running as a service or perform a manual sync - -The application will now sync with OneDrive with the new credentials. - -## Configuration - -Configuration is determined by three layers: the default values, values set in the configuration file, and values passed in via the command line. The default values provide a reasonable default, and configuration is optional. - -Most command line options have a respective configuration file setting. - -If you want to change the defaults, you can copy and edit the included config file into your configuration directory. Valid default directories for the config file are: -* `~/.config/onedrive` -* `/etc/onedrive` - -**Example:** -```text -mkdir -p ~/.config/onedrive -wget https://raw.githubusercontent.com/abraunegg/onedrive/master/config -O ~/.config/onedrive/config -nano ~/.config/onedrive/config -``` -This file does not get created by default, and should only be created if you want to change the 'default' operational parameters. - -See the [config](https://raw.githubusercontent.com/abraunegg/onedrive/master/config) file for the full list of options, and [All available commands](https://github.com/abraunegg/onedrive/blob/master/docs/USAGE.md#all-available-commands) for all possible keys and their default values. - -**Note:** The location of the application configuration information can also be specified by using the `--confdir` configuration option which can be passed in at client run-time. - -### The default configuration file is listed below: -```text -# Configuration for OneDrive Linux Client -# This file contains the list of supported configuration fields -# with their default values. -# All values need to be enclosed in quotes -# 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_dir = "~/OneDrive" -# skip_file = "~*|.~*|*.tmp" -# monitor_interval = "300" -# skip_dir = "" -# log_dir = "/var/log/onedrive/" -# drive_id = "" -# upload_only = "false" -# check_nomount = "false" -# check_nosync = "false" -# download_only = "false" -# disable_notifications = "false" -# disable_upload_validation = "false" -# enable_logging = "false" -# force_http_11 = "false" -# local_first = "false" -# no_remote_delete = "false" -# skip_symlinks = "false" -# debug_https = "false" -# skip_dotfiles = "false" -# skip_size = "1000" -# dry_run = "false" -# min_notify_changes = "5" -# monitor_log_frequency = "6" -# monitor_fullscan_frequency = "12" -# sync_root_files = "false" -# classify_as_big_delete = "1000" -# user_agent = "" -# remove_source_files = "false" -# skip_dir_strict_match = "false" -# application_id = "" -# resync = "false" -# resync_auth = "false" -# bypass_data_preservation = "false" -# azure_ad_endpoint = "" -# azure_tenant_id = "common" -# sync_business_shared_folders = "false" -# sync_dir_permissions = "700" -# sync_file_permissions = "600" -# rate_limit = "131072" -# webhook_enabled = "false" -# webhook_public_url = "" -# webhook_listening_host = "" -# webhook_listening_port = "8888" -# webhook_expiration_interval = "86400" -# webhook_renewal_interval = "43200" -# space_reservation = "50" -# display_running_config = "false" -# read_only_auth_scope = "false" -# cleanup_local_files = "false" -# operation_timeout = "3600" -# dns_timeout = "60" -# connect_timeout = "10" -# data_timeout = "600" -# ip_protocol_version = "0" -``` - -### 'config' file configuration examples: -The below are 'config' file examples to assist with configuration of the 'config' file: - -#### sync_dir -Configure your local sync directory location. - -Example: -```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_dir="~/MyDirToSync" -# skip_file = "~*|.~*|*.tmp" -# monitor_interval = "300" -# skip_dir = "" -# log_dir = "/var/log/onedrive/" -``` -**Please Note:** -Proceed with caution here when changing the default sync dir from `~/OneDrive` to `~/MyDirToSync` - -The issue here is around how the client stores the sync_dir path in the database. If the config file is missing, or you don't use the `--syncdir` parameter - what will happen is the client will default back to `~/OneDrive` and 'think' that either all your data has been deleted - thus delete the content on OneDrive, or will start downloading all data from OneDrive into the default location. - -**Note:** After changing `sync_dir`, you must perform a full re-synchronization by adding `--resync` to your existing command line - for example: `onedrive --synchronize --resync` - -**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 the [Unix Permissions Calculator](https://chmod-calculator.com/) 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 -This option is used to 'skip' certain directories and supports pattern matching. - -Patterns are case insensitive. `*` and `?` [wildcards characters](https://technet.microsoft.com/en-us/library/bb490639.aspx) are supported. Use `|` to separate multiple patterns. - -**Important:** Entries under `skip_dir` are relative to your `sync_dir` path. - -Example: -```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_dir = "~/OneDrive" -# skip_file = "~*|.~*|*.tmp" -# monitor_interval = "300" -skip_dir = "Desktop|Documents/IISExpress|Documents/SQL Server Management Studio|Documents/Visual Studio*|Documents/WindowsPowerShell" -# log_dir = "/var/log/onedrive/" -``` - -**Note:** The `skip_dir` can be specified multiple times, for example: -```text -skip_dir = "SomeDir|OtherDir|ThisDir|ThatDir" -skip_dir = "/Path/To/A/Directory" -skip_dir = "/Another/Path/To/Different/Directory" -``` -This will be interpreted the same as: -```text -skip_dir = "SomeDir|OtherDir|ThisDir|ThatDir|/Path/To/A/Directory|/Another/Path/To/Different/Directory" -``` - -**Note:** After changing `skip_dir`, you must perform a full re-synchronization by adding `--resync` to your existing command line - for example: `onedrive --synchronize --resync` - -#### skip_file -This option is used to 'skip' certain files and supports pattern matching. - -Patterns are case insensitive. `*` and `?` [wildcards characters](https://technet.microsoft.com/en-us/library/bb490639.aspx) are supported. Use `|` to separate multiple patterns. - -Files can be skipped in the following fashion: -* Specify a wildcard, eg: '*.txt' (skip all txt files) -* Explicitly specify the filename and it's full path relative to your sync_dir, eg: '/path/to/file/filename.ext' -* Explicitly specify the filename only and skip every instance of this filename, eg: 'filename.ext' - -By default, the following files will be skipped: -* Files that start with ~ -* Files that start with .~ (like .~lock.* files generated by LibreOffice) -* Files that end in .tmp - -**Important:** Do not use a skip_file entry of `.*` as this will prevent correct searching of local changes to process. - -Example: -```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_dir = "~/OneDrive" -skip_file = "~*|/Documents/OneNote*|/Documents/config.xlaunch|myfile.ext|/Documents/keepass.kdbx" -# monitor_interval = "300" -# skip_dir = "" -# log_dir = "/var/log/onedrive/" -``` - -**Note:** The `skip_file` can be specified multiple times, for example: -```text -skip_file = "~*|.~*|*.tmp|*.swp" -skip_file = "*.blah" -skip_file = "never_sync.file" -skip_file = "/Documents/keepass.kdbx" -``` -This will be interpreted the same as: -```text -skip_file = "~*|.~*|*.tmp|*.swp|*.blah|never_sync.file|/Documents/keepass.kdbx" -``` - -**Note:** after changing `skip_file`, you must perform a full re-synchronization by adding `--resync` to your existing command line - for example: `onedrive --synchronize --resync` - -#### skip_dotfiles -Setting this to `"true"` will skip all .files and .folders while syncing. - -Example: -```text -# skip_symlinks = "false" -# debug_https = "false" -skip_dotfiles = "true" -# dry_run = "false" -# monitor_interval = "300" -``` - -#### monitor_interval -The monitor interval is defined as the wait time 'between' sync's when running in monitor mode. When this interval expires, the client will check OneDrive for changes online, performing data integrity checks and scanning the local 'sync_dir' for new content. - -By default without configuration, 'monitor_interval' is set to 300 seconds. Setting this value to 600 will run the sync process every 10 minutes. - -Example: -```text -# skip_dotfiles = "false" -# dry_run = "false" -monitor_interval = "600" -# min_notify_changes = "5" -# monitor_log_frequency = "6" -``` -**Note:** It is strongly advised you do not use a value of less than 300 seconds for 'monitor_interval'. Using a value less than 300 means your application will be constantly needlessly checking OneDrive online for changes. Future versions of the application may enforce the checking of this minimum value. - -#### monitor_fullscan_frequency -This configuration option controls the number of 'monitor_interval' iterations between when a full scan of your data is performed to ensure data integrity and consistency. - -By default without configuration, 'monitor_fullscan_frequency' is set to 12. In this default state, this means that a full scan is performed every 'monitor_interval' x 'monitor_fullscan_frequency' = 3600 seconds. This is only applicable when running in --monitor mode. - -Setting this value to 24 means that the full scan of OneDrive and checking the integrity of the data stored locally will occur every 2 hours (assuming 'monitor_interval' is set to 300 seconds): - -Example: -```text -# min_notify_changes = "5" -# monitor_log_frequency = "6" -monitor_fullscan_frequency = "24" -# sync_root_files = "false" -# classify_as_big_delete = "1000" -``` - -**Note:** When running in --monitor mode, at application start-up, a full scan will be performed to ensure data integrity. This option has zero effect when running the application in `--synchronize` mode and a full scan will always be performed. - -#### monitor_log_frequency -This configuration option controls the output of when logging is performed to detail that a sync is occuring with OneDrive when using `--monitor` mode. The frequency of syncing with OneDrive is controled via 'monitor_interval'. - -By default without configuration, 'monitor_log_frequency' is set to 6. - -By default, at application start-up when using `--monitor` mode, the following will be logged to indicate that the application has correctly started and performed all the initial processing steps: -``` -Configuring Global Azure AD Endpoints -Initializing the Synchronization Engine ... -Initializing monitor ... -OneDrive monitor interval (seconds): 300 -Starting a sync with OneDrive -Syncing changes from OneDrive ... -Performing a database consistency and integrity check on locally stored data ... -Sync with OneDrive is complete -``` -Then, based on 'monitor_log_frequency', the following will be logged when the value is reached: -``` -Starting a sync with OneDrive -Syncing changes from OneDrive ... -Sync with OneDrive is complete -``` -**Note:** The additional log output `Performing a database consistency and integrity check on locally stored data ...` will only be displayed when this activity is occuring which is triggered by 'monitor_fullscan_frequency'. - -#### min_notify_changes -This option defines the minimum number of pending incoming changes necessary to trigger a desktop notification. This allows controlling the frequency of notifications. - -Example: -```text -# dry_run = "false" -# monitor_interval = "300" -min_notify_changes = "50" -# monitor_log_frequency = "6" -# monitor_fullscan_frequency = "12" -``` - -#### operation_timeout -Operation Timeout is the maximum amount of time (seconds) a file operation is allowed to take. This includes DNS resolution, connecting, data transfer, etc. - -Example: -```text -# sync_file_permissions = "600" -# rate_limit = "131072" -operation_timeout = "3600" -``` - -#### ip_protocol_version -By default, the application will use IPv4 and IPv6 to resolve and communicate with Microsoft OneDrive. In some Linux distributions (most notably Ubuntu and those distributions based on Ubuntu) this will cause problems due to how DNS resolution is being performed. - -To configure the application to use a specific IP version, configure the following in your config file: -```text -# operation_timeout = "3600" -# dns_timeout = "60" -# connect_timeout = "10" -# data_timeout = "600" -ip_protocol_version = "1" - -``` -**Note:** -* A value of 0 will mean the client will use IPv4 and IPv6. This is the default. -* A value of 1 will mean the client will use IPv4 only. -* A value of 2 will mean the client will use IPv6 only. - -#### classify_as_big_delete -This configuration option will help prevent the online deletion of files and folders online, when the directory that has been deleted contains more items than the specified value. - -By default, this value is 1000 which will count files and folders as children of the directory that has been deleted. - -To change this value, configure the following in your config file: -```text -# monitor_fullscan_frequency = "12" -# sync_root_files = "false" -classify_as_big_delete = "3000" -# user_agent = "" -# remove_source_files = "false" -``` - -**Note:** -* This option only looks at Directories. It has zero effect on deleting files located in your 'sync_dir' root -* This option (in v2.4.x and below) only gets activated when using `--monitor`. In `--synchronize` mode it is ignored as it is assumed you performed that desired operation before you started your next manual sync with OneDrive. -* Be sensible with setting this value - do not use a low value such as '1' as this will prevent you from syncing your data each and every time you delete a single file. - - -#### 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 `--reauth` to re-authenticate the client with this updated application ID. - -## Frequently Asked Configuration Questions - -### How to sync only specific or single directory? -There are two methods to achieve this: -* Utilise '--single-directory' option to only sync this specific path -* Utilise 'sync_list' to configure what files and directories to sync, and what should be exluded - -### How to 'skip' directories from syncing? -There are several mechanisms available to 'skip' a directory from the sync process: -* Utilise 'skip_dir' to configure what directories to skip. Refer to above for configuration advice. -* Utilise 'sync_list' to configure what files and directories to sync, and what should be exluded - -One further method is to add a '.nosync' empty file to any folder. When this file is present, adding `--check-for-nosync` to your command line will now make the sync process skip any folder where the '.nosync' file is present. - -To make this a permanent change to always skip folders when a '.nosync' empty file is present, add the following to your config file: - -Example: -```text -# upload_only = "false" -# check_nomount = "false" -check_nosync = "true" -# download_only = "false" -# disable_notifications = "false" -``` -**Default:** False - -### How to 'skip' files from syncing? -There are two methods to achieve this: -* Utilise 'skip_file' to configure what files to skip. Refer to above for configuration advice. -* Utilise 'sync_list' to configure what files and directories to sync, and what should be exluded - -### How to 'skip' dot files and folders from syncing? -There are three methods to achieve this: -* Utilise 'skip_file' or 'skip_dir' to configure what files or folders to skip. Refer to above for configuration advice. -* Utilise 'sync_list' to configure what files and directories to sync, and what should be exluded -* Utilise 'skip_dotfiles' to skip any dot file (for example: `.Trash-1000` or `.xdg-volume-info`) from syncing to OneDrive. - -Example: -```text -# skip_symlinks = "false" -# debug_https = "false" -skip_dotfiles = "true" -# skip_size = "1000" -# dry_run = "false" -``` -**Default:** False - -### How to 'skip' files larger than a certain size from syncing? -There are two methods to achieve this: -* Use `--skip-size ARG` as part of a CLI command to skip new files larger than this size (in MB) -* Use `skip_size = "value"` as part of your 'config' file where files larger than this size (in MB) will be skipped - -### How to 'rate limit' the application to control bandwidth consumed for upload & download operations? -To minimise the Internet bandwidth for upload and download operations, you can configure the 'rate_limit' option within the config file. - -Example valid values for this are as follows: -* 131072 = 128 KB/s - minimum for basic application operations to prevent timeouts -* 262144 = 256 KB/s -* 524288 = 512 KB/s -* 1048576 = 1 MB/s -* 10485760 = 10 MB/s -* 104857600 = 100 MB/s - -Example: -```text -# sync_business_shared_folders = "false" -# sync_dir_permissions = "700" -# sync_file_permissions = "600" -rate_limit = "131072" -``` - -**Note:** A number greater than '131072' is a valid value, with '104857600' being tested as an upper limit. - -### How to prevent your local disk from filling up? -By default, the application will reserve 50MB of disk space to prevent your filesystem to run out of disk space. This value can be modified by adding the following to your config file: - -Example: -```text -... -# webhook_expiration_interval = "86400" -# webhook_renewal_interval = "43200" -space_reservation = "10" -``` - -The value entered is in MB (Mega Bytes). In this example, a value of 10MB is being used, and will be converted to bytes by the application. The value being used can be reviewed when using `--display-config`: -``` -Config option 'sync_dir_permissions' = 700 -Config option 'sync_file_permissions' = 600 -Config option 'space_reservation' = 10485760 -Config option 'application_id' = -Config option 'azure_ad_endpoint' = -Config option 'azure_tenant_id' = common -``` - -Any value is valid here, however, if you use a value of '0' a value of '1' will actually be used, so that you actually do not run out of disk space. - -### How are symbolic links handled by the client? -Microsoft OneDrive has zero concept or understanding of symbolic links, and attempting to upload a symbolic link to Microsoft OneDrive generates a platform API error. All data (files and folders) that are uploaded to OneDrive must be whole files or actual directories. - -As such, there are only two methods to support symbolic links with this client: -1. Follow the Linux symbolic link and upload what ever the link is pointing at to OneDrive. This is the default behaviour. -2. Skip symbolic links by configuring the application to do so. In skipping, no data, no link, no reference is uploaded to OneDrive. - -To skip symbolic links, edit your configuration as per below: - -```text -# local_first = "false" -# no_remote_delete = "false" -skip_symlinks = "true" -# debug_https = "false" -# skip_dotfiles = "false" -``` -Setting this to `"true"` will configure the client to skip all symbolic links while syncing. - -The default setting is `"false"` which will sync the whole folder structure referenced by the symbolic link, duplicating the contents on OneDrive in the place where the symbolic link is. - -### How to sync shared folders (OneDrive Personal)? -Folders shared with you can be synced by adding them to your OneDrive. To do that open your Onedrive, go to the Shared files list, right click on the folder you want to sync and then click on "Add to my OneDrive". - -### How to sync shared folders (OneDrive Business or Office 365)? -Refer to [./BusinessSharedFolders.md](BusinessSharedFolders.md) for configuration assistance. - -Do not use the 'Add shortcut to My files' from the OneDrive web based interface to add a 'shortcut' to your shared folder. This shortcut is not supported by the OneDrive API, thus it cannot be used. - -### How to sync sharePoint / Office 365 Shared Libraries? -Refer to [./SharePoint-Shared-Libraries.md](SharePoint-Shared-Libraries.md) for configuration assistance. - -### How to run a user systemd service at boot without user login? -In some cases it may be desirable for the systemd service to start without having to login as your 'user' - -To avoid this issue, you need to reconfigure your 'user' account so that the systemd services you have created will startup without you having to login to your system: -```text -loginctl enable-linger -``` - -### How to create a shareable link? -In some cases it may be desirable to create a shareable file link and give this link to other users to access a specific file. - -To do this, use the following command: -```text -onedrive --create-share-link -``` -**Note:** By default this will be a read-only link. - -To make this a read-write link, use the following command: -```text -onedrive --create-share-link --with-editing-perms -``` -**Note:** The ordering of the option file path and option flag is important. - -### How to sync both Personal and Business accounts at the same time? -You must configure separate instances of the application configuration for each account. - -Refer to [./advanced-usage.md](advanced-usage.md) for configuration assistance. - -### How to sync multiple SharePoint Libraries at the same time? -You must configure a separate instances of the application configuration for each SharePoint Library. - -Refer to [./advanced-usage.md](advanced-usage.md) for configuration assistance. - -## Running 'onedrive' in 'monitor' mode -Monitor mode (`--monitor`) allows the onedrive process to continually monitor your local file system for changes to files. - -Two common errors can occur when using monitor mode: -* Intialisation failure -* Unable to add a new inotify watch - -Both of these errors are local environment issues, where the following system variables need to be increased as the current system values are potentially too low: -* `fs.file-max` -* `fs.inotify.max_user_watches` - -To determine what the existing values are on your system use the following commands: -```text -sysctl fs.file-max -sysctl fs.inotify.max_user_watches -``` - -To determine what value to change to, you need to count all the files and folders in your configured 'sync_dir': -```text -cd /path/to/your/sync/dir -ls -laR | wc -l -``` - -To make a change to these variables using your file and folder count: -``` -sudo sysctl fs.file-max= -sudo sysctl fs.inotify.max_user_watches= -``` - -To make these changes permanent, refer to your OS reference documentation. - -### Use webhook to subscribe to remote updates in 'monitor' mode - -A webhook can be optionally enabled in the monitor mode to allow the onedrive process to subscribe to remote updates. Remote changes can be synced to your local file system as soon as possible, without waiting for the next sync cycle. - -To enable this feature, you need to configure the following options in the config file: - -```text -webhook_enabled = "true" -webhook_public_url = "" -``` - -Setting `webhook_enabled` to `true` enables the webhook in 'monitor' mode. The onedrive process will listen for incoming updates at a configurable endpoint, which defaults to `0.0.0.0:8888`. The `webhook_public_url` must be set to an public-facing url for Microsoft to send updates to your webhook. If your host is directly exposed to the Internet, the `webhook_public_url` can be set to `http://:8888/` to match the default endpoint. However, the recommended approach is to configure a reverse proxy like nginx. - -**Note:** A valid HTTPS certificate is required for your public-facing URL if using nginx. - -For example, below is a nginx config snippet to proxy traffic into the webhook: - -```text -server { - listen 80; - location /webhooks/onedrive { - proxy_http_version 1.1; - proxy_pass http://127.0.0.1:8888; - } -} -``` - -With nginx running, you can configure `webhook_public_url` to `https:///webhooks/onedrive`. - -If you receive this application error: -```text -Subscription validation request failed. Response must exactly match validationToken query parameter. -``` -The most likely cause for this error will be your nginx configuration. To resolve, potentially investigate the following configuration for nginx: - -```text -server { - listen 80; - location /webhooks/onedrive { - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Original-Request-URI $request_uri; - proxy_read_timeout 300s; - proxy_connect_timeout 75s; - proxy_buffering off; - proxy_http_version 1.1; - proxy_pass http://127.0.0.1:8888; - } -} -``` - -For any further nginx configuration assistance, please refer to: https://docs.nginx.com/ - -### More webhook configuration options - -Below options can be optionally configured. The default is usually good enough. - -#### webhook_listening_host and webhook_listening_port - -Set `webhook_listening_host` and `webhook_listening_port` to change the webhook listening endpoint. If `webhook_listening_host` is left empty, which is the default, the webhook will bind to `0.0.0.0`. The default `webhook_listening_port` is `8888`. - -``` -webhook_listening_host = "" -webhook_listening_port = "8888" -``` - -#### webhook_expiration_interval and webhook_renewal_interval - -Set `webhook_expiration_interval` and `webhook_renewal_interval` to change the frequency of subscription renewal. By default, the webhook asks Microsoft to keep subscriptions alive for 24 hours, and it renews subscriptions when it is less than 12 hours before their expiration. - -``` -# Default expiration interval is 24 hours -webhook_expiration_interval = "86400" - -# Default renewal interval is 12 hours -webhook_renewal_interval = "43200" -``` - -## Running 'onedrive' as a system service -There are a few ways to use onedrive as a service -* via init.d -* via systemd -* via runit - -**Note:** If using the service files, you may need to increase the `fs.inotify.max_user_watches` value on your system to handle the number of files in the directory you are monitoring as the initial value may be too low. - -### OneDrive service running as root user via init.d -```text -chkconfig onedrive on -service onedrive start -``` -To see the logs run: -```text -tail -f /var/log/onedrive/.onedrive.log -``` -To change what 'user' the client runs under (by default root), manually edit the init.d service file and modify `daemon --user root onedrive_service.sh` for the correct user. - -### OneDrive service running as root user via systemd (Arch, Ubuntu, Debian, OpenSuSE, Fedora) -First, su to root using `su - root`, then enable the systemd service: -```text -systemctl --user enable onedrive -systemctl --user start onedrive -``` -**Note:** `systemctl --user` directive is not applicable for Red Hat Enterprise Linux (RHEL) or CentOS Linux platforms - see below. - -**Note:** This will run the 'onedrive' process with a UID/GID of '0', thus, any files or folders that are created will be owned by 'root' - -To view the status of the service running, use the following: -```text -systemctl --user status onedrive.service -``` - -To see the systemd application logs run: -```text -journalctl --user-unit=onedrive -f -``` - -**Note:** It is a 'systemd' requirement that the XDG environment variables exist for correct enablement and operation of systemd services. If you receive this error when enabling the systemd service: -``` -Failed to connect to bus: No such file or directory -``` -The most likely cause is that the XDG environment variables are missing. To fix this, you must add the following to `.bashrc` or any other file which is run on user login: -``` -export XDG_RUNTIME_DIR="/run/user/$UID" -export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus" -``` - -To make this change effective, you must logout of all user accounts where this change has been made. - -**Note:** On some systems (for example - Raspbian / Ubuntu / Debian on Raspberry Pi) the above XDG fix may not be reliable after system reboots. The potential alternative to start the client via systemd as root, is to perform the following: -1. Create a symbolic link from `/home/root/.config/onedrive` pointing to `/root/.config/onedrive/` -2. Create a systemd service using the '@' service file: `systemctl enable onedrive@root.service` -3. Start the root@service: `systemctl start onedrive@root.service` - -This will ensure that the service will correctly restart on system reboot. - -To see the systemd application logs run: -```text -journalctl --unit=onedrive@ -f -``` - -### OneDrive service running as root user via systemd (Red Hat Enterprise Linux, CentOS Linux) -```text -systemctl enable onedrive -systemctl start onedrive -``` -**Note:** This will run the 'onedrive' process with a UID/GID of '0', thus, any files or folders that are created will be owned by 'root' - -To see the systemd application logs run: -```text -journalctl --unit=onedrive -f -``` - -### OneDrive service running as a non-root user via systemd (All Linux Distributions) -In some cases it is desirable to run the OneDrive client as a service, but not running as the 'root' user. In this case, follow the directions below to configure the service for your normal user login. - -1. As the user, who will be running the service, run the application in standalone mode, authorize the application for use & validate that the synchronization is working as expected: -```text -onedrive --synchronize --verbose -``` -2. Once the application is validated and working for your user, as the 'root' user, where is your username from step 1 above. -```text -systemctl enable onedrive@.service -systemctl start onedrive@.service -``` -3. To view the status of the service running for the user, use the following: -```text -systemctl status onedrive@.service -``` - -To see the systemd application logs run: -```text -journalctl --unit=onedrive@ -f -``` - -### OneDrive service running as a non-root user via systemd (with notifications enabled) (Arch, Ubuntu, Debian, OpenSuSE, Fedora) -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 `onedrive@` service files for your username - eg: -```text -sudo systemctl stop onedrive@alex.service -sudo systemctl disable onedrive@alex.service -``` -3. Enable service as per the following: -```text -systemctl --user enable onedrive -systemctl --user start onedrive -``` - -To view the status of the service running for the user, use the following: -```text -systemctl --user status onedrive.service -``` - -To see the systemd application logs run: -```text -journalctl --user-unit=onedrive -f -``` - -**Note:** `systemctl --user` directive is not applicable for Red Hat Enterprise Linux (RHEL) or CentOS Linux platforms - -### OneDrive service running as a non-root user via runit (antiX, Devuan, Artix, Void) - -1. Create the following folder if not present already `/etc/sv/runsvdir-` - - - where `` is the `USER` targeted for the service - - _e.g_ `# mkdir /etc/sv/runsvdir-nolan` - -2. Create a file called `run` under the previously created folder with - executable permissions - - - `# touch /etc/sv/runsvdir-/run` - - `# chmod 0755 /etc/sv/runsvdir-/run` - -3. Edit the `run` file with the following contents (priviledges needed) - - ```sh - #!/bin/sh - export USER="" - export HOME="/home/" - - groups="$(id -Gn "${USER}" | tr ' ' ':')" - svdir="${HOME}/service" - - exec chpst -u "${USER}:${groups}" runsvdir "${svdir}" - ``` - - - do not forget to correct the `` according to the `USER` set on - step #1 - -4. Enable the previously created folder as a service - - - `# ln -fs /etc/sv/runsvdir- /var/service/` - -5. Create a subfolder on the `USER`'s `HOME` directory to store the services - (or symlinks) - - - `$ mkdir ~/service` - -6. Create a subfolder for OneDrive specifically - - - `$ mkdir ~/service/onedrive/` - -7. Create a file called `run` under the previously created folder with - executable permissions - - - `$ touch ~/service/onedrive/run` - - `$ chmod 0755 ~/service/onedrive/run` - -8. Append the following contents to the `run` file - - ```sh - #!/usr/bin/env sh - exec /usr/bin/onedrive --monitor - ``` - - - in some scenario the path for the `onedrive` binary might differ, you can - obtain it regardless by running `$ command -v onedrive` - -9. Reboot to apply changes - -10. Check status of user-defined services - - - `$ sv status ~/service/*` - -You may refer to Void's documentation regarding -[Per-User Services](https://docs.voidlinux.org/config/services/user-services.html) -for extra details. - -## Additional Configuration -### Advanced Configuration of the OneDrive Free Client -* Configuring the client to use mulitple OneDrive accounts / configurations, for example: - * Setup to use onedrive with both Personal and Business accounts - * Setup to use onedrive with multiple SharePoint Libraries -* Configuring the client for use in dual-boot (Windows / Linux) situations -* Configuring the client for use when 'sync_dir' is a mounted directory -* Upload data from the local ~/OneDrive folder to a specific location on OneDrive - -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: -1. Set proxy configuration in `~/.bashrc` to allow the authorization process and when utilizing `--synchronize` -2. If running as a systemd service, edit the applicable systemd service file to include the proxy configuration information: -```text -[Unit] -Description=OneDrive Free Client -Documentation=https://github.com/abraunegg/onedrive -After=network-online.target -Wants=network-online.target - -[Service] -Environment="HTTP_PROXY=http://ip.address:port" -Environment="HTTPS_PROXY=http://ip.address:port" -ExecStart=/usr/local/bin/onedrive --monitor -Restart=on-failure -RestartSec=3 - -[Install] -WantedBy=default.target -``` - -**Note:** After modifying the service files, you will need to run `sudo systemctl daemon-reload` to ensure the service file changes are picked up. A restart of the OneDrive service will also be required to pick up the change to send the traffic via the proxy server - -### Setup selinux for a sync folder outside of the home folder -If selinux is enforced and the sync folder is outside of the home folder, as long as there is no policy for cloud fileservice providers, label the file system folder to user_home_t. -```text -sudo semanage fcontext -a -t user_home_t /path/to/onedriveSyncFolder -sudo restorecon -R -v /path/to/onedriveSyncFolder -``` -To remove this change from selinux and restore the default behaivor: -```text -sudo semanage fcontext -d /path/to/onedriveSyncFolder -sudo restorecon -R -v /path/to/onedriveSyncFolder -``` - -## All available commands -Output of `onedrive --help` -```text -OneDrive - a client for OneDrive Cloud Services - -Usage: - onedrive [options] --synchronize - Do a one time synchronization - onedrive [options] --monitor - Monitor filesystem and sync regularly - onedrive [options] --display-config - Display the currently used configuration - onedrive [options] --display-sync-status - Query OneDrive service and report on pending changes - onedrive -h | --help - Show this help screen - onedrive --version - Show version - -Options: - - --auth-files ARG - Perform authorization via two files passed in as ARG in the format `authUrl:responseUrl` - The authorization URL is written to the `authUrl`, then onedrive waits for the file `responseUrl` - to be present, and reads the response from that file. - --auth-response ARG - Perform authentication not via interactive dialog but via providing the response url directly. - --check-for-nomount - Check for the presence of .nosync in the syncdir root. If found, do not perform sync. - --check-for-nosync - Check for the presence of .nosync in each directory. If found, skip directory from sync. - --classify-as-big-delete - Number of children in a path that is locally removed which will be classified as a 'big data delete' - --cleanup-local-files - Cleanup additional local files when using --download-only. This will remove local data. - --confdir ARG - Set the directory used to store the configuration files - --create-directory ARG - Create a directory on OneDrive - no sync will be performed. - --create-share-link ARG - Create a shareable link for an existing file on OneDrive - --debug-https - Debug OneDrive HTTPS communication. - --destination-directory ARG - Destination directory for renamed or move on OneDrive - no sync will be performed. - --disable-download-validation - Disable download validation when downloading from OneDrive - --disable-notifications - Do not use desktop notifications in monitor mode. - --disable-upload-validation - Disable upload validation when uploading to OneDrive - --display-config - Display what options the client will use as currently configured - no sync will be performed. - --display-running-config - Display what options the client has been configured to use on application startup. - --display-sync-status - Display the sync status of the client - no sync will be performed. - --download-only - Replicate the OneDrive online state locally, by only downloading changes from OneDrive. Do not upload local changes to OneDrive. - --dry-run - Perform a trial sync with no changes made - --enable-logging - Enable client activity to a separate log file - --force - Force the deletion of data when a 'big delete' is detected - --force-http-11 - Force the use of HTTP 1.1 for all operations - --force-sync - Force a synchronization of a specific folder, only when using --single-directory and ignoring all non-default skip_dir and skip_file rules - --get-O365-drive-id ARG - Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library - --get-file-link ARG - Display the file link of a synced file - --help -h - This help information. - --list-shared-folders - List OneDrive Business Shared Folders - --local-first - Synchronize from the local directory source first, before downloading changes from OneDrive. - --log-dir ARG - Directory where logging output is saved to, needs to end with a slash. - --logout - Logout the current user - --min-notify-changes ARG - Minimum number of pending incoming changes necessary to trigger a desktop notification - --modified-by ARG - Display the last modified by details of a given path - --monitor -m - Keep monitoring for local and remote changes - --monitor-fullscan-frequency ARG - Number of sync runs before performing a full local scan of the synced directory - --monitor-interval ARG - Number of seconds by which each sync operation is undertaken when idle under monitor mode. - --monitor-log-frequency ARG - Frequency of logging in monitor mode - --no-remote-delete - Do not delete local file 'deletes' from OneDrive when using --upload-only - --operation-timeout ARG - Maximum amount of time (in seconds) an operation is allowed to take - --print-token - Print the access token, useful for debugging - --reauth - Reauthenticate the client with OneDrive - --remove-directory ARG - Remove a directory on OneDrive - no sync will be performed. - --remove-source-files - Remove source file after successful transfer to OneDrive when using --upload-only - --resync - Forget the last saved state, perform a full sync - --resync-auth - Approve the use of performing a --resync action - --single-directory ARG - Specify a single local directory within the OneDrive root to sync. - --skip-dir ARG - Skip any directories that match this pattern from syncing - --skip-dir-strict-match - When matching skip_dir directories, only match explicit matches - --skip-dot-files - Skip dot files and folders from syncing - --skip-file ARG - Skip any files that match this pattern from syncing - --skip-size ARG - Skip new files larger than this size (in MB) - --skip-symlinks - Skip syncing of symlinks - --source-directory ARG - Source directory to rename or move on OneDrive - no sync will be performed. - --space-reservation ARG - The amount of disk space to reserve (in MB) to avoid 100% disk space utilisation - --sync-root-files - Sync all files in sync_dir root when using sync_list. - --sync-shared-folders - Sync OneDrive Business Shared Folders - --syncdir ARG - Specify the local directory used for synchronization to OneDrive - --synchronize - Perform a synchronization - --upload-only - Replicate the locally configured sync_dir state to OneDrive, by only uploading local changes to OneDrive. Do not download changes from OneDrive. - --user-agent ARG - Specify a User Agent string to the http client - --verbose -v+ - Print more details, useful for debugging (repeat for extra debugging) - --version - Print the version and exit - --with-editing-perms - Create a read-write shareable link for an existing file on OneDrive when used with --create-share-link -``` diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md deleted file mode 100644 index 2701909d..00000000 --- a/docs/advanced-usage.md +++ /dev/null @@ -1,302 +0,0 @@ -# Advanced Configuration of the OneDrive Free Client -This document covers the following scenarios: -* [Configuring the client to use multiple OneDrive accounts / configurations](#configuring-the-client-to-use-multiple-onedrive-accounts--configurations) -* [Configuring the client to use multiple OneDrive accounts / configurations using Docker](#configuring-the-client-to-use-multiple-onedrive-accounts--configurations-using-docker) -* [Configuring the client for use in dual-boot (Windows / Linux) situations](#configuring-the-client-for-use-in-dual-boot-windows--linux-situations) -* [Configuring the client for use when 'sync_dir' is a mounted directory](#configuring-the-client-for-use-when-sync_dir-is-a-mounted-directory) -* [Upload data from the local ~/OneDrive folder to a specific location on OneDrive](#upload-data-from-the-local-onedrive-folder-to-a-specific-location-on-onedrive) - -## Configuring the client to use multiple OneDrive accounts / configurations -Essentially, each OneDrive account or SharePoint Shared Library which you require to be synced needs to have its 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` - -### Step1: Create a new systemd service file -#### Red Hat Enterprise Linux, CentOS Linux -Copy the required service file to a new name: -```text -sudo cp /usr/lib/systemd/system/onedrive.service /usr/lib/systemd/system/onedrive-my-new-config -``` -or -```text -sudo cp /usr/lib/systemd/system/onedrive@.service /usr/lib/systemd/system/onedrive-my-new-config@.service -``` - -#### Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora -Copy the required service file to a new name: -```text -sudo cp /usr/lib/systemd/user/onedrive.service /usr/lib/systemd/user/onedrive-my-new-config.service -``` -or -```text -sudo cp /lib/systemd/system/onedrive@.service /lib/systemd/system/onedrive-my-new-config@.service -``` - -### Step 2: Edit new systemd service file -Edit the new systemd file, updating 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" -``` - -**Note:** When running the client manually, `--confdir="~/.config/......` is acceptable. In a systemd configuration file, the full path must be used. The `~` must be expanded. - -### Step 3: Enable the new systemd service -Once the file is correctly editied, you can enable the new systemd service using the following commands. - -#### Red Hat Enterprise Linux, CentOS Linux -```text -systemctl enable onedrive-my-new-config -systemctl start onedrive-my-new-config -``` - -#### Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora -```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 -``` - -### Step 4: Viewing systemd status and logs for the custom service -#### Viewing systemd service status - Red Hat Enterprise Linux, CentOS Linux -```text -systemctl status onedrive-my-new-config -``` - -#### Viewing systemd service status - Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora -```text -systemctl --user status onedrive-my-new-config -``` - -#### Viewing journalctl systemd logs - Red Hat Enterprise Linux, CentOS Linux -```text -journalctl --unit=onedrive-my-new-config -f -``` - -#### Viewing journalctl systemd logs - Others such as Arch, Ubuntu, Debian, OpenSuSE, Fedora -```text -journalctl --user --unit=onedrive-my-new-config -f -``` - -### Step 5: (Optional) Run custom systemd service at boot without user login -In some cases it may be desirable for the systemd service to start without having to login as your 'user' - -All the systemd steps above that utilise the `--user` option, will run the systemd service as your particular user. As such, the systemd service will not start unless you actually login to your system. - -To avoid this issue, you need to reconfigure your 'user' account so that the systemd services you have created will startup without you having to login to your system: -```text -loginctl enable-linger -``` - -Example: -```text -alex@ubuntu-headless:~$ loginctl enable-linger alex -``` - -Repeat these steps for each OneDrive new account that you wish to use. - -## Configuring the client to use multiple OneDrive accounts / configurations using Docker -In some situations it may be desirable to run multiple Docker containers at the same time, each with their own configuration. - -To run the Docker container successfully, it needs two unique Docker volumes to operate: -* Your configuration Docker volumes -* Your data Docker volume - -When running multiple Docker containers, this is no different - each Docker container must have it's own configuration and data volume. - -### High level steps: -1. Create the required unique Docker volumes for the configuration volume -2. Create the required unique local path used for the Docker data volume -3. Start the multiple Docker containers with the required configuration for each container - -#### Create the required unique Docker volumes for the configuration volume -Create the required unique Docker volumes for the configuration volume(s): -```text -docker volume create onedrive_conf_sharepoint_site1 -docker volume create onedrive_conf_sharepoint_site2 -docker volume create onedrive_conf_sharepoint_site3 -... -docker volume create onedrive_conf_sharepoint_site50 -``` - -#### Create the required unique local path used for the Docker data volume -Create the required unique local path used for the Docker data volume -```text -mkdir -p /use/full/local/path/no/tilda/SharePointSite1 -mkdir -p /use/full/local/path/no/tilda/SharePointSite2 -mkdir -p /use/full/local/path/no/tilda/SharePointSite3 -... -mkdir -p /use/full/local/path/no/tilda/SharePointSite50 -``` - -#### Start the Docker container with the required configuration (example) -```text -docker run -it --name onedrive -v onedrive_conf_sharepoint_site1:/onedrive/conf -v "/use/full/local/path/no/tilda/SharePointSite1:/onedrive/data" driveone/onedrive:latest -docker run -it --name onedrive -v onedrive_conf_sharepoint_site2:/onedrive/conf -v "/use/full/local/path/no/tilda/SharePointSite2:/onedrive/data" driveone/onedrive:latest -docker run -it --name onedrive -v onedrive_conf_sharepoint_site3:/onedrive/conf -v "/use/full/local/path/no/tilda/SharePointSite3:/onedrive/data" driveone/onedrive:latest -... -docker run -it --name onedrive -v onedrive_conf_sharepoint_site50:/onedrive/conf -v "/use/full/local/path/no/tilda/SharePointSite50:/onedrive/data" driveone/onedrive:latest -``` - -#### TIP -To avoid 're-authenticating' and 'authorising' each individual Docker container, if all the Docker containers are using the 'same' OneDrive credentials, you can re-use the 'refresh_token' from one Docker container to another by copying this file to the configuration Docker volume of each Docker container. - -If the account credentials are different .. you will need to re-authenticate each Docker container individually. - -## 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
SharePoint | -|---|---| -| ![Uncheck-Personal](./images/personal-files-on-demand.png) | ![Uncheck-Business](./images/business-files-on-demand.png) | - -## Configuring the client for use when 'sync_dir' is a mounted directory -In some environments, your setup might be that your configured 'sync_dir' is pointing to another mounted file system - a NFS|CIFS location, an external drive (USB stuc, eSATA etc). As such, you configure your 'sync_dir' as follows: -```text -sync_dir = "/path/to/mountpoint/OneDrive" -``` - -The issue here is - how does the client react if the mount point gets removed - network loss, device removal? - -The client has zero knowledge of any event that causes a mountpoint to become unavailable, thus, the client (if you are running as a service) will assume that you deleted the files, thus, will go ahead and delete all your files on OneDrive. This is most certainly an undesirable action. - -There are a few options here which you can configure in your 'config' file to assist you to prevent this sort of item from occuring: -1. classify_as_big_delete -2. check_nomount -3. check_nosync - -**Note:** Before making any change to your configuration, stop any sync process & stop any onedrive systemd service from running. - -### classify_as_big_delete -By default, this uses a value of 1000 files|folders. An undesirable unmount if you have more than 1000 files, this default level will prevent the client from executing the online delete. Modify this value up or down as desired - -### check_nomount & check_nosync -These two options are really the right safe guards to use. - -In your 'mount point', *before* you mount your external folder|device, create empty `.nosync` file, so that this is the *only* file present in the mount location before you mount your data to your mount point. When you mount your data, this '.nosync' file will not be visible, but, if the device you are mounting goes away - this '.nosync' file is the only file visible. - -Next, in your 'config' file, configure the following options: `check_nomount = "true"` and `check_nosync = "true"` - -What this will do is tell the client, if at *any* point you see this file - stop syncing - thus, protecting your online data from being deleted by the mounted device being suddenly unavailable. - -After making this sort of change - test with `--dry-run` so you can see the impacts of your mount point being unavailable, and how the client is now reacting. Once you are happy with how the system will react, restart your sync processes. - - -## Upload data from the local ~/OneDrive folder to a specific location on OneDrive -In some environments, you may not want your local ~/OneDrive folder to be uploaded directly to the root of your OneDrive account online. - -Unfortunatly, the OneDrive API lacks any facility to perform a re-direction of data during upload. - -The workaround for this is to structure your local filesystem and reconfigure your client to achieve the desired goal. - -### High level steps: -1. Create a new folder, for example `/opt/OneDrive` -2. Configure your application config 'sync_dir' to look at this folder -3. Inside `/opt/OneDrive` create the folder you wish to sync the data online to, for example: `/opt/OneDrive/RemoteOnlineDestination` -4. Configure the application to only sync `/opt/OneDrive/RemoteDestination` via 'sync_list' -5. Symbolically link `~/OneDrive` -> `/opt/OneDrive/RemoteOnlineDestination` - -### Outcome: -* Your `~/OneDrive` will look / feel as per normal -* The data will be stored online under `/RemoteOnlineDestination` - -### Testing: -* Validate your configuration with `onedrive --display-config` -* Test your configuration with `onedrive --dry-run` diff --git a/docs/application-security.md b/docs/application-security.md deleted file mode 100644 index 7c22c4f1..00000000 --- a/docs/application-security.md +++ /dev/null @@ -1,97 +0,0 @@ -# OneDrive Client for Linux Application Security -This document details the following information: - -* Why is this application an 'unverified publisher'? -* Application Security and Permission Scopes -* How to change Permission Scopes -* How to review your existing application access consent - -## Why is this application an 'unverified publisher'? -Publisher Verification, as per the Microsoft [process](https://learn.microsoft.com/en-us/azure/active-directory/develop/publisher-verification-overview) has actually been configured, and, actually has been verified! - -### Verified Publisher Configuration Evidence -As per the image below, the Azure portal shows that the 'Publisher Domain' has actually been verified: -![confirmed_verified_publisher](./images/confirmed_verified_publisher.jpg) - -* The 'Publisher Domain' is: https://abraunegg.github.io/ -* The required 'Microsoft Identity Association' is: https://abraunegg.github.io/.well-known/microsoft-identity-association.json - -## Application Security and Permission Scopes -There are 2 main components regarding security for this application: -* Azure Application Permissions -* User Authentication Permissions - -Keeping this in mind, security options should follow the security principal of 'least privilege': -> The principle that a security architecture should be designed so that each entity -> is granted the minimum system resources and authorizations that the entity needs -> to perform its function. - -Reference: [https://csrc.nist.gov/glossary/term/least_privilege](https://csrc.nist.gov/glossary/term/least_privilege) - -As such, the following API permissions are used by default: - -### Default Azure Application Permissions - -| API / Permissions name | Type | Description | Admin consent required | -|---|---|---|---| -| Files.Read | Delegated | Have read-only access to user files | No | -| Files.Read.All | Delegated | Have read-only access to all files user can access | No | -| Sites.Read.All | Delegated | Have read-only access to all items in all site collections | No | -| offline_access | Delegated | Maintain access to data you have given it access to | No | - -![default_authentication_scopes](./images/default_authentication_scopes.jpg) - -### Default User Authentication Permissions - -When a user authenticates with Microsoft OneDrive, additional account permissions are provided by service to give the user specific access to their data. These are delegated permissions provided by the platform: - -| API / Permissions name | Type | Description | Admin consent required | -|---|---|---|---| -| Files.ReadWrite | Delegated | Have full access to user files | No | -| Files.ReadWrite.All | Delegated | Have full access to all files user can access | No | -| Sites.ReadWrite.All | Delegated | Have full access to all items in all site collections | No | -| offline_access | Delegated | Maintain access to data you have given it access to | No | - -When these delegated API permissions are combined, these provide the effective authentication scope for the OneDrive Client for Linux to access your data. The resulting effective 'default' permissions will be: - -| API / Permissions name | Type | Description | Admin consent required | -|---|---|---|---| -| Files.ReadWrite | Delegated | Have full access to user files | No | -| Files.ReadWrite.All | Delegated | Have full access to all files user can access | No | -| Sites.ReadWrite.All | Delegated | Have full access to all items in all site collections | No | -| offline_access | Delegated | Maintain access to data you have given it access to | No | - -These 'default' permissions will allow the OneDrive Client for Linux to read, write and delete data associated with your OneDrive Account. - -## Configuring read-only access to your OneDrive data -In some situations, it may be desirable to configure the OneDrive Client for Linux totally in read-only operation. - -To change the application to 'read-only' access, add the following to your configuration file: -```text -read_only_auth_scope = "true" -``` -This will change the user authentication scope request to use read-only access. - -**Note:** When changing this value, you *must* re-authenticate the client using the `--reauth` option to utilise the change in authentication scopes. - -When using read-only authentication scopes, the uploading of any data or local change to OneDrive will fail with the following error: -``` -2022-Aug-06 13:16:45.3349625 ERROR: Microsoft OneDrive API returned an error with the following message: -2022-Aug-06 13:16:45.3351661 Error Message: HTTP request returned status code 403 (Forbidden) -2022-Aug-06 13:16:45.3352467 Error Reason: Access denied -2022-Aug-06 13:16:45.3352838 Error Timestamp: 2022-06-12T13:16:45 -2022-Aug-06 13:16:45.3353171 API Request ID: -``` - -As such, it is also advisable for you to add the following to your configuration file so that 'uploads' are prevented: -```text -download_only = "true" -``` - -**Important:** Additionally when using 'read_only_auth_scope' you also will need to remove your existing application access consent otherwise old authentication consent will be valid and will be used. This will mean the application will technically have the consent to upload data. See below on how to remove your prior application consent. - -## Reviewing your existing application access consent - -To review your existing application access consent, you need to access the following URL: https://account.live.com/consent/Manage - -From here, you are able to review what applications have been given what access to your data, and remove application access as required. diff --git a/docs/build-rpm-howto.md b/docs/build-rpm-howto.md deleted file mode 100644 index 5439c366..00000000 --- a/docs/build-rpm-howto.md +++ /dev/null @@ -1,379 +0,0 @@ -# RPM Package Build Process -The instuctions below have been tested on the following systems: -* CentOS 7 x86_64 -* CentOS 8 x86_64 - -These instructions should also be applicable for RedHat & Fedora platforms, or any other RedHat RPM based distribution. - -## Prepare Package Development Environment (CentOS 7, 8) -Install the following dependencies on your build system: -```text -sudo yum groupinstall -y 'Development Tools' -sudo yum install -y libcurl-devel -sudo yum install -y sqlite-devel -sudo yum install -y libnotify-devel -sudo yum install -y wget -sudo yum install -y http://downloads.dlang.org/releases/2.x/2.088.0/dmd-2.088.0-0.fedora.x86_64.rpm -mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS} -``` - -## Build RPM from spec file -Build the RPM from the provided spec file: -```text -wget https://github.com/abraunegg/onedrive/archive/refs/tags/v2.4.22.tar.gz -O ~/rpmbuild/SOURCES/v2.4.22.tar.gz -wget https://raw.githubusercontent.com/abraunegg/onedrive/master/contrib/spec/onedrive.spec.in -O ~/rpmbuild/SPECS/onedrive.spec -rpmbuild -ba ~/rpmbuild/SPECS/onedrive.spec -``` - -## RPM Build Example Results -Below are example output results of building, installing and running the RPM package on the respective platforms: - -### CentOS 7 -```text -[alex@localhost ~]$ rpmbuild -ba ~/rpmbuild/SPECS/onedrive.spec -Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.wi6Tdz -+ umask 022 -+ cd /home/alex/rpmbuild/BUILD -+ cd /home/alex/rpmbuild/BUILD -+ rm -rf onedrive-2.4.15 -+ /usr/bin/tar -xf - -+ /usr/bin/gzip -dc /home/alex/rpmbuild/SOURCES/v2.4.15.tar.gz -+ STATUS=0 -+ '[' 0 -ne 0 ']' -+ cd onedrive-2.4.15 -+ /usr/bin/chmod -Rf a+rX,u+w,g-w,o-w . -+ exit 0 -Executing(%build): /bin/sh -e /var/tmp/rpm-tmp.dyeEuM -+ umask 022 -+ cd /home/alex/rpmbuild/BUILD -+ cd onedrive-2.4.15 -+ CFLAGS='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic' -+ export CFLAGS -+ CXXFLAGS='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic' -+ export CXXFLAGS -+ FFLAGS='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -I/usr/lib64/gfortran/modules' -+ export FFLAGS -+ FCFLAGS='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -I/usr/lib64/gfortran/modules' -+ export FCFLAGS -+ LDFLAGS='-Wl,-z,relro ' -+ export LDFLAGS -+ '[' 1 == 1 ']' -+ '[' x86_64 == ppc64le ']' -++ find . -name config.guess -o -name config.sub -+ ./configure --build=x86_64-redhat-linux-gnu --host=x86_64-redhat-linux-gnu --program-prefix= --disable-dependency-tracking --prefix=/usr --exec-prefix=/usr --bindir=/usr/bin --sbindir=/usr/sbin --sysconfdir=/etc --datadir=/usr/share --includedir=/usr/include --libdir=/usr/lib64 --libexecdir=/usr/libexec --localstatedir=/var --sharedstatedir=/var/lib --mandir=/usr/share/man --infodir=/usr/share/info -configure: WARNING: unrecognized options: --disable-dependency-tracking -checking for a BSD-compatible install... /usr/bin/install -c -checking for x86_64-redhat-linux-gnu-pkg-config... no -checking for pkg-config... /usr/bin/pkg-config -checking pkg-config is at least version 0.9.0... yes -checking for dmd... dmd -checking version of D compiler... 2.087.0 -checking for curl... yes -checking for sqlite... yes -configure: creating ./config.status -config.status: creating Makefile -config.status: creating contrib/pacman/PKGBUILD -config.status: creating contrib/spec/onedrive.spec -config.status: creating onedrive.1 -config.status: creating contrib/systemd/onedrive.service -config.status: creating contrib/systemd/onedrive@.service -configure: WARNING: unrecognized options: --disable-dependency-tracking -+ make -if [ -f .git/HEAD ] ; then \ - git describe --tags > version ; \ -else \ - echo v2.4.15 > version ; \ -fi -dmd -w -g -O -J. -L-lcurl -L-lsqlite3 -L-ldl src/config.d src/itemdb.d src/log.d src/main.d src/monitor.d src/onedrive.d src/qxor.d src/selective.d src/sqlite.d src/sync.d src/upload.d src/util.d src/progress.d src/arsd/cgi.d -ofonedrive -+ exit 0 -Executing(%install): /bin/sh -e /var/tmp/rpm-tmp.L3JbHy -+ umask 022 -+ cd /home/alex/rpmbuild/BUILD -+ '[' /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64 '!=' / ']' -+ rm -rf /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64 -++ dirname /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64 -+ mkdir -p /home/alex/rpmbuild/BUILDROOT -+ mkdir /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64 -+ cd onedrive-2.4.15 -+ /usr/bin/make install DESTDIR=/home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64 PREFIX=/home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64 -/usr/bin/install -c -D onedrive /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64/usr/bin/onedrive -/usr/bin/install -c -D onedrive.1 /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64/usr/share/man/man1/onedrive.1 -/usr/bin/install -c -D -m 644 contrib/logrotate/onedrive.logrotate /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64/etc/logrotate.d/onedrive -mkdir -p /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64/usr/share/doc/onedrive -/usr/bin/install -c -D -m 644 README.md config LICENSE CHANGELOG.md docs/Docker.md docs/INSTALL.md docs/SharePoint-Shared-Libraries.md docs/USAGE.md docs/BusinessSharedFolders.md docs/advanced-usage.md /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64/usr/share/doc/onedrive -/usr/bin/install -c -d -m 0755 /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64/usr/lib/systemd/user /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64/usr/lib/systemd/system -/usr/bin/install -c -m 0644 contrib/systemd/onedrive@.service /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64/usr/lib/systemd/system -/usr/bin/install -c -m 0644 contrib/systemd/onedrive.service /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64/usr/lib/systemd/system -+ /usr/lib/rpm/check-buildroot -+ /usr/lib/rpm/redhat/brp-compress -+ /usr/lib/rpm/redhat/brp-strip /usr/bin/strip -+ /usr/lib/rpm/redhat/brp-strip-comment-note /usr/bin/strip /usr/bin/objdump -+ /usr/lib/rpm/redhat/brp-strip-static-archive /usr/bin/strip -+ /usr/lib/rpm/brp-python-bytecompile /usr/bin/python 1 -+ /usr/lib/rpm/redhat/brp-python-hardlink -+ /usr/lib/rpm/redhat/brp-java-repack-jars -Processing files: onedrive-2.4.15-1.el7.x86_64 -Executing(%doc): /bin/sh -e /var/tmp/rpm-tmp.cpSXho -+ umask 022 -+ cd /home/alex/rpmbuild/BUILD -+ cd onedrive-2.4.15 -+ DOCDIR=/home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64/usr/share/doc/onedrive-2.4.15 -+ export DOCDIR -+ /usr/bin/mkdir -p /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64/usr/share/doc/onedrive-2.4.15 -+ cp -pr README.md /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64/usr/share/doc/onedrive-2.4.15 -+ cp -pr LICENSE /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64/usr/share/doc/onedrive-2.4.15 -+ cp -pr CHANGELOG.md /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64/usr/share/doc/onedrive-2.4.15 -+ exit 0 -Provides: config(onedrive) = 2.4.15-1.el7 onedrive = 2.4.15-1.el7 onedrive(x86-64) = 2.4.15-1.el7 -Requires(rpmlib): rpmlib(CompressedFileNames) <= 3.0.4-1 rpmlib(FileDigests) <= 4.6.0-1 rpmlib(PayloadFilesHavePrefix) <= 4.0-1 -Requires(post): systemd -Requires(preun): systemd -Requires(postun): systemd -Requires: ld-linux-x86-64.so.2()(64bit) ld-linux-x86-64.so.2(GLIBC_2.3)(64bit) libc.so.6()(64bit) libc.so.6(GLIBC_2.14)(64bit) libc.so.6(GLIBC_2.15)(64bit) libc.so.6(GLIBC_2.2.5)(64bit) libc.so.6(GLIBC_2.3.2)(64bit) libc.so.6(GLIBC_2.3.4)(64bit) libc.so.6(GLIBC_2.4)(64bit) libc.so.6(GLIBC_2.6)(64bit) libc.so.6(GLIBC_2.8)(64bit) libc.so.6(GLIBC_2.9)(64bit) libcurl.so.4()(64bit) libdl.so.2()(64bit) libdl.so.2(GLIBC_2.2.5)(64bit) libgcc_s.so.1()(64bit) libgcc_s.so.1(GCC_3.0)(64bit) libgcc_s.so.1(GCC_4.2.0)(64bit) libm.so.6()(64bit) libm.so.6(GLIBC_2.2.5)(64bit) libpthread.so.0()(64bit) libpthread.so.0(GLIBC_2.2.5)(64bit) libpthread.so.0(GLIBC_2.3.2)(64bit) libpthread.so.0(GLIBC_2.3.4)(64bit) librt.so.1()(64bit) librt.so.1(GLIBC_2.2.5)(64bit) libsqlite3.so.0()(64bit) rtld(GNU_HASH) -Checking for unpackaged file(s): /usr/lib/rpm/check-files /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el7.x86_64 -Wrote: /home/alex/rpmbuild/SRPMS/onedrive-2.4.15-1.el7.src.rpm -Wrote: /home/alex/rpmbuild/RPMS/x86_64/onedrive-2.4.15-1.el7.x86_64.rpm -Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.nWoW33 -+ umask 022 -+ cd /home/alex/rpmbuild/BUILD -+ cd onedrive-2.4.15 -+ exit 0 -[alex@localhost ~]$ sudo yum -y install /home/alex/rpmbuild/RPMS/x86_64/onedrive-2.4.15-1.el7.x86_64.rpm -Loaded plugins: fastestmirror -Examining /home/alex/rpmbuild/RPMS/x86_64/onedrive-2.4.15-1.el7.x86_64.rpm: onedrive-2.4.15-1.el7.x86_64 -Marking /home/alex/rpmbuild/RPMS/x86_64/onedrive-2.4.15-1.el7.x86_64.rpm to be installed -Resolving Dependencies ---> Running transaction check ----> Package onedrive.x86_64 0:2.4.15-1.el7 will be installed ---> Finished Dependency Resolution - -Dependencies Resolved - -============================================================================================================================================================================================== - Package Arch Version Repository Size -============================================================================================================================================================================================== -Installing: - onedrive x86_64 2.4.15-1.el7 /onedrive-2.4.15-1.el7.x86_64 7.2 M - -Transaction Summary -============================================================================================================================================================================================== -Install 1 Package - -Total size: 7.2 M -Installed size: 7.2 M -Downloading packages: -Running transaction check -Running transaction test -Transaction test succeeded -Running transaction - Installing : onedrive-2.4.15-1.el7.x86_64 1/1 - Verifying : onedrive-2.4.15-1.el7.x86_64 1/1 - -Installed: - onedrive.x86_64 0:2.4.15-1.el7 - -Complete! -[alex@localhost ~]$ which onedrive -/usr/bin/onedrive -[alex@localhost ~]$ onedrive --version -onedrive v2.4.15 -[alex@localhost ~]$ onedrive --display-config -onedrive version = v2.4.15 -Config path = /home/alex/.config/onedrive -Config file found in config path = false -Config option 'check_nosync' = false -Config option 'sync_dir' = /home/alex/OneDrive -Config option 'skip_dir' = -Config option 'skip_file' = ~*|.~*|*.tmp -Config option 'skip_dotfiles' = false -Config option 'skip_symlinks' = false -Config option 'monitor_interval' = 300 -Config option 'min_notify_changes' = 5 -Config option 'log_dir' = /var/log/onedrive/ -Config option 'classify_as_big_delete' = 1000 -Config option 'upload_only' = false -Config option 'no_remote_delete' = false -Config option 'remove_source_files' = false -Config option 'sync_root_files' = false -Selective sync 'sync_list' configured = false -Business Shared Folders configured = false -[alex@localhost ~]$ -``` - -### CentOS 8 -```text -[alex@localhost ~]$ rpmbuild -ba ~/rpmbuild/SPECS/onedrive.spec -Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.UINFyE -+ umask 022 -+ cd /home/alex/rpmbuild/BUILD -+ cd /home/alex/rpmbuild/BUILD -+ rm -rf onedrive-2.4.15 -+ /usr/bin/gzip -dc /home/alex/rpmbuild/SOURCES/v2.4.15.tar.gz -+ /usr/bin/tar -xof - -+ STATUS=0 -+ '[' 0 -ne 0 ']' -+ cd onedrive-2.4.15 -+ /usr/bin/chmod -Rf a+rX,u+w,g-w,o-w . -+ exit 0 -Executing(%build): /bin/sh -e /var/tmp/rpm-tmp.cX1WQa -+ umask 022 -+ cd /home/alex/rpmbuild/BUILD -+ cd onedrive-2.4.15 -+ CFLAGS='-O2 -g -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector-strong -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection' -+ export CFLAGS -+ CXXFLAGS='-O2 -g -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector-strong -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection' -+ export CXXFLAGS -+ FFLAGS='-O2 -g -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector-strong -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -I/usr/lib64/gfortran/modules' -+ export FFLAGS -+ FCFLAGS='-O2 -g -pipe -Wall -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wp,-D_GLIBCXX_ASSERTIONS -fexceptions -fstack-protector-strong -grecord-gcc-switches -specs=/usr/lib/rpm/redhat/redhat-hardened-cc1 -specs=/usr/lib/rpm/redhat/redhat-annobin-cc1 -m64 -mtune=generic -fasynchronous-unwind-tables -fstack-clash-protection -fcf-protection -I/usr/lib64/gfortran/modules' -+ export FCFLAGS -+ LDFLAGS='-Wl,-z,relro -Wl,-z,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld' -+ export LDFLAGS -+ '[' 1 = 1 ']' -+++ dirname ./configure -++ find . -name config.guess -o -name config.sub -+ '[' 1 = 1 ']' -+ '[' x '!=' 'x-Wl,-z,now -specs=/usr/lib/rpm/redhat/redhat-hardened-ld' ']' -++ find . -name ltmain.sh -+ ./configure --build=x86_64-redhat-linux-gnu --host=x86_64-redhat-linux-gnu --program-prefix= --disable-dependency-tracking --prefix=/usr --exec-prefix=/usr --bindir=/usr/bin --sbindir=/usr/sbin --sysconfdir=/etc --datadir=/usr/share --includedir=/usr/include --libdir=/usr/lib64 --libexecdir=/usr/libexec --localstatedir=/var --sharedstatedir=/var/lib --mandir=/usr/share/man --infodir=/usr/share/info -configure: WARNING: unrecognized options: --disable-dependency-tracking -checking for a BSD-compatible install... /usr/bin/install -c -checking for x86_64-redhat-linux-gnu-pkg-config... /usr/bin/x86_64-redhat-linux-gnu-pkg-config -checking pkg-config is at least version 0.9.0... yes -checking for dmd... dmd -checking version of D compiler... 2.087.0 -checking for curl... yes -checking for sqlite... yes -configure: creating ./config.status -config.status: creating Makefile -config.status: creating contrib/pacman/PKGBUILD -config.status: creating contrib/spec/onedrive.spec -config.status: creating onedrive.1 -config.status: creating contrib/systemd/onedrive.service -config.status: creating contrib/systemd/onedrive@.service -configure: WARNING: unrecognized options: --disable-dependency-tracking -+ make -if [ -f .git/HEAD ] ; then \ - git describe --tags > version ; \ -else \ - echo v2.4.15 > version ; \ -fi -dmd -w -g -O -J. -L-lcurl -L-lsqlite3 -L-ldl src/config.d src/itemdb.d src/log.d src/main.d src/monitor.d src/onedrive.d src/qxor.d src/selective.d src/sqlite.d src/sync.d src/upload.d src/util.d src/progress.d src/arsd/cgi.d -ofonedrive -+ exit 0 -Executing(%install): /bin/sh -e /var/tmp/rpm-tmp.dNFPdx -+ umask 022 -+ cd /home/alex/rpmbuild/BUILD -+ '[' /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64 '!=' / ']' -+ rm -rf /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64 -++ dirname /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64 -+ mkdir -p /home/alex/rpmbuild/BUILDROOT -+ mkdir /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64 -+ cd onedrive-2.4.15 -+ /usr/bin/make install DESTDIR=/home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64 'INSTALL=/usr/bin/install -p' PREFIX=/home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64 -/usr/bin/install -p -D onedrive /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/usr/bin/onedrive -/usr/bin/install -p -D onedrive.1 /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/usr/share/man/man1/onedrive.1 -/usr/bin/install -p -D -m 644 contrib/logrotate/onedrive.logrotate /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/etc/logrotate.d/onedrive -mkdir -p /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/usr/share/doc/onedrive -/usr/bin/install -p -D -m 644 README.md config LICENSE CHANGELOG.md docs/Docker.md docs/INSTALL.md docs/SharePoint-Shared-Libraries.md docs/USAGE.md docs/BusinessSharedFolders.md docs/advanced-usage.md /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/usr/share/doc/onedrive -/usr/bin/install -p -d -m 0755 /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/usr/lib/systemd/user /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/usr/lib/systemd/system -/usr/bin/install -p -m 0644 contrib/systemd/onedrive@.service /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/usr/lib/systemd/system -/usr/bin/install -p -m 0644 contrib/systemd/onedrive.service /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/usr/lib/systemd/system -+ /usr/lib/rpm/check-buildroot -+ /usr/lib/rpm/redhat/brp-ldconfig -/sbin/ldconfig: Warning: ignoring configuration file that cannot be opened: /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/etc/ld.so.conf: No such file or directory -+ /usr/lib/rpm/brp-compress -+ /usr/lib/rpm/brp-strip /usr/bin/strip -+ /usr/lib/rpm/brp-strip-comment-note /usr/bin/strip /usr/bin/objdump -+ /usr/lib/rpm/brp-strip-static-archive /usr/bin/strip -+ /usr/lib/rpm/brp-python-bytecompile 1 -+ /usr/lib/rpm/brp-python-hardlink -+ PYTHON3=/usr/libexec/platform-python -+ /usr/lib/rpm/redhat/brp-mangle-shebangs -Processing files: onedrive-2.4.15-1.el8.x86_64 -Executing(%doc): /bin/sh -e /var/tmp/rpm-tmp.TnFKbZ -+ umask 022 -+ cd /home/alex/rpmbuild/BUILD -+ cd onedrive-2.4.15 -+ DOCDIR=/home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/usr/share/doc/onedrive -+ export LC_ALL=C -+ LC_ALL=C -+ export DOCDIR -+ /usr/bin/mkdir -p /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/usr/share/doc/onedrive -+ cp -pr README.md /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/usr/share/doc/onedrive -+ cp -pr LICENSE /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/usr/share/doc/onedrive -+ cp -pr CHANGELOG.md /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64/usr/share/doc/onedrive -+ exit 0 -warning: File listed twice: /usr/share/doc/onedrive -warning: File listed twice: /usr/share/doc/onedrive/CHANGELOG.md -warning: File listed twice: /usr/share/doc/onedrive/LICENSE -warning: File listed twice: /usr/share/doc/onedrive/README.md -Provides: config(onedrive) = 2.4.15-1.el8 onedrive = 2.4.15-1.el8 onedrive(x86-64) = 2.4.15-1.el8 -Requires(rpmlib): rpmlib(CompressedFileNames) <= 3.0.4-1 rpmlib(FileDigests) <= 4.6.0-1 rpmlib(PayloadFilesHavePrefix) <= 4.0-1 -Requires(post): systemd -Requires(preun): systemd -Requires(postun): systemd -Requires: ld-linux-x86-64.so.2()(64bit) ld-linux-x86-64.so.2(GLIBC_2.3)(64bit) libc.so.6()(64bit) libc.so.6(GLIBC_2.14)(64bit) libc.so.6(GLIBC_2.15)(64bit) libc.so.6(GLIBC_2.2.5)(64bit) libc.so.6(GLIBC_2.3.2)(64bit) libc.so.6(GLIBC_2.3.4)(64bit) libc.so.6(GLIBC_2.4)(64bit) libc.so.6(GLIBC_2.6)(64bit) libc.so.6(GLIBC_2.8)(64bit) libc.so.6(GLIBC_2.9)(64bit) libcurl.so.4()(64bit) libdl.so.2()(64bit) libdl.so.2(GLIBC_2.2.5)(64bit) libgcc_s.so.1()(64bit) libgcc_s.so.1(GCC_3.0)(64bit) libgcc_s.so.1(GCC_4.2.0)(64bit) libm.so.6()(64bit) libm.so.6(GLIBC_2.2.5)(64bit) libpthread.so.0()(64bit) libpthread.so.0(GLIBC_2.2.5)(64bit) libpthread.so.0(GLIBC_2.3.2)(64bit) libpthread.so.0(GLIBC_2.3.4)(64bit) librt.so.1()(64bit) librt.so.1(GLIBC_2.2.5)(64bit) libsqlite3.so.0()(64bit) rtld(GNU_HASH) -Checking for unpackaged file(s): /usr/lib/rpm/check-files /home/alex/rpmbuild/BUILDROOT/onedrive-2.4.15-1.el8.x86_64 -Wrote: /home/alex/rpmbuild/SRPMS/onedrive-2.4.15-1.el8.src.rpm -Wrote: /home/alex/rpmbuild/RPMS/x86_64/onedrive-2.4.15-1.el8.x86_64.rpm -Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.FAMTFz -+ umask 022 -+ cd /home/alex/rpmbuild/BUILD -+ cd onedrive-2.4.15 -+ exit 0 -[alex@localhost ~]$ sudo yum -y install /home/alex/rpmbuild/RPMS/x86_64/onedrive-2.4.15-1.el8.x86_64.rpm -Last metadata expiration check: 0:04:07 ago on Fri 14 Jan 2022 14:22:13 EST. -Dependencies resolved. -============================================================================================================================================================================================== - Package Architecture Version Repository Size -============================================================================================================================================================================================== -Installing: - onedrive x86_64 2.4.15-1.el8 @commandline 1.5 M - -Transaction Summary -============================================================================================================================================================================================== -Install 1 Package - -Total size: 1.5 M -Installed size: 7.1 M -Downloading Packages: -Running transaction check -Transaction check succeeded. -Running transaction test -Transaction test succeeded. -Running transaction - Preparing : 1/1 - Installing : onedrive-2.4.15-1.el8.x86_64 1/1 - Running scriptlet: onedrive-2.4.15-1.el8.x86_64 1/1 - Verifying : onedrive-2.4.15-1.el8.x86_64 1/1 - -Installed: - onedrive-2.4.15-1.el8.x86_64 - -Complete! -[alex@localhost ~]$ which onedrive -/usr/bin/onedrive -[alex@localhost ~]$ onedrive --version -onedrive v2.4.15 -[alex@localhost ~]$ onedrive --display-config -onedrive version = v2.4.15 -Config path = /home/alex/.config/onedrive -Config file found in config path = false -Config option 'check_nosync' = false -Config option 'sync_dir' = /home/alex/OneDrive -Config option 'skip_dir' = -Config option 'skip_file' = ~*|.~*|*.tmp -Config option 'skip_dotfiles' = false -Config option 'skip_symlinks' = false -Config option 'monitor_interval' = 300 -Config option 'min_notify_changes' = 5 -Config option 'log_dir' = /var/log/onedrive/ -Config option 'classify_as_big_delete' = 1000 -Config option 'upload_only' = false -Config option 'no_remote_delete' = false -Config option 'remove_source_files' = false -Config option 'sync_root_files' = false -Selective sync 'sync_list' configured = false -Business Shared Folders configured = false -[alex@localhost ~]$ -``` diff --git a/docs/images/SharedLibraries.jpg b/docs/images/SharedLibraries.jpg deleted file mode 100644 index 03c3995ab5d6bcbc07a2007e8f1e95b71350462b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17382 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 diff --git a/docs/images/application_registration_done.jpg b/docs/images/application_registration_done.jpg deleted file mode 100644 index e9559d606a0beffa6400a27a1d9cc38f520df159..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73999 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 J~(?Xwh~0x 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

DR3T3r$^wra?u{JPW@MbNg*@>h_20(7m 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 diff --git a/docs/images/business-files-on-demand.png b/docs/images/business-files-on-demand.png deleted file mode 100644 index ff78c20d20112f2f248b010d880d184d9a18fe26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15128 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 diff --git a/docs/images/confirmed_verified_publisher.jpg b/docs/images/confirmed_verified_publisher.jpg deleted file mode 100644 index 2720fbe94e374cc2510f75eb7ad2b6665dad789d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45819 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 diff --git a/docs/images/default_authentication_scopes.jpg b/docs/images/default_authentication_scopes.jpg deleted file mode 100644 index edccdddeae9fdf7c2c8f1f7af3a12e4835e13543..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88294 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 iN`Kl#{ja?6-1PtKMESqdQXu~?wHNHh#z4}zTl diff --git a/docs/known-issues.md b/docs/known-issues.md deleted file mode 100644 index 6d970ff9..00000000 --- a/docs/known-issues.md +++ /dev/null @@ -1,54 +0,0 @@ -# Known Issues -The below are known issues with this client: - -## Moving files into different folders should not cause data to delete and be re-uploaded -**Issue Tracker:** [#876](https://github.com/abraunegg/onedrive/issues/876) - -**Description:** - -When running the client in standalone mode (`--synchronize`) moving folders that are successfully synced around between subsequent standalone syncs causes a deletion & re-upload of data to occur. - -**Explanation:** - -Technically, the client is 'working' correctly, as, when moving files, you are 'deleting' them from the current location, but copying them to the 'new location'. As the client is running in standalone sync mode, there is no way to track what OS operations have been done when the client is not running - thus, this is why the 'delete and upload' is occurring. - -**Workaround:** - -If the tracking of moving data to new local directories is requried, it is better to run the client in service mode (`--monitor`) rather than in standalone mode, as the 'move' of files can then be handled at the point when it occurs, so that the data is moved to the new location on OneDrive without the need to be deleted and re-uploaded. - -## Application 'stops' running without any visible reason -**Issue Tracker:** [#494](https://github.com/abraunegg/onedrive/issues/494), [#753](https://github.com/abraunegg/onedrive/issues/753), [#792](https://github.com/abraunegg/onedrive/issues/792), [#884](https://github.com/abraunegg/onedrive/issues/884), [#1162](https://github.com/abraunegg/onedrive/issues/1162), [#1408](https://github.com/abraunegg/onedrive/issues/1408), [#1520](https://github.com/abraunegg/onedrive/issues/1520), [#1526](https://github.com/abraunegg/onedrive/issues/1526) - -**Description:** - -When running the client and performing an upload or download operation, the application just stops working without any reason or explanation. If `echo $?` is used after the application has exited without visible reason, an error level of 141 may be provided. - -Additionally, this issue has mainly been seen when the client is operating against Microsoft's Europe Data Centre's. - -**Explanation:** - -The client is heavily dependant on Curl and OpenSSL to perform the activities with the Microsoft OneDrive service. Generally, when this issue occurs, the following is found in the HTTPS Debug Log: -``` -OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 104 -``` -The only way to determine that this is the cause of the application ceasing to work is to generate a HTTPS debug log using the following additional flags: -``` ---verbose --verbose --debug-https -``` - -This is indicative of the following: -* Some sort of flaky Internet connection somewhere between you and the OneDrive service -* Some sort of 'broken' HTTPS transparent inspection service inspecting your traffic somewhere between you and the OneDrive service - -**How to resolve:** - -The best avenue of action here are: -* Ensure your OS is as up-to-date as possible -* Get support from your OS vendor -* Speak to your ISP or Help Desk for assistance -* Open a ticket with OpenSSL and/or Curl teams to better handle this sort of connection failure -* Generate a HTTPS Debug Log for this application and open a new support request with Microsoft and provide the debug log file for their analysis. - -If you wish to diagnose this issue further, refer to the following: - -https://maulwuff.de/research/ssl-debugging.html diff --git a/docs/national-cloud-deployments.md b/docs/national-cloud-deployments.md deleted file mode 100644 index 6b348388..00000000 --- a/docs/national-cloud-deployments.md +++ /dev/null @@ -1,145 +0,0 @@ -# How to configure access to specific Microsoft Azure deployments -## Application Version -Before reading this document, please ensure you are running application version [![Version](https://img.shields.io/github/v/release/abraunegg/onedrive)](https://github.com/abraunegg/onedrive/releases) or greater. Use `onedrive --version` to determine what application version you are using and upgrade your client if required. - -## Process Overview -In some cases it is a requirement to utilise specific Microsoft Azure cloud deployments to conform with data and security reuqirements that requires data to reside within the geographic borders of that country. -Current national clouds that are supported are: -* Microsoft Cloud for US Government -* Microsoft Cloud Germany -* Azure and Office365 operated by 21Vianet in China - -In order to successfully use these specific Microsoft Azure deployments, the following steps are required: -1. Register an application with the Microsoft identity platform using the Azure portal -2. Configure the new application with the appropriate authentication scopes -3. Validate that the authentication / redirect URI is correct for your application registration -4. Configure the onedrive client to use the new application id as provided during application registration -5. Configure the onedrive client to use the right Microsoft Azure deployment region that your application was registered with -6. Authenticate the client - -## Step 1: Register a new application with Microsoft Azure -1. Log into your applicable Microsoft Azure Portal with your applicable Office365 identity: - -| National Cloud Environment | Microsoft Azure Portal | -|---|---| -| Microsoft Cloud for US Government | https://portal.azure.com/ | -| Microsoft Cloud Germany | https://portal.azure.com/ | -| Azure and Office365 operated by 21Vianet | https://portal.azure.cn/ | - -2. Select 'Azure Active Directory' as the service you wish to configure -3. Under 'Manage', select 'App registrations' to register a new application -4. Click 'New registration' -5. Type in the appropriate details required as per below: - -![application_registration](./images/application_registration.jpg) - -6. To save the application registration, click 'Register' and something similar to the following will be displayed: - -![application_registration_done](./images/application_registration_done.jpg) - -**Note:** The Application (client) ID UUID as displayed after client registration, is what is required as the 'application_id' for Step 4 below. - -## Step 2: Configure application authentication scopes -Configure the API permissions as per the following: - -| API / Permissions name | Type | Description | Admin consent required | -|---|---|---|---| -| Files.ReadWrite | Delegated | Have full access to user files | No | -| Files.ReadWrite.All | Delegated | Have full access to all files user can access | No | -| Sites.ReadWrite.All | Delegated | Have full access to all items in all site collections | No | -| offline_access | Delegated | Maintain access to data you have given it access to | No | - -![authentication_scopes](./images/authentication_scopes.jpg) - -## Step 3: Validate that the authentication / redirect URI is correct -Add the appropriate redirect URI for your Azure deployment: - -![authentication_response_uri](./images/authentication_response_uri.jpg) - -A valid entry for the response URI should be one of: -* https://login.microsoftonline.us/common/oauth2/nativeclient (Microsoft Cloud for US Government) -* https://login.microsoftonline.de/common/oauth2/nativeclient (Microsoft Cloud Germany) -* https://login.chinacloudapi.cn/common/oauth2/nativeclient (Azure and Office365 operated by 21Vianet in China) - -For a single-tenant application, it may be necessary to use your specific tenant id instead of "common": -* https://login.microsoftonline.us/example.onmicrosoft.us/oauth2/nativeclient (Microsoft Cloud for US Government) -* https://login.microsoftonline.de/example.onmicrosoft.de/oauth2/nativeclient (Microsoft Cloud Germany) -* https://login.chinacloudapi.cn/example.onmicrosoft.cn/oauth2/nativeclient (Azure and Office365 operated by 21Vianet in China) - -## Step 4: Configure the onedrive client to use new application registration -Update to your 'onedrive' configuration file (`~/.config/onedrive/config`) the following: -```text -application_id = "insert valid entry here" -``` - -This will reconfigure the client to use the new application registration you have created. - -**Example:** -```text -application_id = "22c49a0d-d21c-4792-aed1-8f163c982546" -``` - -## Step 5: Configure the onedrive client to use the specific Microsoft Azure deployment -Update to your 'onedrive' configuration file (`~/.config/onedrive/config`) the following: -```text -azure_ad_endpoint = "insert valid entry here" -``` - -Valid entries are: -* USL4 (Microsoft Cloud for US Government) -* USL5 (Microsoft Cloud for US Government - DOD) -* DE (Microsoft Cloud Germany) -* CN (Azure and Office365 operated by 21Vianet in China) - -This will configure your client to use the correct Azure AD and Graph endpoints as per [https://docs.microsoft.com/en-us/graph/deployments](https://docs.microsoft.com/en-us/graph/deployments) - -**Example:** -```text -azure_ad_endpoint = "USL4" -``` - -If the Microsoft Azure deployment does not support multi-tenant applications, update to your 'onedrive' configuration file (`~/.config/onedrive/config`) the following: -```text -azure_tenant_id = "insert valid entry here" -``` - -This will configure your client to use the specified tenant id in its Azure AD and Graph endpoint URIs, instead of "common". -The tenant id may be the GUID Directory ID (formatted "00000000-0000-0000-0000-000000000000"), or the fully qualified tenant name (e.g. "example.onmicrosoft.us"). -The GUID Directory ID may be located in the Azure administation page as per [https://docs.microsoft.com/en-us/onedrive/find-your-office-365-tenant-id](https://docs.microsoft.com/en-us/onedrive/find-your-office-365-tenant-id). Note that you may need to go to your national-deployment-specific administration page, rather than following the links within that document. -The tenant name may be obtained by following the PowerShell instructions on [https://docs.microsoft.com/en-us/onedrive/find-your-office-365-tenant-id](https://docs.microsoft.com/en-us/onedrive/find-your-office-365-tenant-id); it is shown as the "TenantDomain" upon completion of the "Connect-AzureAD" command. - -**Example:** -```text -azure_tenant_id = "example.onmicrosoft.us" -# or -azure_tenant_id = "0c4be462-a1ab-499b-99e0-da08ce52a2cc" -``` - -## Step 6: Authenticate the client -Run the application without any additional command switches. - -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 - -Authorize this app visiting: - -https://..... - -Enter the response uri: - -``` - -**Example:** -``` -[user@hostname ~]$ onedrive -Authorize this app visiting: - -https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=22c49a0d-d21c-4792-aed1-8f163c982546&scope=Files.ReadWrite%20Files.ReadWrite.all%20Sites.ReadWrite.All%20offline_access&response_type=code&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient - -Enter the response uri: https://login.microsoftonline.com/common/oauth2/nativeclient?code= - -Application has been successfully authorised, however no additional command switches were provided. - -Please use --help for further assistance in regards to running this application. -``` diff --git a/docs/privacy-policy.md b/docs/privacy-policy.md deleted file mode 100644 index 64fe1dd3..00000000 --- a/docs/privacy-policy.md +++ /dev/null @@ -1,65 +0,0 @@ -# Privacy Policy -Effective Date: May 16 2018 - -## Introduction - -This Privacy Policy outlines how OneDrive Client for Linux ("we," "our," or "us") collects, uses, and protects information when you use our software ("OneDrive Client for Linux"). We respect your privacy and are committed to ensuring the confidentiality and security of any information you provide while using the Software. - -## Information We Do Not Collect - -We want to be transparent about the fact that we do not collect any personal data, usage data, or tracking data through the Software. This means: - -1. **No Personal Data**: We do not collect any information that can be used to personally identify you, such as your name, email address, phone number, or physical address. - -2. **No Usage Data**: We do not collect data about how you use the Software, such as the features you use, the duration of your sessions, or any interactions within the Software. - -3. **No Tracking Data**: We do not use cookies or similar tracking technologies to monitor your online behavior or track your activities across websites or apps. - -## How We Use Your Information - -Since we do not collect any personal, usage, or tracking data, there is no information for us to use for any purpose. - -## Third-Party Services - -The Software may include links to third-party websites or services, but we do not have control over the privacy practices or content of these third-party services. We encourage you to review the privacy policies of any third-party services you access through the Software. - -## Children's Privacy - -Since we do not collect any personal, usage, or tracking data, there is no restriction on the use of this application by anyone under the age of 18. - -## Information You Choose to Share - -While we do not collect personal data, usage data, or tracking data through the Software, there may be instances where you voluntarily choose to share information with us, particularly when submitting bug reports. These bug reports may contain sensitive information such as account details, file names, and directory names. It's important to note that these details are included in the logs and debug logs solely for the purpose of diagnosing and resolving technical issues with the Software. - -We want to emphasize that, even in these cases, we do not have access to your actual data. The logs and debug logs provided in bug reports are used exclusively for technical troubleshooting and debugging purposes. We take measures to treat this information with the utmost care, and it is only accessible to our technical support and development teams. We do not use this information for any other purpose, and we have strict security measures in place to protect it. - -## Protecting Your Sensitive Data - -We are committed to safeguarding your sensitive data and maintaining its confidentiality. To ensure its protection: - -1. **Limited Access**: Only authorized personnel within our technical support and development teams have access to the logs and debug logs containing sensitive data, and they are trained in handling this information securely. - -2. **Data Encryption**: We use industry-standard encryption protocols to protect the transmission and storage of sensitive data. - -3. **Data Retention**: We retain bug report data for a limited time necessary for resolving the reported issue. Once the issue is resolved, we promptly delete or anonymize the data. - -4. **Security Measures**: We employ robust security measures to prevent unauthorized access, disclosure, or alteration of sensitive data. - -By submitting a bug report, you acknowledge and consent to the inclusion of sensitive information in logs and debug logs for the sole purpose of addressing technical issues with the Software. - -## Your Responsibilities - -While we take measures to protect your sensitive data, it is essential for you to exercise caution when submitting bug reports. Please refrain from including any sensitive or personally identifiable information that is not directly related to the technical issue you are reporting. You have the option to redact or obfuscate sensitive details in bug reports to further protect your data. - -## Changes to this Privacy Policy - -We may update this Privacy Policy from time to time to reflect changes in our practices or for other operational, legal, or regulatory reasons. We will notify you of any material changes by posting the updated Privacy Policy on our website or through the Software. We encourage you to review this Privacy Policy periodically. - -## Contact Us - -If you have any questions or concerns about this Privacy Policy or our privacy practices, please contact us at support@mynas.com.au or via GitHub (https://github.com/abraunegg/onedrive) - -## Conclusion - -By using the Software, you agree to the terms outlined in this Privacy Policy. If you do not agree with any part of this policy, please discontinue the use of the Software. - diff --git a/docs/terms-of-service.md b/docs/terms-of-service.md deleted file mode 100644 index cdf7c432..00000000 --- a/docs/terms-of-service.md +++ /dev/null @@ -1,54 +0,0 @@ -# OneDrive Client for Linux - Software Service Terms of Service - -## 1. Introduction - -These Terms of Service ("Terms") govern your use of the OneDrive Client for Linux ("Application") software and related Microsoft OneDrive services ("Service") provided by Microsoft. By accessing or using the Service, you agree to comply with and be bound by these Terms. If you do not agree to these Terms, please do not use the Service. - -## 2. License Compliance - -The OneDrive Client for Linux software is licensed under the GNU General Public License, version 3.0 (the "GPLv3"). Your use of the software must comply with the terms and conditions of the GPLv3. A copy of the GPLv3 can be found here: https://www.gnu.org/licenses/gpl-3.0.en.html - -## 3. Use of the Service - -### 3.1. Access and Accounts - -You may need to create an account or provide personal information to access certain features of the Service. You are responsible for maintaining the confidentiality of your account information and are solely responsible for all activities that occur under your account. - -### 3.2. Prohibited Activities - -You agree not to: - -- Use the Service in any way that violates applicable laws or regulations. -- Use the Service to engage in any unlawful, harmful, or fraudulent activity. -- Use the Service in any manner that disrupts, damages, or impairs the Service. - -## 4. Intellectual Property - -The OneDrive Client for Linux software is subject to the GPLv3, and you must respect all copyrights, trademarks, and other intellectual property rights associated with the software. Any contributions you make to the software must also comply with the GPLv3. - -## 5. Disclaimer of Warranties - -The OneDrive Client for Linux software is provided "as is" without any warranties, either expressed or implied. We do not guarantee that the use of the Application will be error-free or uninterrupted. - -Microsoft is not responsible for OneDrive Client for Linux. Any issues or problems with OneDrive Client for Linux should be raised on GitHub at https://github.com/abraunegg/onedrive or email support@mynas.com.au - -OneDrive Client for Linux is not responsible for the Microsoft OneDrive Service or the Microsoft Graph API Service that this Application utilizes. Any issue with either Microsoft OneDrive or Microsoft Graph API should be raised with Microsoft via their support channel in your country. - -## 6. Limitation of Liability - -To the fullest extent permitted by law, we shall not be liable for any direct, indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses, resulting from (a) your use or inability to use the Service, or (b) any other matter relating to the Service. - -This limitiation of liability explicitly relates to the use of the OneDrive Client for Linux software and does not affect your rights under the GPLv3. - -## 7. Changes to Terms - -We reserve the right to update or modify these Terms at any time without prior notice. Any changes will be effective immediately upon posting on GitHub. Your continued use of the Service after the posting of changes constitutes your acceptance of such changes. Changes can be reviewed on GitHub. - -## 8. Governing Law - -These Terms shall be governed by and construed in accordance with the laws of Australia, without regard to its conflict of law principles. - -## 9. Contact Us - -If you have any questions or concerns about these Terms, please contact us at https://github.com/abraunegg/onedrive or email support@mynas.com.au - diff --git a/docs/ubuntu-package-install.md b/docs/ubuntu-package-install.md deleted file mode 100644 index 292a822c..00000000 --- a/docs/ubuntu-package-install.md +++ /dev/null @@ -1,414 +0,0 @@ -# Installation of 'onedrive' package on Debian and Ubuntu - -This document covers the appropriate steps to install the 'onedrive' client using the provided packages for Debian and Ubuntu. - -#### Important information for all Ubuntu and Ubuntu based distribution users: -This information is specifically for the following platforms and distributions: - -* Lubuntu -* Linux Mint -* POP OS -* Peppermint OS -* Raspbian -* Ubuntu - -Whilst there are [onedrive](https://packages.ubuntu.com/search?keywords=onedrive&searchon=names&suite=all§ion=all) Universe packages available for Ubuntu, do not install 'onedrive' from these Universe packages. The default Ubuntu Universe packages are out-of-date and are not supported and should not be used. - -## Determine which instructions to use -Ubuntu and its clones are based on various different releases, thus, you must use the correct instructions below, otherwise you may run into package dependancy issues and will be unable to install the client. - -### Step 1: Remove any configured PPA and associated 'onedrive' package and systemd service files -Many Internet 'help' pages provide inconsistent details on how to install the OneDrive Client for Linux. A number of these websites continue to point users to install the client via the yann1ck PPA repository however this PPA no longer exists and should not be used. - -To remove the PPA repository and the older client, perform the following actions: -```text -sudo apt remove onedrive -sudo add-apt-repository --remove ppa:yann1ck/onedrive -``` - -Additionally, Ubuntu and its clones have a bad habit of creating a 'default' systemd service file when installing the 'onedrive' package so that the client will automatically run the client post being authenticated. This systemd entry is erroneous and needs to be removed. -``` -Created symlink /etc/systemd/user/default.target.wants/onedrive.service → /usr/lib/systemd/user/onedrive.service. -``` -To remove this symbolic link, run the following command: -``` -sudo rm /etc/systemd/user/default.target.wants/onedrive.service -``` - -### Step 2: Ensure your system is up-to-date -Use a script, similar to the following to ensure your system is updated correctly: -```text -#!/bin/bash -rm -rf /var/lib/dpkg/lock-frontend -rm -rf /var/lib/dpkg/lock -apt-get update -apt-get upgrade -y -apt-get dist-upgrade -y -apt-get autoremove -y -apt-get autoclean -y -``` - -Run this script as 'root' by using `su -` to elevate to 'root'. Example below: -```text -Welcome to Ubuntu 20.04.1 LTS (GNU/Linux 5.4.0-48-generic x86_64) - - * Documentation: https://help.ubuntu.com - * Management: https://landscape.canonical.com - * Support: https://ubuntu.com/advantage - -425 updates can be installed immediately. -208 of these updates are security updates. -To see these additional updates run: apt list --upgradable - -Your Hardware Enablement Stack (HWE) is supported until April 2025. -Last login: Thu Jan 20 14:21:48 2022 from my.ip.address -alex@ubuntu-20-LTS:~$ su - -Password: -root@ubuntu-20-LTS:~# ls -la -total 28 -drwx------ 3 root root 4096 Oct 10 2020 . -drwxr-xr-x 20 root root 4096 Oct 10 2020 .. --rw------- 1 root root 175 Jan 20 14:23 .bash_history --rw-r--r-- 1 root root 3106 Dec 6 2019 .bashrc -drwx------ 2 root root 4096 Apr 23 2020 .cache --rw-r--r-- 1 root root 161 Dec 6 2019 .profile --rwxr-xr-x 1 root root 174 Oct 10 2020 update-os.sh -root@ubuntu-20-LTS:~# cat update-os.sh -#!/bin/bash -rm -rf /var/lib/dpkg/lock-frontend -rm -rf /var/lib/dpkg/lock -apt-get update -apt-get upgrade -y -apt-get dist-upgrade -y -apt-get autoremove -y -apt-get autoclean -y -root@ubuntu-20-LTS:~# ./update-os.sh -Hit:1 http://au.archive.ubuntu.com/ubuntu focal InRelease -Hit:2 http://au.archive.ubuntu.com/ubuntu focal-updates InRelease -Hit:3 http://au.archive.ubuntu.com/ubuntu focal-backports InRelease -Hit:4 http://security.ubuntu.com/ubuntu focal-security InRelease -Reading package lists... 96% -... -Sourcing file `/etc/default/grub' -Sourcing file `/etc/default/grub.d/init-select.cfg' -Generating grub configuration file ... -Found linux image: /boot/vmlinuz-5.13.0-27-generic -Found initrd image: /boot/initrd.img-5.13.0-27-generic -Found linux image: /boot/vmlinuz-5.4.0-48-generic -Found initrd image: /boot/initrd.img-5.4.0-48-generic -Found memtest86+ image: /boot/memtest86+.elf -Found memtest86+ image: /boot/memtest86+.bin -done -Removing linux-modules-5.4.0-26-generic (5.4.0-26.30) ... -Processing triggers for libc-bin (2.31-0ubuntu9.2) ... -Reading package lists... Done -Building dependency tree -Reading state information... Done -root@ubuntu-20-LTS:~# -``` - -Reboot your system after running this process before continuing with Step 3. -```text -reboot -``` - -### Step 3: Determine what your OS is based on -Determine what your OS is based on. To do this, run the following command: -```text -lsb_release -a -``` -**Example:** -```text -alex@ubuntu-system:~$ lsb_release -a -No LSB modules are available. -Distributor ID: Ubuntu -Description: Ubuntu 22.04 LTS -Release: 22.04 -Codename: jammy -``` - -### Step 4: Pick the correct instructions to use -If required, review the table below based on your 'lsb_release' information to pick the appropriate instructions to use: - -| Release & Codename | Instructions to use | -|--------------------|---------------------| -| Linux Mint 19.x | This platform is End-of-Life (EOL) and no longer supported. You must upgrade to Linux Mint 21.x | -| Linux Mint 20.x | Use [Ubuntu 20.04](#distribution-ubuntu-2004) instructions below | -| Linux Mint 21.x | Use [Ubuntu 22.04](#distribution-ubuntu-2204) instructions below | -| Linux Mint Debian Edition (LMDE) 5 / Elsie | Use [Debian 11](#distribution-debian-11) instructions below | -| Linux Mint Debian Edition (LMDE) 6 / Faye | Use [Debian 12](#distribution-debian-12) instructions below | -| Debian 9 | This platform is End-of-Life (EOL) and no longer supported. You must upgrade to Debian 12 | -| Debian 10 | You must build from source or upgrade your Operating System to Debian 12 | -| Debian 11 | Use [Debian 11](#distribution-debian-11) instructions below | -| Debian 12 | Use [Debian 12](#distribution-debian-12) instructions below | -| Raspbian GNU/Linux 10 | You must build from source or upgrade your Operating System to Raspbian GNU/Linux 12 | -| Raspbian GNU/Linux 11 | Use [Debian 11](#distribution-debian-11) instructions below | -| Raspbian GNU/Linux 12 | Use [Debian 12](#distribution-debian-12) instructions below | -| Ubuntu 18.04 / Bionic | This platform is End-of-Life (EOL) and no longer supported. You must upgrade to Ubuntu 22.04 | -| Ubuntu 20.04 / Focal | Use [Ubuntu 20.04](#distribution-ubuntu-2004) instructions below | -| Ubuntu 21.04 / Hirsute | Use [Ubuntu 21.04](#distribution-ubuntu-2104) instructions below | -| Ubuntu 21.10 / Impish | Use [Ubuntu 21.10](#distribution-ubuntu-2110) instructions below | -| Ubuntu 22.04 / Jammy | Use [Ubuntu 22.04](#distribution-ubuntu-2204) instructions below | -| Ubuntu 22.10 / Kinetic | Use [Ubuntu 22.10](#distribution-ubuntu-2210) instructions below | -| Ubuntu 23.04 / Lunar | Use [Ubuntu 23.04](#distribution-ubuntu-2304) instructions below | -| Ubuntu 23.10 / Mantic | Use [Ubuntu 23.10](#distribution-ubuntu-2310) instructions below | - -## Distribution Package Install Instructions - -### Distribution: Debian 11 -The packages support the following platform architectures: -|  i686  | x86_64 | ARMHF | AARCH64 | -|:----:|:------:|:-----:|:-------:| -|✔|✔|✔|✔| - -#### Step 1: Add the OpenSuSE Build Service repository release key -Add the OpenSuSE Build Service repository release key using the following command: -```text -wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/Debian_11/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null -``` - -#### Step 2: Add the OpenSuSE Build Service repository -Add the OpenSuSE Build Service repository using the following command: -```text -echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/Debian_11/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list -``` - -#### Step 3: Update your apt package cache -Run: `sudo apt-get update` - -#### Step 4: Install 'onedrive' -Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` - -#### Step 5: Read 'Known Issues' with these packages -Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. - -### Distribution: Debian 12 -The packages support the following platform architectures: -|  i686  | x86_64 | ARMHF | AARCH64 | -|:----:|:------:|:-----:|:-------:| -|✔|✔|✔|✔| - -#### Step 1: Add the OpenSuSE Build Service repository release key -Add the OpenSuSE Build Service repository release key using the following command: -```text -wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/Debian_12/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null -``` - -#### Step 2: Add the OpenSuSE Build Service repository -Add the OpenSuSE Build Service repository using the following command: -```text -echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/Debian_12/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list -``` - -#### Step 3: Update your apt package cache -Run: `sudo apt-get update` - -#### Step 4: Install 'onedrive' -Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` - -#### Step 5: Read 'Known Issues' with these packages -Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. - -### Distribution: Ubuntu 20.04 -The packages support the following platform architectures: -|  i686  | x86_64 | ARMHF | AARCH64 | -|:----:|:------:|:-----:|:-------:| -|❌|✔|✔|✔| - -#### Step 1: Add the OpenSuSE Build Service repository release key -Add the OpenSuSE Build Service repository release key using the following command: -```text -wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_20.04/Release.key | sudo apt-key add - -``` - -#### Step 2: Add the OpenSuSE Build Service repository -Add the OpenSuSE Build Service repository using the following command: -```text -echo 'deb https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_20.04/ ./' | sudo tee /etc/apt/sources.list.d/onedrive.list -``` - -#### Step 3: Update your apt package cache -Run: `sudo apt-get update` - -#### Step 4: Install 'onedrive' -Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` - -#### Step 5: Read 'Known Issues' with these packages -Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. - -### Distribution: Ubuntu 21.04 -The packages support the following platform architectures: -|  i686  | x86_64 | ARMHF | AARCH64 | -|:----:|:------:|:-----:|:-------:| -|❌|✔|✔|✔| - -#### Step 1: Add the OpenSuSE Build Service repository release key -Add the OpenSuSE Build Service repository release key using the following command: -```text -wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_21.04/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null -``` - -#### Step 2: Add the OpenSuSE Build Service repository -Add the OpenSuSE Build Service repository using the following command: -```text -echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_21.04/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list -``` - -#### Step 3: Update your apt package cache -Run: `sudo apt-get update` - -#### Step 4: Install 'onedrive' -Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` - -#### Step 5: Read 'Known Issues' with these packages -Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. - -### Distribution: Ubuntu 21.10 -The packages support the following platform architectures: -|  i686  | x86_64 | ARMHF | AARCH64 | -|:----:|:------:|:-----:|:-------:| -|❌|✔|✔|✔| - -#### Step 1: Add the OpenSuSE Build Service repository release key -Add the OpenSuSE Build Service repository release key using the following command: -```text -wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_21.10/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null -``` - -#### Step 2: Add the OpenSuSE Build Service repository -Add the OpenSuSE Build Service repository using the following command: -```text -echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_21.10/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list -``` - -#### Step 3: Update your apt package cache -Run: `sudo apt-get update` - -#### Step 4: Install 'onedrive' -Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` - -#### Step 5: Read 'Known Issues' with these packages -Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. - -### Distribution: Ubuntu 22.04 -The packages support the following platform architectures: -|  i686  | x86_64 | ARMHF | AARCH64 | -|:----:|:------:|:-----:|:-------:| -|❌|✔|✔|✔| - -#### Step 1: Add the OpenSuSE Build Service repository release key -Add the OpenSuSE Build Service repository release key using the following command: -```text -wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_22.04/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null -``` - -#### Step 2: Add the OpenSuSE Build Service repository -Add the OpenSuSE Build Service repository using the following command: -```text -echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_22.04/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list -``` - -#### Step 3: Update your apt package cache -Run: `sudo apt-get update` - -#### Step 4: Install 'onedrive' -Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` - -#### Step 5: Read 'Known Issues' with these packages -Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. - -### Distribution: Ubuntu 22.10 -The packages support the following platform architectures: -|  i686  | x86_64 | ARMHF | AARCH64 | -|:----:|:------:|:-----:|:-------:| -|❌|✔|✔|✔| - -#### Step 1: Add the OpenSuSE Build Service repository release key -Add the OpenSuSE Build Service repository release key using the following command: -```text -wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_22.10/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null -``` - -#### Step 2: Add the OpenSuSE Build Service repository -Add the OpenSuSE Build Service repository using the following command: -```text -echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_22.10/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list -``` - -#### Step 3: Update your apt package cache -Run: `sudo apt-get update` - -#### Step 4: Install 'onedrive' -Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` - -#### Step 5: Read 'Known Issues' with these packages -Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. - -### Distribution: Ubuntu 23.04 -The packages support the following platform architectures: -|  i686  | x86_64 | ARMHF | AARCH64 | -|:----:|:------:|:-----:|:-------:| -|❌|✔|✔|✔| - -#### Step 1: Add the OpenSuSE Build Service repository release key -Add the OpenSuSE Build Service repository release key using the following command: -```text -wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_23.04/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null -``` - -#### Step 2: Add the OpenSuSE Build Service repository -Add the OpenSuSE Build Service repository using the following command: -```text -echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_23.04/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list -``` - -#### Step 3: Update your apt package cache -Run: `sudo apt-get update` - -#### Step 4: Install 'onedrive' -Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` - -#### Step 5: Read 'Known Issues' with these packages -Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. - -### Distribution: Ubuntu 23.10 -The packages support the following platform architectures: -|  i686  | x86_64 | ARMHF | AARCH64 | -|:----:|:------:|:-----:|:-------:| -|❌|✔|❌|✔| - -#### Step 1: Add the OpenSuSE Build Service repository release key -Add the OpenSuSE Build Service repository release key using the following command: -```text -wget -qO - https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_23.10/Release.key | gpg --dearmor | sudo tee /usr/share/keyrings/obs-onedrive.gpg > /dev/null -``` - -#### Step 2: Add the OpenSuSE Build Service repository -Add the OpenSuSE Build Service repository using the following command: -```text -echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/obs-onedrive.gpg] https://download.opensuse.org/repositories/home:/npreining:/debian-ubuntu-onedrive/xUbuntu_23.10/ ./" | sudo tee /etc/apt/sources.list.d/onedrive.list -``` - -#### Step 3: Update your apt package cache -Run: `sudo apt-get update` - -#### Step 4: Install 'onedrive' -Run: `sudo apt install --no-install-recommends --no-install-suggests onedrive` - -#### Step 5: Read 'Known Issues' with these packages -Read and understand the [known issues](#known-issues-with-installing-from-the-above-packages) with these packages below, taking any action that is needed. - - -## Known Issues with Installing from the above packages - -### 1. The client may segfault | core-dump when exiting -When the client is run in `--monitor` mode manually, or when using the systemd service, the client may segfault on exit. - -This issue is caused by the way the 'onedrive' packages are built using the distribution LDC package & the default distribution compiler options which is the root cause for this issue. Refer to: https://bugs.launchpad.net/ubuntu/+source/ldc/+bug/1895969 - -**Additional references:** -* https://github.com/abraunegg/onedrive/issues/1053 -* https://github.com/abraunegg/onedrive/issues/1609 - -**Resolution Options:** -* Uninstall the package and build client from source diff --git a/install-sh b/install-sh deleted file mode 100755 index 8175c640..00000000 --- a/install-sh +++ /dev/null @@ -1,518 +0,0 @@ -#!/bin/sh -# install - install a program, script, or datafile - -scriptversion=2018-03-11.20; # UTC - -# This originates from X11R5 (mit/util/scripts/install.sh), which was -# later released in X11R6 (xc/config/util/install.sh) with the -# following copyright and license. -# -# Copyright (C) 1994 X Consortium -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN -# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC- -# TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# -# Except as contained in this notice, the name of the X Consortium shall not -# be used in advertising or otherwise to promote the sale, use or other deal- -# ings in this Software without prior written authorization from the X Consor- -# tium. -# -# -# FSF changes to this file are in the public domain. -# -# Calling this script install-sh is preferred over install.sh, to prevent -# 'make' implicit rules from creating a file called install from it -# when there is no Makefile. -# -# This script is compatible with the BSD install script, but was written -# from scratch. - -tab=' ' -nl=' -' -IFS=" $tab$nl" - -# Set DOITPROG to "echo" to test this script. - -doit=${DOITPROG-} -doit_exec=${doit:-exec} - -# Put in absolute file names if you don't have them in your path; -# or use environment vars. - -chgrpprog=${CHGRPPROG-chgrp} -chmodprog=${CHMODPROG-chmod} -chownprog=${CHOWNPROG-chown} -cmpprog=${CMPPROG-cmp} -cpprog=${CPPROG-cp} -mkdirprog=${MKDIRPROG-mkdir} -mvprog=${MVPROG-mv} -rmprog=${RMPROG-rm} -stripprog=${STRIPPROG-strip} - -posix_mkdir= - -# Desired mode of installed file. -mode=0755 - -chgrpcmd= -chmodcmd=$chmodprog -chowncmd= -mvcmd=$mvprog -rmcmd="$rmprog -f" -stripcmd= - -src= -dst= -dir_arg= -dst_arg= - -copy_on_change=false -is_target_a_directory=possibly - -usage="\ -Usage: $0 [OPTION]... [-T] SRCFILE DSTFILE - or: $0 [OPTION]... SRCFILES... DIRECTORY - or: $0 [OPTION]... -t DIRECTORY SRCFILES... - or: $0 [OPTION]... -d DIRECTORIES... - -In the 1st form, copy SRCFILE to DSTFILE. -In the 2nd and 3rd, copy all SRCFILES to DIRECTORY. -In the 4th, create DIRECTORIES. - -Options: - --help display this help and exit. - --version display version info and exit. - - -c (ignored) - -C install only if different (preserve the last data modification time) - -d create directories instead of installing files. - -g GROUP $chgrpprog installed files to GROUP. - -m MODE $chmodprog installed files to MODE. - -o USER $chownprog installed files to USER. - -s $stripprog installed files. - -t DIRECTORY install into DIRECTORY. - -T report an error if DSTFILE is a directory. - -Environment variables override the default commands: - CHGRPPROG CHMODPROG CHOWNPROG CMPPROG CPPROG MKDIRPROG MVPROG - RMPROG STRIPPROG -" - -while test $# -ne 0; do - case $1 in - -c) ;; - - -C) copy_on_change=true;; - - -d) dir_arg=true;; - - -g) chgrpcmd="$chgrpprog $2" - shift;; - - --help) echo "$usage"; exit $?;; - - -m) mode=$2 - case $mode in - *' '* | *"$tab"* | *"$nl"* | *'*'* | *'?'* | *'['*) - echo "$0: invalid mode: $mode" >&2 - exit 1;; - esac - shift;; - - -o) chowncmd="$chownprog $2" - shift;; - - -s) stripcmd=$stripprog;; - - -t) - is_target_a_directory=always - dst_arg=$2 - # Protect names problematic for 'test' and other utilities. - case $dst_arg in - -* | [=\(\)!]) dst_arg=./$dst_arg;; - esac - shift;; - - -T) is_target_a_directory=never;; - - --version) echo "$0 $scriptversion"; exit $?;; - - --) shift - break;; - - -*) echo "$0: invalid option: $1" >&2 - exit 1;; - - *) break;; - esac - shift -done - -# We allow the use of options -d and -T together, by making -d -# take the precedence; this is for compatibility with GNU install. - -if test -n "$dir_arg"; then - if test -n "$dst_arg"; then - echo "$0: target directory not allowed when installing a directory." >&2 - exit 1 - fi -fi - -if test $# -ne 0 && test -z "$dir_arg$dst_arg"; then - # When -d is used, all remaining arguments are directories to create. - # When -t is used, the destination is already specified. - # Otherwise, the last argument is the destination. Remove it from $@. - for arg - do - if test -n "$dst_arg"; then - # $@ is not empty: it contains at least $arg. - set fnord "$@" "$dst_arg" - shift # fnord - fi - shift # arg - dst_arg=$arg - # Protect names problematic for 'test' and other utilities. - case $dst_arg in - -* | [=\(\)!]) dst_arg=./$dst_arg;; - esac - done -fi - -if test $# -eq 0; then - if test -z "$dir_arg"; then - echo "$0: no input file specified." >&2 - exit 1 - fi - # It's OK to call 'install-sh -d' without argument. - # This can happen when creating conditional directories. - exit 0 -fi - -if test -z "$dir_arg"; then - if test $# -gt 1 || test "$is_target_a_directory" = always; then - if test ! -d "$dst_arg"; then - echo "$0: $dst_arg: Is not a directory." >&2 - exit 1 - fi - fi -fi - -if test -z "$dir_arg"; then - do_exit='(exit $ret); exit $ret' - trap "ret=129; $do_exit" 1 - trap "ret=130; $do_exit" 2 - trap "ret=141; $do_exit" 13 - trap "ret=143; $do_exit" 15 - - # Set umask so as not to create temps with too-generous modes. - # However, 'strip' requires both read and write access to temps. - case $mode in - # Optimize common cases. - *644) cp_umask=133;; - *755) cp_umask=22;; - - *[0-7]) - if test -z "$stripcmd"; then - u_plus_rw= - else - u_plus_rw='% 200' - fi - cp_umask=`expr '(' 777 - $mode % 1000 ')' $u_plus_rw`;; - *) - if test -z "$stripcmd"; then - u_plus_rw= - else - u_plus_rw=,u+rw - fi - cp_umask=$mode$u_plus_rw;; - esac -fi - -for src -do - # Protect names problematic for 'test' and other utilities. - case $src in - -* | [=\(\)!]) src=./$src;; - esac - - if test -n "$dir_arg"; then - dst=$src - dstdir=$dst - test -d "$dstdir" - dstdir_status=$? - else - - # Waiting for this to be detected by the "$cpprog $src $dsttmp" command - # might cause directories to be created, which would be especially bad - # if $src (and thus $dsttmp) contains '*'. - if test ! -f "$src" && test ! -d "$src"; then - echo "$0: $src does not exist." >&2 - exit 1 - fi - - if test -z "$dst_arg"; then - echo "$0: no destination specified." >&2 - exit 1 - fi - dst=$dst_arg - - # If destination is a directory, append the input filename. - if test -d "$dst"; then - if test "$is_target_a_directory" = never; then - echo "$0: $dst_arg: Is a directory" >&2 - exit 1 - fi - dstdir=$dst - dstbase=`basename "$src"` - case $dst in - */) dst=$dst$dstbase;; - *) dst=$dst/$dstbase;; - esac - dstdir_status=0 - else - dstdir=`dirname "$dst"` - test -d "$dstdir" - dstdir_status=$? - fi - fi - - case $dstdir in - */) dstdirslash=$dstdir;; - *) dstdirslash=$dstdir/;; - esac - - obsolete_mkdir_used=false - - if test $dstdir_status != 0; then - case $posix_mkdir in - '') - # Create intermediate dirs using mode 755 as modified by the umask. - # This is like FreeBSD 'install' as of 1997-10-28. - umask=`umask` - case $stripcmd.$umask in - # Optimize common cases. - *[2367][2367]) mkdir_umask=$umask;; - .*0[02][02] | .[02][02] | .[02]) mkdir_umask=22;; - - *[0-7]) - mkdir_umask=`expr $umask + 22 \ - - $umask % 100 % 40 + $umask % 20 \ - - $umask % 10 % 4 + $umask % 2 - `;; - *) mkdir_umask=$umask,go-w;; - esac - - # With -d, create the new directory with the user-specified mode. - # Otherwise, rely on $mkdir_umask. - if test -n "$dir_arg"; then - mkdir_mode=-m$mode - else - mkdir_mode= - fi - - posix_mkdir=false - case $umask in - *[123567][0-7][0-7]) - # POSIX mkdir -p sets u+wx bits regardless of umask, which - # is incompatible with FreeBSD 'install' when (umask & 300) != 0. - ;; - *) - # Note that $RANDOM variable is not portable (e.g. dash); Use it - # here however when possible just to lower collision chance. - tmpdir=${TMPDIR-/tmp}/ins$RANDOM-$$ - - trap 'ret=$?; rmdir "$tmpdir/a/b" "$tmpdir/a" "$tmpdir" 2>/dev/null; exit $ret' 0 - - # Because "mkdir -p" follows existing symlinks and we likely work - # directly in world-writeable /tmp, make sure that the '$tmpdir' - # directory is successfully created first before we actually test - # 'mkdir -p' feature. - if (umask $mkdir_umask && - $mkdirprog $mkdir_mode "$tmpdir" && - exec $mkdirprog $mkdir_mode -p -- "$tmpdir/a/b") >/dev/null 2>&1 - then - if test -z "$dir_arg" || { - # Check for POSIX incompatibilities with -m. - # HP-UX 11.23 and IRIX 6.5 mkdir -m -p sets group- or - # other-writable bit of parent directory when it shouldn't. - # FreeBSD 6.1 mkdir -m -p sets mode of existing directory. - test_tmpdir="$tmpdir/a" - ls_ld_tmpdir=`ls -ld "$test_tmpdir"` - case $ls_ld_tmpdir in - d????-?r-*) different_mode=700;; - d????-?--*) different_mode=755;; - *) false;; - esac && - $mkdirprog -m$different_mode -p -- "$test_tmpdir" && { - ls_ld_tmpdir_1=`ls -ld "$test_tmpdir"` - test "$ls_ld_tmpdir" = "$ls_ld_tmpdir_1" - } - } - then posix_mkdir=: - fi - rmdir "$tmpdir/a/b" "$tmpdir/a" "$tmpdir" - else - # Remove any dirs left behind by ancient mkdir implementations. - rmdir ./$mkdir_mode ./-p ./-- "$tmpdir" 2>/dev/null - fi - trap '' 0;; - esac;; - esac - - if - $posix_mkdir && ( - umask $mkdir_umask && - $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir" - ) - then : - else - - # The umask is ridiculous, or mkdir does not conform to POSIX, - # or it failed possibly due to a race condition. Create the - # directory the slow way, step by step, checking for races as we go. - - case $dstdir in - /*) prefix='/';; - [-=\(\)!]*) prefix='./';; - *) prefix='';; - esac - - oIFS=$IFS - IFS=/ - set -f - set fnord $dstdir - shift - set +f - IFS=$oIFS - - prefixes= - - for d - do - test X"$d" = X && continue - - prefix=$prefix$d - if test -d "$prefix"; then - prefixes= - else - if $posix_mkdir; then - (umask=$mkdir_umask && - $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir") && break - # Don't fail if two instances are running concurrently. - test -d "$prefix" || exit 1 - else - case $prefix in - *\'*) qprefix=`echo "$prefix" | sed "s/'/'\\\\\\\\''/g"`;; - *) qprefix=$prefix;; - esac - prefixes="$prefixes '$qprefix'" - fi - fi - prefix=$prefix/ - done - - if test -n "$prefixes"; then - # Don't fail if two instances are running concurrently. - (umask $mkdir_umask && - eval "\$doit_exec \$mkdirprog $prefixes") || - test -d "$dstdir" || exit 1 - obsolete_mkdir_used=true - fi - fi - fi - - if test -n "$dir_arg"; then - { test -z "$chowncmd" || $doit $chowncmd "$dst"; } && - { test -z "$chgrpcmd" || $doit $chgrpcmd "$dst"; } && - { test "$obsolete_mkdir_used$chowncmd$chgrpcmd" = false || - test -z "$chmodcmd" || $doit $chmodcmd $mode "$dst"; } || exit 1 - else - - # Make a couple of temp file names in the proper directory. - dsttmp=${dstdirslash}_inst.$$_ - rmtmp=${dstdirslash}_rm.$$_ - - # Trap to clean up those temp files at exit. - trap 'ret=$?; rm -f "$dsttmp" "$rmtmp" && exit $ret' 0 - - # Copy the file name to the temp name. - (umask $cp_umask && $doit_exec $cpprog "$src" "$dsttmp") && - - # and set any options; do chmod last to preserve setuid bits. - # - # If any of these fail, we abort the whole thing. If we want to - # ignore errors from any of these, just make sure not to ignore - # errors from the above "$doit $cpprog $src $dsttmp" command. - # - { test -z "$chowncmd" || $doit $chowncmd "$dsttmp"; } && - { test -z "$chgrpcmd" || $doit $chgrpcmd "$dsttmp"; } && - { test -z "$stripcmd" || $doit $stripcmd "$dsttmp"; } && - { test -z "$chmodcmd" || $doit $chmodcmd $mode "$dsttmp"; } && - - # If -C, don't bother to copy if it wouldn't change the file. - if $copy_on_change && - old=`LC_ALL=C ls -dlL "$dst" 2>/dev/null` && - new=`LC_ALL=C ls -dlL "$dsttmp" 2>/dev/null` && - set -f && - set X $old && old=:$2:$4:$5:$6 && - set X $new && new=:$2:$4:$5:$6 && - set +f && - test "$old" = "$new" && - $cmpprog "$dst" "$dsttmp" >/dev/null 2>&1 - then - rm -f "$dsttmp" - else - # Rename the file to the real destination. - $doit $mvcmd -f "$dsttmp" "$dst" 2>/dev/null || - - # The rename failed, perhaps because mv can't rename something else - # to itself, or perhaps because mv is so ancient that it does not - # support -f. - { - # Now remove or move aside any old file at destination location. - # We try this two ways since rm can't unlink itself on some - # systems and the destination file might be busy for other - # reasons. In this case, the final cleanup might fail but the new - # file should still install successfully. - { - test ! -f "$dst" || - $doit $rmcmd -f "$dst" 2>/dev/null || - { $doit $mvcmd -f "$dst" "$rmtmp" 2>/dev/null && - { $doit $rmcmd -f "$rmtmp" 2>/dev/null; :; } - } || - { echo "$0: cannot unlink or rename $dst" >&2 - (exit 1); exit 1 - } - } && - - # Now rename the file to the real destination. - $doit $mvcmd "$dsttmp" "$dst" - } - fi || exit 1 - - trap '' 0 - fi -done - -# Local variables: -# eval: (add-hook 'before-save-hook 'time-stamp) -# time-stamp-start: "scriptversion=" -# time-stamp-format: "%:y-%02m-%02d.%02H" -# time-stamp-time-zone: "UTC0" -# time-stamp-end: "; # UTC" -# End: diff --git a/onedrive.1.in b/onedrive.1.in deleted file mode 100644 index 5caacb0d..00000000 --- a/onedrive.1.in +++ /dev/null @@ -1,391 +0,0 @@ -.TH ONEDRIVE "1" "@PACKAGE_DATE@" "@PACKAGE_VERSION@" "User Commands" -.SH NAME -onedrive \- folder synchronization with OneDrive -.SH SYNOPSIS -.B onedrive -[\fI\,OPTION\/\fR] \-\-synchronize -.br -.B onedrive -[\fI\,OPTION\/\fR] \-\-monitor -.br -.B onedrive -[\fI\,OPTION\/\fR] \-\-display-config -.br -.B onedrive -[\fI\,OPTION\/\fR] \-\-display-sync-status -.SH DESCRIPTION -A complete tool to interact with OneDrive on Linux. -.SH OPTIONS -Without any option given, no sync is done and the program exits. -.TP -\fB\-\-auth\-files\fP ARG -Perform authorization via two files passed in as \fBARG\fP in the format \fBauthUrl:responseUrl\fP. -The authorization URL is written to the \fBauthUrl\fP, then \fBonedrive\fP waits for -the file \fBresponseUrl\fP to be present, and reads the response from that file. -.TP -\fB\-\-auth\-response\fP ARG -Perform authentication not via interactive dialog but via providing the response url directly. -.TP -\fB\-\-check\-for\-nomount\fP -Check for the presence of .nosync in the syncdir root. If found, do not perform sync. -.br -Configuration file key: \fBcheck_nomount\fP (default: \fBfalse\fP) -.TP -\fB\-\-check\-for\-nosync\fP -Check for the presence of .nosync in each directory. If found, skip directory from sync. -.br -Configuration file key: \fBcheck_nosync\fP (default: \fBfalse\fP) -.TP -\fB\-\-classify\-as\-big\-delete\fP -Number of children in a path that is locally removed which will be classified as a 'big data delete' -.br -Configuration file key: \fBclassify_as_big_delete\fP (default: \fB1000\fP) -.TP -\fB\-\-cleanup\-local\-files\fP -Cleanup additional local files when using \-\-download-only. This will remove local data. -.br -Configuration file key: \fBcleanup_local_files\fP (default: \fBfalse\fP) -.TP -\fB\-\-confdir\fP ARG -Set the directory used to store the configuration files -.TP -\fB\-\-create\-directory\fP ARG -Create a directory on OneDrive \- no sync will be performed. -.TP -\fB\-\-create\-share\-link\fP ARG -Create a shareable link for an existing file on OneDrive -.TP -\fB\-\-debug\-https\fP -Debug OneDrive HTTPS communication. -.br -Configuration file key: \fBdebug_https\fP (default: \fBfalse\fP) -.TP -\fB\-\-destination\-directory\fP ARG -Destination directory for renamed or move on OneDrive \- no sync will be performed. -.TP -\fB\-\-disable\-download\-validation\fP -Disable download validation when downloading from OneDrive -.br -Configuration file key: \fBdisable_download_validation\fP (default: \fBfalse\fP) -.TP -\fB\-\-disable\-notifications\fP -Do not use desktop notifications in monitor mode -.br -Configuration file key: \fBdisable_notifications\fP (default: \fBfalse\fP) -.TP -\fB\-\-disable\-upload\-validation\fP -Disable upload validation when uploading to OneDrive -.br -Configuration file key: \fBdisable_upload_validation\fP (default: \fBfalse\fP) -.TP -\fB\-\-display\-config\fP -Display what options the client will use as currently configured \- no sync will be performed. -.TP -\fB\-\-display\-running\-config\fP -Display what options the client has been configured to use on application startup. -.TP -\fB\-\-display\-sync\-status\fP -Display the sync status of the client \- no sync will be performed. -.TP -\fB\-\-download\-only\fP -Replicate the OneDrive online state locally, by only downloading changes from OneDrive. Do not upload local changes to OneDrive. -.br -Configuration file key: \fBdownload_only\fP (default: \fBfalse\fP) -.TP -\fB\-\-dry\-run\fP -Perform a trial sync with no changes made. Can ONLY be used with --synchronize. Will be ignored for --monitor -.br -Configuration file key: \fBdry_run\fP (default: \fBfalse\fP) -.TP -\fB\-\-enable\-logging\fP -Enable client activity to a separate log file -.br -Configuration file key: \fBenable_logging\fP (default: \fBfalse\fP) -.TP -\fB\-\-force\fP -Force the deletion of data when a 'big delete' is detected -.TP -\fB\-\-force\-http\-11\fP -Force the use of HTTP 1.1 for all operations -.br -Configuration file key: \fBforce_http_11\fP (default: \fBfalse\fP) -.TP -\fB\-\-force\-sync\fP -Force a synchronization of a specific folder, only when using --synchronize --single-directory and ignore -.br -all non-default skip_dir and skip_file rules -.TP -\fB\-\-get\-O365\-drive\-id\fP ARG -Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library -.TP -\fB\-\-get\-file\-link\fP ARG -Display the file link of a synced file -.TP -\fB\-\-list\-shared\-folders\fP -List OneDrive Business Shared Folders -.TP -\fB\-\-local\-first\fP -Synchronize from the local directory source first, before downloading changes from OneDrive. -.br -Configuration file key: \fBlocal_first\fP (default: \fBfalse\fP) -.TP -\fB\-\-logout\fP -Logout the current user -.TP -\fB\-\-log\-dir\fP ARG -defines the directory where logging output is saved to, needs to end with a slash -.br -Configuration file key: \fBlog_dir\fP (default: \fB/var/log/onedrive/\fP) -.TP -\fB\-\-min\-notify\-changes\fP -the minimum number of pending incoming changes necessary to trigger -a desktop notification -.br -Configuration file key: \fBmin_notify_changes\fP (default: \fB5\fP) -.TP -\fB\-m \-\-modified\-by\fP ARG -Display the last modified by details of a given path -.TP -\fB\-m \-\-monitor\fP -Keep monitoring for local and remote changes -.TP -\fB\-\-monitor\-interval\fP ARG -The number of seconds by which each sync operation is undertaken when -idle under monitor mode -.br -Configuration file key: \fBmonitor_interval\fP (default: \fB300\fP) -.TP -\fB\-\-monitor\-fullscan-frequency\fP ARG -Number of sync runs before performing a full local scan of the synced directory -.br -Configuration file key: \fBmonitor_fullscan_frequency\fP (default: \fB10\fP) -.TP -\fB\-\-monitor\-log\-frequency\fP ARG -Frequency of logging in monitor mode -.br -Configuration file key: \fBmonitor_log_frequency\fP (default: \fB5\fP) -.TP -\fB\-\-no\-remote\-delete\fP -Do not delete local file 'deletes' from OneDrive when using \fB\-\-upload\-only\fR -.br -Configuration file key: \fBno_remote_delete\fP (default: \fBfalse\fP) -.TP -\fB\-\-operation\-timeout\fP ARG -Set the maximum amount of time (seconds) a file operation is allowed to take. This includes DNS resolution, connecting, data transfer, etc. -.br -Configuration file key: \fBoperation_timeout\fP (default: \fB3600\fP) -.TP -\fB\-\-print\-token\fP -Print the access token, useful for debugging -.TP -\fB\-\-reauth\fP -Reauthenticate the client with OneDrive -.TP -\fB\-\-remove\-directory\fP ARG -Remove a directory on OneDrive \- no sync will be performed. -.TP -\fB\-\-remove\-source\-files\fP -Remove source file after successful transfer to OneDrive when using \-\-upload-only -.br -Configuration file key: \fBremove_source_files\fP (default: \fBfalse\fP) -.TP -\fB\-\-resync\fP -Forget the last saved state, perform a full sync -.TP -\fB\-\-resync\-auth\fP -Approve the use of performing a --resync action without needing CLI authorization -.TP -\fB\-\-single\-directory\fP ARG -Specify a single local directory within the OneDrive root to sync. -.TP -\fB\-\-skip\-dir\fP ARG -Skip any directories that match this pattern from syncing -.TP -\fB\-\-skip\-dir\-strict\-match\fP -When matching skip_dir directories, only match explicit matches -.br -Configuration file key: \fBskip_dir_strict_match\fP (default: \fBfalse\fP) -.TP -\fB\-\-skip\-dot\-files\fP -Skip dot files and folders from syncing -.br -Configuration file key: \fBskip_dotfiles\fP (default: \fBfalse\fP) -.TP -\fB\-\-skip\-file\fP -Skip any files that match this pattern from syncing -.br -Configuration file key: \fBskip_file\fP (default: \fB~*|.~*|*.tmp\fP) -.TP -\fB\-\-skip\-size\fP ARG -Skip new files larger than this size (in MB) -.TP -\fB\-\-skip\-symlinks\fP -Skip syncing of symlinks -.br -Configuration file key: \fBskip_symlinks\fP (default: \fBfalse\fP) -.TP -\fB\-\-source\-directory\fP ARG -Source directory to rename or move on OneDrive \- no sync will be performed. -.TP -\fB\-\-space\-reservation\fP ARG -The amount of disk space to reserve (in MB) to avoid 100% disk space utilisation -.TP -\fB\-\-sync\-root\-files\fP -Sync all files in sync_dir root when using sync_list. -.TP -\fB\-\-sync\-shared\-folders\fP -Sync OneDrive Business Shared Folders -.br -Configuration file key: \fBsync_business_shared_folders\fP (default: \fBfalse\fP) -.TP -\fB\-\-syncdir\fP ARG -Set the directory used to sync the files that are synced -.br -Configuration file key: \fBsync_dir\fP (default: \fB~/OneDrive\fP) -.TP -\fB\-\-synchronize\fP -Perform a synchronization -.TP -\fB\-\-upload\-only\fP -Replicate the locally configured sync_dir state to OneDrive, by only uploading local changes to OneDrive. Do not download changes from OneDrive. -.br -Configuration file key: \fBupload_only\fP (default: \fBfalse\fP) -.TP -\fB\-\-user\-agent\fP ARG -Set the used User Agent identifier -.br -Configuration file key: \fBuser_agent\fP (default: don't change) -.TP -\fB\-v \-\-verbose\fP -Print more details, useful for debugging. Given two times (or more) -enables even more verbose debug statements. -.TP -\fB\-\-version\fP -Print the version and exit -.TP -\fB\-\-with\-editing\-perms\fP -Create a read-write shareable link for an existing file on OneDrive when used with --create-share-link -.TP -\fB\-h \-\-help\fP -This help information. -.PP - -.SH FEATURES - -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 Folder support for OneDrive Personal and OneDrive Business accounts - -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 - -You should copy the default config file into your home directory before making changes: -.nf -\fB -mkdir\ \-p\ ~/.config/onedrive -cp\ @DOCDIR@/config\ ~/.config/onedrive/config -\fP -.fi - -For the supported options see the above list of command line options -for the availability of a configuration key. -.PP -Pattern are case insensitive. -\fB*\fP and \fB?\fP wildcards characters are supported. -Use \fB|\fP to separate multiple patterns. - -After changing the filters (\fBskip_file\fP or \fBskip_dir\fP in your configs) you must -execute \fBonedrive --synchronize --resync\fP. - -.SH FIRST RUN - -After installing the application you must run it at least once from the terminal -to authorize it. - -You will be asked to open a specific link 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 the permission, you will be -redirected to a blank page. Copy the URI of the blank page into the application. - - -.SH SYSTEMD INTEGRATION - -Service files are installed into user and system directories. -.TP -OneDrive service running as root user -To enable this mode, run as root user -.nf -\fB -systemctl enable onedrive -systemctl start onedrive -\fP -.fi - -.TP -OneDrive service running as root user for a non-root user -This mode allows starting the OneDrive service automatically with -system start for multiple users. For each \fB\fP run: -.nf -\fB -systemctl enable onedrive@ -systemctl start onedrive@ -\fP -.fi - -.TP -OneDrive service running as non-root user -In this mode the service will be started when the user logs in. -Run as user -.nf -\fB -systemctl --user enable onedrive -systemctl --user start onedrive -\fP -.fi - -.SH LOGGING OUTPUT - -When running onedrive all actions can be logged to a separate log file. -This can be enabled by using the \fB--enable-logging\fP flag. -By default, log files will be written to \fB/var/log/onedrive\fP. - -All logfiles will be in the format of \fB%username%.onedrive.log\fP, -where \fB%username%\fP represents the user who ran the client. - - -.SH NOTIFICATIONS - -If OneDrive has been compiled with support for notifications, a running -\fBonedrive\fP in monitor mode will send notifications about -initialization and errors via libnotify to the dbus. - -Note that this does not work if \fBonedrive\fP is started as root -for a user via the \fBonedrive@\fP service. - -.SH SEE ALSO - -Further examples and documentation is available in -\f[C]README.md\f[] -\f[C]docs/USAGE.md\f[] -\f[C]docs/advanced-usage.md\f[] -\f[C]docs/BusinessSharedFolders.md\f[] -\f[C]docs/SharePoint-Shared-Libraries.md\f[] -\f[C]docs/national-cloud-deployments.md\f[] diff --git a/src/arsd/README.md b/src/arsd/README.md deleted file mode 100644 index f4f1d0d5..00000000 --- a/src/arsd/README.md +++ /dev/null @@ -1,8 +0,0 @@ -The files in this directory have been obtained form the following places: - -cgi.d - https://github.com/adamdruppe/arsd/blob/a870179988b8881b04126856105f0fad2cc0018d/cgi.d - License: Boost Software License - Version 1.0 - - Copyright 2008-2021, Adam D. Ruppe - see https://github.com/adamdruppe/arsd/blob/a870179988b8881b04126856105f0fad2cc0018d/LICENSE diff --git a/src/arsd/cgi.d b/src/arsd/cgi.d deleted file mode 100644 index 79f5feaa..00000000 --- a/src/arsd/cgi.d +++ /dev/null @@ -1,11810 +0,0 @@ -// FIXME: if an exception is thrown, we shouldn't necessarily cache... -// FIXME: there's some annoying duplication of code in the various versioned mains - -// add the Range header in there too. should return 206 - -// FIXME: cgi per-request arena allocator - -// i need to add a bunch of type templates for validations... mayne @NotNull or NotNull! - -// FIXME: I might make a cgi proxy class which can change things; the underlying one is still immutable -// but the later one can edit and simplify the api. You'd have to use the subclass tho! - -/* -void foo(int f, @("test") string s) {} - -void main() { - static if(is(typeof(foo) Params == __parameters)) - //pragma(msg, __traits(getAttributes, Params[0])); - pragma(msg, __traits(getAttributes, Params[1..2])); - else - pragma(msg, "fail"); -} -*/ - -// Note: spawn-fcgi can help with fastcgi on nginx - -// FIXME: to do: add openssl optionally -// make sure embedded_httpd doesn't send two answers if one writes() then dies - -// future direction: websocket as a separate process that you can sendfile to for an async passoff of those long-lived connections - -/* - Session manager process: it spawns a new process, passing a - command line argument, to just be a little key/value store - of some serializable struct. On Windows, it CreateProcess. - On Linux, it can just fork or maybe fork/exec. The session - key is in a cookie. - - Server-side event process: spawns an async manager. You can - push stuff out to channel ids and the clients listen to it. - - websocket process: spawns an async handler. They can talk to - each other or get info from a cgi request. - - Tempting to put web.d 2.0 in here. It would: - * map urls and form generation to functions - * have data presentation magic - * do the skeleton stuff like 1.0 - * auto-cache generated stuff in files (at least if pure?) - * introspect functions in json for consumers - - - https://linux.die.net/man/3/posix_spawn -*/ - -/++ - Provides a uniform server-side API for CGI, FastCGI, SCGI, and HTTP web applications. Offers both lower- and higher- level api options among other common (optional) things like websocket and event source serving support, session management, and job scheduling. - - --- - import arsd.cgi; - - // Instead of writing your own main(), you should write a function - // that takes a Cgi param, and use mixin GenericMain - // for maximum compatibility with different web servers. - void hello(Cgi cgi) { - cgi.setResponseContentType("text/plain"); - - if("name" in cgi.get) - cgi.write("Hello, " ~ cgi.get["name"]); - else - cgi.write("Hello, world!"); - } - - mixin GenericMain!hello; - --- - - Or: - --- - import arsd.cgi; - - class MyApi : WebObject { - @UrlName("") - string hello(string name = null) { - if(name is null) - return "Hello, world!"; - else - return "Hello, " ~ name; - } - } - mixin DispatcherMain!( - "/".serveApi!MyApi - ); - --- - - $(NOTE - Please note that using the higher-level api will add a dependency on arsd.dom and arsd.jsvar to your application. - If you use `dmd -i` or `ldc2 -i` to build, it will just work, but with dub, you will have do `dub add arsd-official:jsvar` - and `dub add arsd-official:dom` yourself. - ) - - Test on console (works in any interface mode): - $(CONSOLE - $ ./cgi_hello GET / name=whatever - ) - - If using http version (default on `dub` builds, or on custom builds when passing `-version=embedded_httpd` to dmd): - $(CONSOLE - $ ./cgi_hello --port 8080 - # now you can go to http://localhost:8080/?name=whatever - ) - - Please note: the default port for http is 8085 and for scgi is 4000. I recommend you set your own by the command line argument in a startup script instead of relying on any hard coded defaults. It is possible though to code your own with [RequestServer], however. - - - Build_Configurations: - - cgi.d tries to be flexible to meet your needs. It is possible to configure it both at runtime (by writing your own `main` function and constructing a [RequestServer] object) or at compile time using the `version` switch to the compiler or a dub `subConfiguration`. - - If you are using `dub`, use: - - ```sdlang - subConfiguration "arsd-official:cgi" "VALUE_HERE" - ``` - - or to dub.json: - - ```json - "subConfigurations": {"arsd-official:cgi": "VALUE_HERE"} - ``` - - to change versions. The possible options for `VALUE_HERE` are: - - $(LIST - * `embedded_httpd` for the embedded httpd version (built-in web server). This is the default for dub builds. You can run the program then connect directly to it from your browser. - * `cgi` for traditional cgi binaries. These are run by an outside web server as-needed to handle requests. - * `fastcgi` for FastCGI builds. FastCGI is managed from an outside helper, there's one built into Microsoft IIS, Apache httpd, and Lighttpd, and a generic program you can use with nginx called `spawn-fcgi`. If you don't already know how to use it, I suggest you use one of the other modes. - * `scgi` for SCGI builds. SCGI is a simplified form of FastCGI, where you run the server as an application service which is proxied by your outside webserver. - * `stdio_http` for speaking raw http over stdin and stdout. This is made for systemd services. See [RequestServer.serveSingleHttpConnectionOnStdio] for more information. - ) - - With dmd, use: - - $(TABLE_ROWS - - * + Interfaces - + (mutually exclusive) - - * - `-version=plain_cgi` - - The default building the module alone without dub - a traditional, plain CGI executable will be generated. - * - `-version=embedded_httpd` - - A HTTP server will be embedded in the generated executable. This is default when building with dub. - * - `-version=fastcgi` - - A FastCGI executable will be generated. - * - `-version=scgi` - - A SCGI (SimpleCGI) executable will be generated. - * - `-version=embedded_httpd_hybrid` - - A HTTP server that uses a combination of processes, threads, and fibers to better handle large numbers of idle connections. Recommended if you are going to serve websockets in a non-local application. - * - `-version=embedded_httpd_threads` - - The embedded HTTP server will use a single process with a thread pool. (use instead of plain `embedded_httpd` if you want this specific implementation) - * - `-version=embedded_httpd_processes` - - The embedded HTTP server will use a prefork style process pool. (use instead of plain `embedded_httpd` if you want this specific implementation) - * - `-version=embedded_httpd_processes_accept_after_fork` - - It will call accept() in each child process, after forking. This is currently the only option, though I am experimenting with other ideas. You probably should NOT specify this right now. - * - `-version=stdio_http` - - The embedded HTTP server will be spoken over stdin and stdout. - - * + Tweaks - + (can be used together with others) - - * - `-version=cgi_with_websocket` - - The CGI class has websocket server support. (This is on by default now.) - - * - `-version=with_openssl` - - not currently used - * - `-version=cgi_embedded_sessions` - - The session server will be embedded in the cgi.d server process - * - `-version=cgi_session_server_process` - - The session will be provided in a separate process, provided by cgi.d. - ) - - For example, - - For CGI, `dmd yourfile.d cgi.d` then put the executable in your cgi-bin directory. - - For FastCGI: `dmd yourfile.d cgi.d -version=fastcgi` and run it. spawn-fcgi helps on nginx. You can put the file in the directory for Apache. On IIS, run it with a port on the command line (this causes it to call FCGX_OpenSocket, which can work on nginx too). - - For SCGI: `dmd yourfile.d cgi.d -version=scgi` and run the executable, providing a port number on the command line. - - For an embedded HTTP server, run `dmd yourfile.d cgi.d -version=embedded_httpd` and run the generated program. It listens on port 8085 by default. You can change this on the command line with the --port option when running your program. - - Simulating_requests: - - If you are using one of the [GenericMain] or [DispatcherMain] mixins, or main with your own call to [RequestServer.trySimulatedRequest], you can simulate requests from your command-ine shell. Call the program like this: - - $(CONSOLE - ./yourprogram GET / name=adr - ) - - And it will print the result to stdout instead of running a server, regardless of build more.. - - CGI_Setup_tips: - - On Apache, you may do `SetHandler cgi-script` in your `.htaccess` file to set a particular file to be run through the cgi program. Note that all "subdirectories" of it also run the program; if you configure `/foo` to be a cgi script, then going to `/foo/bar` will call your cgi handler function with `cgi.pathInfo == "/bar"`. - - Overview_Of_Basic_Concepts: - - cgi.d offers both lower-level handler apis as well as higher-level auto-dispatcher apis. For a lower-level handler function, you'll probably want to review the following functions: - - Input: [Cgi.get], [Cgi.post], [Cgi.request], [Cgi.files], [Cgi.cookies], [Cgi.pathInfo], [Cgi.requestMethod], - and HTTP headers ([Cgi.headers], [Cgi.userAgent], [Cgi.referrer], [Cgi.accept], [Cgi.authorization], [Cgi.lastEventId]) - - Output: [Cgi.write], [Cgi.header], [Cgi.setResponseStatus], [Cgi.setResponseContentType], [Cgi.gzipResponse] - - Cookies: [Cgi.setCookie], [Cgi.clearCookie], [Cgi.cookie], [Cgi.cookies] - - Caching: [Cgi.setResponseExpires], [Cgi.updateResponseExpires], [Cgi.setCache] - - Redirections: [Cgi.setResponseLocation] - - Other Information: [Cgi.remoteAddress], [Cgi.https], [Cgi.port], [Cgi.scriptName], [Cgi.requestUri], [Cgi.getCurrentCompleteUri], [Cgi.onRequestBodyDataReceived] - - Websockets: [Websocket], [websocketRequested], [acceptWebsocket]. For websockets, use the `embedded_httpd_hybrid` build mode for best results, because it is optimized for handling large numbers of idle connections compared to the other build modes. - - Overriding behavior for special cases streaming input data: see the virtual functions [Cgi.handleIncomingDataChunk], [Cgi.prepareForIncomingDataChunks], [Cgi.cleanUpPostDataState] - - A basic program using the lower-level api might look like: - - --- - import arsd.cgi; - - // you write a request handler which always takes a Cgi object - void handler(Cgi cgi) { - /+ - when the user goes to your site, suppose you are being hosted at http://example.com/yourapp - - If the user goes to http://example.com/yourapp/test?name=value - then the url will be parsed out into the following pieces: - - cgi.pathInfo == "/test". This is everything after yourapp's name. (If you are doing an embedded http server, your app's name is blank, so pathInfo will be the whole path of the url.) - - cgi.scriptName == "yourapp". With an embedded http server, this will be blank. - - cgi.host == "example.com" - - cgi.https == false - - cgi.queryString == "name=value" (there's also cgi.search, which will be "?name=value", including the ?) - - The query string is further parsed into the `get` and `getArray` members, so: - - cgi.get == ["name": "value"], meaning you can do `cgi.get["name"] == "value"` - - And - - cgi.getArray == ["name": ["value"]]. - - Why is there both `get` and `getArray`? The standard allows names to be repeated. This can be very useful, - it is how http forms naturally pass multiple items like a set of checkboxes. So `getArray` is the complete data - if you need it. But since so often you only care about one value, the `get` member provides more convenient access. - - We can use these members to process the request and build link urls. Other info from the request are in other members, we'll look at them later. - +/ - switch(cgi.pathInfo) { - // the home page will be a small html form that can set a cookie. - case "/": - cgi.write(` - - -

- - - - - - `, true); // the , true tells it that this is the one, complete response i want to send, allowing some optimizations. - break; - // POSTing to this will set a cookie with our submitted name - case "/set-cookie": - // HTTP has a number of request methods (also called "verbs") to tell - // what you should do with the given resource. - // The most common are GET and POST, the ones used in html forms. - // You can check which one was used with the `cgi.requestMethod` property. - if(cgi.requestMethod == Cgi.RequestMethod.POST) { - - // headers like redirections need to be set before we call `write` - cgi.setResponseLocation("read-cookie"); - - // just like how url params go into cgi.get/getArray, form data submitted in a POST - // body go to cgi.post/postArray. Please note that a POST request can also have get - // params in addition to post params. - // - // There's also a convenience function `cgi.request("name")` which checks post first, - // then get if it isn't found there, and then returns a default value if it is in neither. - if("name" in cgi.post) { - // we can set cookies with a method too - // again, cookies need to be set before calling `cgi.write`, since they - // are a kind of header. - cgi.setCookie("name" , cgi.post["name"]); - } - - // the user will probably never see this, since the response location - // is an automatic redirect, but it is still best to say something anyway - cgi.write("Redirecting you to see the cookie...", true); - } else { - // you can write out response codes and headers - // as well as response bodies - // - // But always check the cgi docs before using the generic - // `header` method - if there is a specific method for your - // header, use it before resorting to the generic one to avoid - // a header value from being sent twice. - cgi.setResponseLocation("405 Method Not Allowed"); - // there is no special accept member, so you can use the generic header function - cgi.header("Accept: POST"); - // but content type does have a method, so prefer to use it: - cgi.setResponseContentType("text/plain"); - - // all the headers are buffered, and will be sent upon the first body - // write. you can actually modify some of them before sending if need be. - cgi.write("You must use the POST http verb on this resource.", true); - } - break; - // and GETting this will read the cookie back out - case "/read-cookie": - // I did NOT pass `,true` here because this is writing a partial response. - // It is possible to stream data to the user in chunks by writing partial - // responses the calling `cgi.flush();` to send the partial response immediately. - // normally, you'd only send partial chunks if you have to - it is better to build - // a response as a whole and send it as a whole whenever possible - but here I want - // to demo that you can. - cgi.write("Hello, "); - if("name" in cgi.cookies) { - import arsd.dom; // dom.d provides a lot of helpers for html - // since the cookie is set, we need to write it out properly to - // avoid cross-site scripting attacks. - // - // Getting this stuff right automatically is a benefit of using the higher - // level apis, but this demo is to show the fundamental building blocks, so - // we're responsible to take care of it. - cgi.write(htmlEntitiesEncode(cgi.cookies["name"])); - } else { - cgi.write("friend"); - } - - // note that I never called cgi.setResponseContentType, since the default is text/html. - // it doesn't hurt to do it explicitly though, just remember to do it before any cgi.write - // calls. - break; - default: - // no path matched - cgi.setResponseStatus("404 Not Found"); - cgi.write("Resource not found.", true); - } - } - - // and this adds the boilerplate to set up a server according to the - // compile version configuration and call your handler as requests come in - mixin GenericMain!handler; // the `handler` here is the name of your function - --- - - Even if you plan to always use the higher-level apis, I still recommend you at least familiarize yourself with the lower level functions, since they provide the lightest weight, most flexible options to get down to business if you ever need them. - - In the lower-level api, the [Cgi] object represents your HTTP transaction. It has functions to describe the request and for you to send your response. It leaves the details of how you o it up to you. The general guideline though is to avoid depending any variables outside your handler function, since there's no guarantee they will survive to another handler. You can use global vars as a lazy initialized cache, but you should always be ready in case it is empty. (One exception: if you use `-version=embedded_httpd_threads -version=cgi_no_fork`, then you can rely on it more, but you should still really write things assuming your function won't have anything survive beyond its return for max scalability and compatibility.) - - A basic program using the higher-level apis might look like: - - --- - /+ - import arsd.cgi; - - struct LoginData { - string currentUser; - } - - class AppClass : WebObject { - string foo() {} - } - - mixin DispatcherMain!( - "/assets/.serveStaticFileDirectory("assets/", true), // serve the files in the assets subdirectory - "/".serveApi!AppClass, - "/thing/".serveRestObject, - ); - +/ - --- - - Guide_for_PHP_users: - (Please note: I wrote this section in 2008. A lot of PHP hosts still ran 4.x back then, so it was common to avoid using classes - introduced in php 5 - to maintain compatibility! If you're coming from php more recently, this may not be relevant anymore, but still might help you.) - - If you are coming from old-style PHP, here's a quick guide to help you get started: - - $(SIDE_BY_SIDE - $(COLUMN - ```php - - ``` - ) - $(COLUMN - --- - import arsd.cgi; - void app(Cgi cgi) { - string foo = cgi.post["foo"]; - string bar = cgi.get["bar"]; - string baz = cgi.cookies["baz"]; - - string user_ip = cgi.remoteAddress; - string host = cgi.host; - string path = cgi.pathInfo; - - cgi.setCookie("baz", "some value"); - - cgi.write("hello!"); - } - - mixin GenericMain!app - --- - ) - ) - - $(H3 Array elements) - - - In PHP, you can give a form element a name like `"something[]"`, and then - `$_POST["something"]` gives an array. In D, you can use whatever name - you want, and access an array of values with the `cgi.getArray["name"]` and - `cgi.postArray["name"]` members. - - $(H3 Databases) - - PHP has a lot of stuff in its standard library. cgi.d doesn't include most - of these, but the rest of my arsd repository has much of it. For example, - to access a MySQL database, download `database.d` and `mysql.d` from my - github repo, and try this code (assuming, of course, your database is - set up): - - --- - import arsd.cgi; - import arsd.mysql; - - void app(Cgi cgi) { - auto database = new MySql("localhost", "username", "password", "database_name"); - foreach(row; mysql.query("SELECT count(id) FROM people")) - cgi.write(row[0] ~ " people in database"); - } - - mixin GenericMain!app; - --- - - Similar modules are available for PostgreSQL, Microsoft SQL Server, and SQLite databases, - implementing the same basic interface. - - See_Also: - - You may also want to see [arsd.dom], [arsd.webtemplate], and maybe some functions from my old [arsd.html] for more code for making - web applications. dom and webtemplate are used by the higher-level api here in cgi.d. - - For working with json, try [arsd.jsvar]. - - [arsd.database], [arsd.mysql], [arsd.postgres], [arsd.mssql], and [arsd.sqlite] can help in - accessing databases. - - If you are looking to access a web application via HTTP, try [arsd.http2]. - - Copyright: - - cgi.d copyright 2008-2023, Adam D. Ruppe. Provided under the Boost Software License. - - Yes, this file is old, and yes, it is still actively maintained and used. -+/ -module arsd.cgi; - -// FIXME: Nullable!T can be a checkbox that enables/disables the T on the automatic form -// and a SumType!(T, R) can be a radio box to pick between T and R to disclose the extra boxes on the automatic form - -/++ - This micro-example uses the [dispatcher] api to act as a simple http file server, serving files found in the current directory and its children. -+/ -unittest { - import arsd.cgi; - - mixin DispatcherMain!( - "/".serveStaticFileDirectory(null, true) - ); -} - -/++ - Same as the previous example, but written out long-form without the use of [DispatcherMain] nor [GenericMain]. -+/ -unittest { - import arsd.cgi; - - void requestHandler(Cgi cgi) { - cgi.dispatcher!( - "/".serveStaticFileDirectory(null, true) - ); - } - - // mixin GenericMain!requestHandler would add this function: - void main(string[] args) { - // this is all the content of [cgiMainImpl] which you can also call - - // cgi.d embeds a few add on functions like real time event forwarders - // and session servers it can run in other processes. this spawns them, if needed. - if(tryAddonServers(args)) - return; - - // cgi.d allows you to easily simulate http requests from the command line, - // without actually starting a server. this function will do that. - if(trySimulatedRequest!(requestHandler, Cgi)(args)) - return; - - RequestServer server; - // you can change the default port here if you like - // server.listeningPort = 9000; - - // then call this to let the command line args override your default - server.configureFromCommandLine(args); - - // here is where you could print out the listeningPort to the user if you wanted - - // and serve the request(s) according to the compile configuration - server.serve!(requestHandler)(); - - // or you could explicitly choose a serve mode like this: - // server.serveEmbeddedHttp!requestHandler(); - } -} - -/++ - cgi.d has built-in testing helpers too. These will provide mock requests and mock sessions that - otherwise run through the rest of the internal mechanisms to call your functions without actually - spinning up a server. -+/ -unittest { - import arsd.cgi; - - void requestHandler(Cgi cgi) { - - } - - // D doesn't let me embed a unittest inside an example unittest - // so this is a function, but you can do it however in your real program - /* unittest */ void runTests() { - auto tester = new CgiTester(&requestHandler); - - auto response = tester.GET("/"); - assert(response.code == 200); - } -} - -static import std.file; - -// for a single thread, linear request thing, use: -// -version=embedded_httpd_threads -version=cgi_no_threads - -version(Posix) { - version(CRuntime_Musl) { - - } else version(minimal) { - - } else { - version(GNU) { - // GDC doesn't support static foreach so I had to cheat on it :( - } else version(FreeBSD) { - // I never implemented the fancy stuff there either - } else { - version=with_breaking_cgi_features; - version=with_sendfd; - version=with_addon_servers; - } - } -} - -version(Windows) { - version(minimal) { - - } else { - // not too concerned about gdc here since the mingw version is fairly new as well - version=with_breaking_cgi_features; - } -} - -void cloexec(int fd) { - version(Posix) { - import core.sys.posix.fcntl; - fcntl(fd, F_SETFD, FD_CLOEXEC); - } -} - -void cloexec(Socket s) { - version(Posix) { - import core.sys.posix.fcntl; - fcntl(s.handle, F_SETFD, FD_CLOEXEC); - } -} - -version(embedded_httpd_hybrid) { - version=embedded_httpd_threads; - version(cgi_no_fork) {} else version(Posix) - version=cgi_use_fork; - version=cgi_use_fiber; -} - -version(cgi_use_fork) - enum cgi_use_fork_default = true; -else - enum cgi_use_fork_default = false; - -// the servers must know about the connections to talk to them; the interfaces are vital -version(with_addon_servers) - version=with_addon_servers_connections; - -version(embedded_httpd) { - version(linux) - version=embedded_httpd_processes; - else { - version=embedded_httpd_threads; - } - - /* - version(with_openssl) { - pragma(lib, "crypto"); - pragma(lib, "ssl"); - } - */ -} - -version(embedded_httpd_processes) - version=embedded_httpd_processes_accept_after_fork; // I am getting much better average performance on this, so just keeping it. But the other way MIGHT help keep the variation down so i wanna keep the code to play with later - -version(embedded_httpd_threads) { - // unless the user overrides the default.. - version(cgi_session_server_process) - {} - else - version=cgi_embedded_sessions; -} -version(scgi) { - // unless the user overrides the default.. - version(cgi_session_server_process) - {} - else - version=cgi_embedded_sessions; -} - -// fall back if the other is not defined so we can cleanly version it below -version(cgi_embedded_sessions) {} -else version=cgi_session_server_process; - - -version=cgi_with_websocket; - -enum long defaultMaxContentLength = 5_000_000; - -/* - - To do a file download offer in the browser: - - cgi.setResponseContentType("text/csv"); - cgi.header("Content-Disposition: attachment; filename=\"customers.csv\""); -*/ - -// FIXME: the location header is supposed to be an absolute url I guess. - -// FIXME: would be cool to flush part of a dom document before complete -// somehow in here and dom.d. - - -// these are public so you can mixin GenericMain. -// FIXME: use a function level import instead! -public import std.string; -public import std.stdio; -public import std.conv; -import std.uri; -import std.uni; -import std.algorithm.comparison; -import std.algorithm.searching; -import std.exception; -import std.base64; -static import std.algorithm; -import std.datetime; -import std.range; - -import std.process; - -import std.zlib; - - -T[] consume(T)(T[] range, int count) { - if(count > range.length) - count = range.length; - return range[count..$]; -} - -int locationOf(T)(T[] data, string item) { - const(ubyte[]) d = cast(const(ubyte[])) data; - const(ubyte[]) i = cast(const(ubyte[])) item; - - // this is a vague sanity check to ensure we aren't getting insanely - // sized input that will infinite loop below. it should never happen; - // even huge file uploads ought to come in smaller individual pieces. - if(d.length > (int.max/2)) - throw new Exception("excessive block of input"); - - for(int a = 0; a < d.length; a++) { - if(a + i.length > d.length) - return -1; - if(d[a..a+i.length] == i) - return a; - } - - return -1; -} - -/// If you are doing a custom cgi class, mixing this in can take care of -/// the required constructors for you -mixin template ForwardCgiConstructors() { - this(long maxContentLength = defaultMaxContentLength, - string[string] env = null, - const(ubyte)[] delegate() readdata = null, - void delegate(const(ubyte)[]) _rawDataOutput = null, - void delegate() _flush = null - ) { super(maxContentLength, env, readdata, _rawDataOutput, _flush); } - - this(string[] args) { super(args); } - - this( - BufferedInputRange inputData, - string address, ushort _port, - int pathInfoStarts = 0, - bool _https = false, - void delegate(const(ubyte)[]) _rawDataOutput = null, - void delegate() _flush = null, - // this pointer tells if the connection is supposed to be closed after we handle this - bool* closeConnection = null) - { - super(inputData, address, _port, pathInfoStarts, _https, _rawDataOutput, _flush, closeConnection); - } - - this(BufferedInputRange ir, bool* closeConnection) { super(ir, closeConnection); } -} - -/// thrown when a connection is closed remotely while we waiting on data from it -class ConnectionClosedException : Exception { - this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { - super(message, file, line, next); - } -} - - -version(Windows) { -// FIXME: ugly hack to solve stdin exception problems on Windows: -// reading stdin results in StdioException (Bad file descriptor) -// this is probably due to http://d.puremagic.com/issues/show_bug.cgi?id=3425 -private struct stdin { - struct ByChunk { // Replicates std.stdio.ByChunk - private: - ubyte[] chunk_; - public: - this(size_t size) - in { - assert(size, "size must be larger than 0"); - } - do { - chunk_ = new ubyte[](size); - popFront(); - } - - @property bool empty() const { - return !std.stdio.stdin.isOpen || std.stdio.stdin.eof; // Ugly, but seems to do the job - } - @property nothrow ubyte[] front() { return chunk_; } - void popFront() { - enforce(!empty, "Cannot call popFront on empty range"); - chunk_ = stdin.rawRead(chunk_); - } - } - - import core.sys.windows.windows; -static: - - T[] rawRead(T)(T[] buf) { - uint bytesRead; - auto result = ReadFile(GetStdHandle(STD_INPUT_HANDLE), buf.ptr, cast(int) (buf.length * T.sizeof), &bytesRead, null); - - if (!result) { - auto err = GetLastError(); - if (err == 38/*ERROR_HANDLE_EOF*/ || err == 109/*ERROR_BROKEN_PIPE*/) // 'good' errors meaning end of input - return buf[0..0]; - // Some other error, throw it - - char* buffer; - scope(exit) LocalFree(buffer); - - // FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100 - // FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 - FormatMessageA(0x1100, null, err, 0, cast(char*)&buffer, 256, null); - throw new Exception(to!string(buffer)); - } - enforce(!(bytesRead % T.sizeof), "I/O error"); - return buf[0..bytesRead / T.sizeof]; - } - - auto byChunk(size_t sz) { return ByChunk(sz); } - - void close() { - std.stdio.stdin.close; - } -} -} - -/// The main interface with the web request -class Cgi { - public: - /// the methods a request can be - enum RequestMethod { GET, HEAD, POST, PUT, DELETE, // GET and POST are the ones that really work - // these are defined in the standard, but idk if they are useful for anything - OPTIONS, TRACE, CONNECT, - // These seem new, I have only recently seen them - PATCH, MERGE, - // this is an extension for when the method is not specified and you want to assume - CommandLine } - - - /+ - /++ - Cgi provides a per-request memory pool - - +/ - void[] allocateMemory(size_t nBytes) { - - } - - /// ditto - void[] reallocateMemory(void[] old, size_t nBytes) { - - } - - /// ditto - void freeMemory(void[] memory) { - - } - +/ - - -/* - import core.runtime; - auto args = Runtime.args(); - - we can call the app a few ways: - - 1) set up the environment variables and call the app (manually simulating CGI) - 2) simulate a call automatically: - ./app method 'uri' - - for example: - ./app get /path?arg arg2=something - - Anything on the uri is treated as query string etc - - on get method, further args are appended to the query string (encoded automatically) - on post method, further args are done as post - - - @name means import from file "name". if name == -, it uses stdin - (so info=@- means set info to the value of stdin) - - - Other arguments include: - --cookie name=value (these are all concated together) - --header 'X-Something: cool' - --referrer 'something' - --port 80 - --remote-address some.ip.address.here - --https yes - --user-agent 'something' - --userpass 'user:pass' - --authorization 'Basic base64encoded_user:pass' - --accept 'content' // FIXME: better example - --last-event-id 'something' - --host 'something.com' - - Non-simulation arguments: - --port xxx listening port for non-cgi things (valid for the cgi interfaces) - --listening-host the ip address the application should listen on, or if you want to use unix domain sockets, it is here you can set them: `--listening-host unix:filename` or, on Linux, `--listening-host abstract:name`. - -*/ - - /** Initializes it with command line arguments (for easy testing) */ - this(string[] args, void delegate(const(ubyte)[]) _rawDataOutput = null) { - rawDataOutput = _rawDataOutput; - // these are all set locally so the loop works - // without triggering errors in dmd 2.064 - // we go ahead and set them at the end of it to the this version - int port; - string referrer; - string remoteAddress; - string userAgent; - string authorization; - string origin; - string accept; - string lastEventId; - bool https; - string host; - RequestMethod requestMethod; - string requestUri; - string pathInfo; - string queryString; - - bool lookingForMethod; - bool lookingForUri; - string nextArgIs; - - string _cookie; - string _queryString; - string[][string] _post; - string[string] _headers; - - string[] breakUp(string s) { - string k, v; - auto idx = s.indexOf("="); - if(idx == -1) { - k = s; - } else { - k = s[0 .. idx]; - v = s[idx + 1 .. $]; - } - - return [k, v]; - } - - lookingForMethod = true; - - scriptName = args[0]; - scriptFileName = args[0]; - - environmentVariables = cast(const) environment.toAA; - - foreach(arg; args[1 .. $]) { - if(arg.startsWith("--")) { - nextArgIs = arg[2 .. $]; - } else if(nextArgIs.length) { - if (nextArgIs == "cookie") { - auto info = breakUp(arg); - if(_cookie.length) - _cookie ~= "; "; - _cookie ~= std.uri.encodeComponent(info[0]) ~ "=" ~ std.uri.encodeComponent(info[1]); - } - else if (nextArgIs == "port") { - port = to!int(arg); - } - else if (nextArgIs == "referrer") { - referrer = arg; - } - else if (nextArgIs == "remote-address") { - remoteAddress = arg; - } - else if (nextArgIs == "user-agent") { - userAgent = arg; - } - else if (nextArgIs == "authorization") { - authorization = arg; - } - else if (nextArgIs == "userpass") { - authorization = "Basic " ~ Base64.encode(cast(immutable(ubyte)[]) (arg)).idup; - } - else if (nextArgIs == "origin") { - origin = arg; - } - else if (nextArgIs == "accept") { - accept = arg; - } - else if (nextArgIs == "last-event-id") { - lastEventId = arg; - } - else if (nextArgIs == "https") { - if(arg == "yes") - https = true; - } - else if (nextArgIs == "header") { - string thing, other; - auto idx = arg.indexOf(":"); - if(idx == -1) - throw new Exception("need a colon in a http header"); - thing = arg[0 .. idx]; - other = arg[idx + 1.. $]; - _headers[thing.strip.toLower()] = other.strip; - } - else if (nextArgIs == "host") { - host = arg; - } - // else - // skip, we don't know it but that's ok, it might be used elsewhere so no error - - nextArgIs = null; - } else if(lookingForMethod) { - lookingForMethod = false; - lookingForUri = true; - - if(arg.asLowerCase().equal("commandline")) - requestMethod = RequestMethod.CommandLine; - else - requestMethod = to!RequestMethod(arg.toUpper()); - } else if(lookingForUri) { - lookingForUri = false; - - requestUri = arg; - - auto idx = arg.indexOf("?"); - if(idx == -1) - pathInfo = arg; - else { - pathInfo = arg[0 .. idx]; - _queryString = arg[idx + 1 .. $]; - } - } else { - // it is an argument of some sort - if(requestMethod == Cgi.RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT || requestMethod == Cgi.RequestMethod.CommandLine) { - auto parts = breakUp(arg); - _post[parts[0]] ~= parts[1]; - allPostNamesInOrder ~= parts[0]; - allPostValuesInOrder ~= parts[1]; - } else { - if(_queryString.length) - _queryString ~= "&"; - auto parts = breakUp(arg); - _queryString ~= std.uri.encodeComponent(parts[0]) ~ "=" ~ std.uri.encodeComponent(parts[1]); - } - } - } - - acceptsGzip = false; - keepAliveRequested = false; - requestHeaders = cast(immutable) _headers; - - cookie = _cookie; - cookiesArray = getCookieArray(); - cookies = keepLastOf(cookiesArray); - - queryString = _queryString; - getArray = cast(immutable) decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder); - get = keepLastOf(getArray); - - postArray = cast(immutable) _post; - post = keepLastOf(_post); - - // FIXME - filesArray = null; - files = null; - - isCalledWithCommandLineArguments = true; - - this.port = port; - this.referrer = referrer; - this.remoteAddress = remoteAddress; - this.userAgent = userAgent; - this.authorization = authorization; - this.origin = origin; - this.accept = accept; - this.lastEventId = lastEventId; - this.https = https; - this.host = host; - this.requestMethod = requestMethod; - this.requestUri = requestUri; - this.pathInfo = pathInfo; - this.queryString = queryString; - this.postBody = null; - } - - private { - string[] allPostNamesInOrder; - string[] allPostValuesInOrder; - string[] allGetNamesInOrder; - string[] allGetValuesInOrder; - } - - CgiConnectionHandle getOutputFileHandle() { - return _outputFileHandle; - } - - CgiConnectionHandle _outputFileHandle = INVALID_CGI_CONNECTION_HANDLE; - - /** Initializes it using a CGI or CGI-like interface */ - this(long maxContentLength = defaultMaxContentLength, - // use this to override the environment variable listing - in string[string] env = null, - // and this should return a chunk of data. return empty when done - const(ubyte)[] delegate() readdata = null, - // finally, use this to do custom output if needed - void delegate(const(ubyte)[]) _rawDataOutput = null, - // to flush teh custom output - void delegate() _flush = null - ) - { - - // these are all set locally so the loop works - // without triggering errors in dmd 2.064 - // we go ahead and set them at the end of it to the this version - int port; - string referrer; - string remoteAddress; - string userAgent; - string authorization; - string origin; - string accept; - string lastEventId; - bool https; - string host; - RequestMethod requestMethod; - string requestUri; - string pathInfo; - string queryString; - - - - isCalledWithCommandLineArguments = false; - rawDataOutput = _rawDataOutput; - flushDelegate = _flush; - auto getenv = delegate string(string var) { - if(env is null) - return std.process.environment.get(var); - auto e = var in env; - if(e is null) - return null; - return *e; - }; - - environmentVariables = env is null ? - cast(const) environment.toAA : - env; - - // fetching all the request headers - string[string] requestHeadersHere; - foreach(k, v; env is null ? cast(const) environment.toAA() : env) { - if(k.startsWith("HTTP_")) { - requestHeadersHere[replace(k["HTTP_".length .. $].toLower(), "_", "-")] = v; - } - } - - this.requestHeaders = assumeUnique(requestHeadersHere); - - requestUri = getenv("REQUEST_URI"); - - cookie = getenv("HTTP_COOKIE"); - cookiesArray = getCookieArray(); - cookies = keepLastOf(cookiesArray); - - referrer = getenv("HTTP_REFERER"); - userAgent = getenv("HTTP_USER_AGENT"); - remoteAddress = getenv("REMOTE_ADDR"); - host = getenv("HTTP_HOST"); - pathInfo = getenv("PATH_INFO"); - - queryString = getenv("QUERY_STRING"); - scriptName = getenv("SCRIPT_NAME"); - { - import core.runtime; - auto sfn = getenv("SCRIPT_FILENAME"); - scriptFileName = sfn.length ? sfn : (Runtime.args.length ? Runtime.args[0] : null); - } - - bool iis = false; - - // Because IIS doesn't pass requestUri, we simulate it here if it's empty. - if(requestUri.length == 0) { - // IIS sometimes includes the script name as part of the path info - we don't want that - if(pathInfo.length >= scriptName.length && (pathInfo[0 .. scriptName.length] == scriptName)) - pathInfo = pathInfo[scriptName.length .. $]; - - requestUri = scriptName ~ pathInfo ~ (queryString.length ? ("?" ~ queryString) : ""); - - iis = true; // FIXME HACK - used in byChunk below - see bugzilla 6339 - - // FIXME: this works for apache and iis... but what about others? - } - - - auto ugh = decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder); - getArray = assumeUnique(ugh); - get = keepLastOf(getArray); - - - // NOTE: on shitpache, you need to specifically forward this - authorization = getenv("HTTP_AUTHORIZATION"); - // this is a hack because Apache is a shitload of fuck and - // refuses to send the real header to us. Compatible - // programs should send both the standard and X- versions - - // NOTE: if you have access to .htaccess or httpd.conf, you can make this - // unnecessary with mod_rewrite, so it is commented - - //if(authorization.length == 0) // if the std is there, use it - // authorization = getenv("HTTP_X_AUTHORIZATION"); - - // the REDIRECT_HTTPS check is here because with an Apache hack, the port can become wrong - if(getenv("SERVER_PORT").length && getenv("REDIRECT_HTTPS") != "on") - port = to!int(getenv("SERVER_PORT")); - else - port = 0; // this was probably called from the command line - - auto ae = getenv("HTTP_ACCEPT_ENCODING"); - if(ae.length && ae.indexOf("gzip") != -1) - acceptsGzip = true; - - accept = getenv("HTTP_ACCEPT"); - lastEventId = getenv("HTTP_LAST_EVENT_ID"); - - auto ka = getenv("HTTP_CONNECTION"); - if(ka.length && ka.asLowerCase().canFind("keep-alive")) - keepAliveRequested = true; - - auto or = getenv("HTTP_ORIGIN"); - origin = or; - - auto rm = getenv("REQUEST_METHOD"); - if(rm.length) - requestMethod = to!RequestMethod(getenv("REQUEST_METHOD")); - else - requestMethod = RequestMethod.CommandLine; - - // FIXME: hack on REDIRECT_HTTPS; this is there because the work app uses mod_rewrite which loses the https flag! So I set it with [E=HTTPS=%HTTPS] or whatever but then it gets translated to here so i want it to still work. This is arguably wrong but meh. - https = (getenv("HTTPS") == "on" || getenv("REDIRECT_HTTPS") == "on"); - - // FIXME: DOCUMENT_ROOT? - - // FIXME: what about PUT? - if(requestMethod == RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT || requestMethod == Cgi.RequestMethod.CommandLine) { - version(preserveData) // a hack to make forwarding simpler - immutable(ubyte)[] data; - size_t amountReceived = 0; - auto contentType = getenv("CONTENT_TYPE"); - - // FIXME: is this ever not going to be set? I guess it depends - // on if the server de-chunks and buffers... seems like it has potential - // to be slow if they did that. The spec says it is always there though. - // And it has worked reliably for me all year in the live environment, - // but some servers might be different. - auto cls = getenv("CONTENT_LENGTH"); - auto contentLength = to!size_t(cls.length ? cls : "0"); - - immutable originalContentLength = contentLength; - if(contentLength) { - if(maxContentLength > 0 && contentLength > maxContentLength) { - setResponseStatus("413 Request entity too large"); - write("You tried to upload a file that is too large."); - close(); - throw new Exception("POST too large"); - } - prepareForIncomingDataChunks(contentType, contentLength); - - - int processChunk(in ubyte[] chunk) { - if(chunk.length > contentLength) { - handleIncomingDataChunk(chunk[0..contentLength]); - amountReceived += contentLength; - contentLength = 0; - return 1; - } else { - handleIncomingDataChunk(chunk); - contentLength -= chunk.length; - amountReceived += chunk.length; - } - if(contentLength == 0) - return 1; - - onRequestBodyDataReceived(amountReceived, originalContentLength); - return 0; - } - - - if(readdata is null) { - foreach(ubyte[] chunk; stdin.byChunk(iis ? contentLength : 4096)) - if(processChunk(chunk)) - break; - } else { - // we have a custom data source.. - auto chunk = readdata(); - while(chunk.length) { - if(processChunk(chunk)) - break; - chunk = readdata(); - } - } - - onRequestBodyDataReceived(amountReceived, originalContentLength); - postArray = assumeUnique(pps._post); - filesArray = assumeUnique(pps._files); - files = keepLastOf(filesArray); - post = keepLastOf(postArray); - this.postBody = pps.postBody; - cleanUpPostDataState(); - } - - version(preserveData) - originalPostData = data; - } - // fixme: remote_user script name - - - this.port = port; - this.referrer = referrer; - this.remoteAddress = remoteAddress; - this.userAgent = userAgent; - this.authorization = authorization; - this.origin = origin; - this.accept = accept; - this.lastEventId = lastEventId; - this.https = https; - this.host = host; - this.requestMethod = requestMethod; - this.requestUri = requestUri; - this.pathInfo = pathInfo; - this.queryString = queryString; - } - - /// Cleans up any temporary files. Do not use the object - /// after calling this. - /// - /// NOTE: it is called automatically by GenericMain - // FIXME: this should be called if the constructor fails too, if it has created some garbage... - void dispose() { - foreach(file; files) { - if(!file.contentInMemory) - if(std.file.exists(file.contentFilename)) - std.file.remove(file.contentFilename); - } - } - - private { - struct PostParserState { - string contentType; - string boundary; - string localBoundary; // the ones used at the end or something lol - bool isMultipart; - bool needsSavedBody; - - ulong expectedLength; - ulong contentConsumed; - immutable(ubyte)[] buffer; - - // multipart parsing state - int whatDoWeWant; - bool weHaveAPart; - string[] thisOnesHeaders; - immutable(ubyte)[] thisOnesData; - - string postBody; - - UploadedFile piece; - bool isFile = false; - - size_t memoryCommitted; - - // do NOT keep mutable references to these anywhere! - // I assume they are unique in the constructor once we're all done getting data. - string[][string] _post; - UploadedFile[][string] _files; - } - - PostParserState pps; - } - - /// This represents a file the user uploaded via a POST request. - static struct UploadedFile { - /// If you want to create one of these structs for yourself from some data, - /// use this function. - static UploadedFile fromData(immutable(void)[] data, string name = null) { - Cgi.UploadedFile f; - f.filename = name; - f.content = cast(immutable(ubyte)[]) data; - f.contentInMemory = true; - return f; - } - - string name; /// The name of the form element. - string filename; /// The filename the user set. - string contentType; /// The MIME type the user's browser reported. (Not reliable.) - - /** - For small files, cgi.d will buffer the uploaded file in memory, and make it - directly accessible to you through the content member. I find this very convenient - and somewhat efficient, since it can avoid hitting the disk entirely. (I - often want to inspect and modify the file anyway!) - - I find the file is very large, it is undesirable to eat that much memory just - for a file buffer. In those cases, if you pass a large enough value for maxContentLength - to the constructor so they are accepted, cgi.d will write the content to a temporary - file that you can re-read later. - - You can override this behavior by subclassing Cgi and overriding the protected - handlePostChunk method. Note that the object is not initialized when you - write that method - the http headers are available, but the cgi.post method - is not. You may parse the file as it streams in using this method. - - - Anyway, if the file is small enough to be in memory, contentInMemory will be - set to true, and the content is available in the content member. - - If not, contentInMemory will be set to false, and the content saved in a file, - whose name will be available in the contentFilename member. - - - Tip: if you know you are always dealing with small files, and want the convenience - of ignoring this member, construct Cgi with a small maxContentLength. Then, if - a large file comes in, it simply throws an exception (and HTTP error response) - instead of trying to handle it. - - The default value of maxContentLength in the constructor is for small files. - */ - bool contentInMemory = true; // the default ought to always be true - immutable(ubyte)[] content; /// The actual content of the file, if contentInMemory == true - string contentFilename; /// the file where we dumped the content, if contentInMemory == false. Note that if you want to keep it, you MUST move the file, since otherwise it is considered garbage when cgi is disposed. - - /// - ulong fileSize() { - if(contentInMemory) - return content.length; - import std.file; - return std.file.getSize(contentFilename); - - } - - /// - void writeToFile(string filenameToSaveTo) const { - import std.file; - if(contentInMemory) - std.file.write(filenameToSaveTo, content); - else - std.file.rename(contentFilename, filenameToSaveTo); - } - } - - // given a content type and length, decide what we're going to do with the data.. - protected void prepareForIncomingDataChunks(string contentType, ulong contentLength) { - pps.expectedLength = contentLength; - - auto terminator = contentType.indexOf(";"); - if(terminator == -1) - terminator = contentType.length; - - pps.contentType = contentType[0 .. terminator]; - auto b = contentType[terminator .. $]; - if(b.length) { - auto idx = b.indexOf("boundary="); - if(idx != -1) { - pps.boundary = b[idx + "boundary=".length .. $]; - pps.localBoundary = "\r\n--" ~ pps.boundary; - } - } - - // while a content type SHOULD be sent according to the RFC, it is - // not required. We're told we SHOULD guess by looking at the content - // but it seems to me that this only happens when it is urlencoded. - if(pps.contentType == "application/x-www-form-urlencoded" || pps.contentType == "") { - pps.isMultipart = false; - pps.needsSavedBody = false; - } else if(pps.contentType == "multipart/form-data") { - pps.isMultipart = true; - enforce(pps.boundary.length, "no boundary"); - } else if(pps.contentType == "text/xml") { // FIXME: could this be special and load the post params - // save the body so the application can handle it - pps.isMultipart = false; - pps.needsSavedBody = true; - } else if(pps.contentType == "application/json") { // FIXME: this could prolly try to load post params too - // save the body so the application can handle it - pps.needsSavedBody = true; - pps.isMultipart = false; - } else { - // the rest is 100% handled by the application. just save the body and send it to them - pps.needsSavedBody = true; - pps.isMultipart = false; - } - } - - // handles streaming POST data. If you handle some other content type, you should - // override this. If the data isn't the content type you want, you ought to call - // super.handleIncomingDataChunk so regular forms and files still work. - - // FIXME: I do some copying in here that I'm pretty sure is unnecessary, and the - // file stuff I'm sure is inefficient. But, my guess is the real bottleneck is network - // input anyway, so I'm not going to get too worked up about it right now. - protected void handleIncomingDataChunk(const(ubyte)[] chunk) { - if(chunk.length == 0) - return; - assert(chunk.length <= 32 * 1024 * 1024); // we use chunk size as a memory constraint thing, so - // if we're passed big chunks, it might throw unnecessarily. - // just pass it smaller chunks at a time. - if(pps.isMultipart) { - // multipart/form-data - - - // FIXME: this might want to be factored out and factorized - // need to make sure the stream hooks actually work. - void pieceHasNewContent() { - // we just grew the piece's buffer. Do we have to switch to file backing? - if(pps.piece.contentInMemory) { - if(pps.piece.content.length <= 10 * 1024 * 1024) - // meh, I'm ok with it. - return; - else { - // this is too big. - if(!pps.isFile) - throw new Exception("Request entity too large"); // a variable this big is kinda ridiculous, just reject it. - else { - // a file this large is probably acceptable though... let's use a backing file. - pps.piece.contentInMemory = false; - // FIXME: say... how do we intend to delete these things? cgi.dispose perhaps. - - int count = 0; - pps.piece.contentFilename = getTempDirectory() ~ "arsd_cgi_uploaded_file_" ~ to!string(getUtcTime()) ~ "-" ~ to!string(count); - // odds are this loop will never be entered, but we want it just in case. - while(std.file.exists(pps.piece.contentFilename)) { - count++; - pps.piece.contentFilename = getTempDirectory() ~ "arsd_cgi_uploaded_file_" ~ to!string(getUtcTime()) ~ "-" ~ to!string(count); - } - // I hope this creates the file pretty quickly, or the loop might be useless... - // FIXME: maybe I should write some kind of custom transaction here. - std.file.write(pps.piece.contentFilename, pps.piece.content); - - pps.piece.content = null; - } - } - } else { - // it's already in a file, so just append it to what we have - if(pps.piece.content.length) { - // FIXME: this is surely very inefficient... we'll be calling this by 4kb chunk... - std.file.append(pps.piece.contentFilename, pps.piece.content); - pps.piece.content = null; - } - } - } - - - void commitPart() { - if(!pps.weHaveAPart) - return; - - pieceHasNewContent(); // be sure the new content is handled every time - - if(pps.isFile) { - // I'm not sure if other environments put files in post or not... - // I used to not do it, but I think I should, since it is there... - pps._post[pps.piece.name] ~= pps.piece.filename; - pps._files[pps.piece.name] ~= pps.piece; - - allPostNamesInOrder ~= pps.piece.name; - allPostValuesInOrder ~= pps.piece.filename; - } else { - pps._post[pps.piece.name] ~= cast(string) pps.piece.content; - - allPostNamesInOrder ~= pps.piece.name; - allPostValuesInOrder ~= cast(string) pps.piece.content; - } - - /* - stderr.writeln("RECEIVED: ", pps.piece.name, "=", - pps.piece.content.length < 1000 - ? - to!string(pps.piece.content) - : - "too long"); - */ - - // FIXME: the limit here - pps.memoryCommitted += pps.piece.content.length; - - pps.weHaveAPart = false; - pps.whatDoWeWant = 1; - pps.thisOnesHeaders = null; - pps.thisOnesData = null; - - pps.piece = UploadedFile.init; - pps.isFile = false; - } - - void acceptChunk() { - pps.buffer ~= chunk; - chunk = null; // we've consumed it into the buffer, so keeping it just brings confusion - } - - immutable(ubyte)[] consume(size_t howMuch) { - pps.contentConsumed += howMuch; - auto ret = pps.buffer[0 .. howMuch]; - pps.buffer = pps.buffer[howMuch .. $]; - return ret; - } - - dataConsumptionLoop: do { - switch(pps.whatDoWeWant) { - default: assert(0); - case 0: - acceptChunk(); - // the format begins with two extra leading dashes, then we should be at the boundary - if(pps.buffer.length < 2) - return; - assert(pps.buffer[0] == '-', "no leading dash"); - consume(1); - assert(pps.buffer[0] == '-', "no second leading dash"); - consume(1); - - pps.whatDoWeWant = 1; - goto case 1; - /* fallthrough */ - case 1: // looking for headers - // here, we should be lined up right at the boundary, which is followed by a \r\n - - // want to keep the buffer under control in case we're under attack - //stderr.writeln("here once"); - //if(pps.buffer.length + chunk.length > 70 * 1024) // they should be < 1 kb really.... - // throw new Exception("wtf is up with the huge mime part headers"); - - acceptChunk(); - - if(pps.buffer.length < pps.boundary.length) - return; // not enough data, since there should always be a boundary here at least - - if(pps.contentConsumed + pps.boundary.length + 6 == pps.expectedLength) { - assert(pps.buffer.length == pps.boundary.length + 4 + 2); // --, --, and \r\n - // we *should* be at the end here! - assert(pps.buffer[0] == '-'); - consume(1); - assert(pps.buffer[0] == '-'); - consume(1); - - // the message is terminated by --BOUNDARY--\r\n (after a \r\n leading to the boundary) - assert(pps.buffer[0 .. pps.boundary.length] == cast(const(ubyte[])) pps.boundary, - "not lined up on boundary " ~ pps.boundary); - consume(pps.boundary.length); - - assert(pps.buffer[0] == '-'); - consume(1); - assert(pps.buffer[0] == '-'); - consume(1); - - assert(pps.buffer[0] == '\r'); - consume(1); - assert(pps.buffer[0] == '\n'); - consume(1); - - assert(pps.buffer.length == 0); - assert(pps.contentConsumed == pps.expectedLength); - break dataConsumptionLoop; // we're done! - } else { - // we're not done yet. We should be lined up on a boundary. - - // But, we want to ensure the headers are here before we consume anything! - auto headerEndLocation = locationOf(pps.buffer, "\r\n\r\n"); - if(headerEndLocation == -1) - return; // they *should* all be here, so we can handle them all at once. - - assert(pps.buffer[0 .. pps.boundary.length] == cast(const(ubyte[])) pps.boundary, - "not lined up on boundary " ~ pps.boundary); - - consume(pps.boundary.length); - // the boundary is always followed by a \r\n - assert(pps.buffer[0] == '\r'); - consume(1); - assert(pps.buffer[0] == '\n'); - consume(1); - } - - // re-running since by consuming the boundary, we invalidate the old index. - auto headerEndLocation = locationOf(pps.buffer, "\r\n\r\n"); - assert(headerEndLocation >= 0, "no header"); - auto thisOnesHeaders = pps.buffer[0..headerEndLocation]; - - consume(headerEndLocation + 4); // The +4 is the \r\n\r\n that caps it off - - pps.thisOnesHeaders = split(cast(string) thisOnesHeaders, "\r\n"); - - // now we'll parse the headers - foreach(h; pps.thisOnesHeaders) { - auto p = h.indexOf(":"); - assert(p != -1, "no colon in header, got " ~ to!string(pps.thisOnesHeaders)); - string hn = h[0..p]; - string hv = h[p+2..$]; - - switch(hn.toLower) { - default: assert(0); - case "content-disposition": - auto info = hv.split("; "); - foreach(i; info[1..$]) { // skipping the form-data - auto o = i.split("="); // FIXME - string pn = o[0]; - string pv = o[1][1..$-1]; - - if(pn == "name") { - pps.piece.name = pv; - } else if (pn == "filename") { - pps.piece.filename = pv; - pps.isFile = true; - } - } - break; - case "content-type": - pps.piece.contentType = hv; - break; - } - } - - pps.whatDoWeWant++; // move to the next step - the data - break; - case 2: - // when we get here, pps.buffer should contain our first chunk of data - - if(pps.buffer.length + chunk.length > 8 * 1024 * 1024) // we might buffer quite a bit but not much - throw new Exception("wtf is up with the huge mime part buffer"); - - acceptChunk(); - - // so the trick is, we want to process all the data up to the boundary, - // but what if the chunk's end cuts the boundary off? If we're unsure, we - // want to wait for the next chunk. We start by looking for the whole boundary - // in the buffer somewhere. - - auto boundaryLocation = locationOf(pps.buffer, pps.localBoundary); - // assert(boundaryLocation != -1, "should have seen "~to!string(cast(ubyte[]) pps.localBoundary)~" in " ~ to!string(pps.buffer)); - if(boundaryLocation != -1) { - // this is easy - we can see it in it's entirety! - - pps.piece.content ~= consume(boundaryLocation); - - assert(pps.buffer[0] == '\r'); - consume(1); - assert(pps.buffer[0] == '\n'); - consume(1); - assert(pps.buffer[0] == '-'); - consume(1); - assert(pps.buffer[0] == '-'); - consume(1); - // the boundary here is always preceded by \r\n--, which is why we used localBoundary instead of boundary to locate it. Cut that off. - pps.weHaveAPart = true; - pps.whatDoWeWant = 1; // back to getting headers for the next part - - commitPart(); // we're done here - } else { - // we can't see the whole thing, but what if there's a partial boundary? - - enforce(pps.localBoundary.length < 128); // the boundary ought to be less than a line... - assert(pps.localBoundary.length > 1); // should already be sane but just in case - bool potentialBoundaryFound = false; - - boundaryCheck: for(int a = 1; a < pps.localBoundary.length; a++) { - // we grow the boundary a bit each time. If we think it looks the - // same, better pull another chunk to be sure it's not the end. - // Starting small because exiting the loop early is desirable, since - // we're not keeping any ambiguity and 1 / 256 chance of exiting is - // the best we can do. - if(a > pps.buffer.length) - break; // FIXME: is this right? - assert(a <= pps.buffer.length); - assert(a > 0); - if(std.algorithm.endsWith(pps.buffer, pps.localBoundary[0 .. a])) { - // ok, there *might* be a boundary here, so let's - // not treat the end as data yet. The rest is good to - // use though, since if there was a boundary there, we'd - // have handled it up above after locationOf. - - pps.piece.content ~= pps.buffer[0 .. $ - a]; - consume(pps.buffer.length - a); - pieceHasNewContent(); - potentialBoundaryFound = true; - break boundaryCheck; - } - } - - if(!potentialBoundaryFound) { - // we can consume the whole thing - pps.piece.content ~= pps.buffer; - pieceHasNewContent(); - consume(pps.buffer.length); - } else { - // we found a possible boundary, but there was - // insufficient data to be sure. - assert(pps.buffer == cast(const(ubyte[])) pps.localBoundary[0 .. pps.buffer.length]); - - return; // wait for the next chunk. - } - } - } - } while(pps.buffer.length); - - // btw all boundaries except the first should have a \r\n before them - } else { - // application/x-www-form-urlencoded and application/json - - // not using maxContentLength because that might be cranked up to allow - // large file uploads. We can handle them, but a huge post[] isn't any good. - if(pps.buffer.length + chunk.length > 8 * 1024 * 1024) // surely this is plenty big enough - throw new Exception("wtf is up with such a gigantic form submission????"); - - pps.buffer ~= chunk; - - // simple handling, but it works... until someone bombs us with gigabytes of crap at least... - if(pps.buffer.length == pps.expectedLength) { - if(pps.needsSavedBody) - pps.postBody = cast(string) pps.buffer; - else - pps._post = decodeVariables(cast(string) pps.buffer, "&", &allPostNamesInOrder, &allPostValuesInOrder); - version(preserveData) - originalPostData = pps.buffer; - } else { - // just for debugging - } - } - } - - protected void cleanUpPostDataState() { - pps = PostParserState.init; - } - - /// you can override this function to somehow react - /// to an upload in progress. - /// - /// Take note that parts of the CGI object is not yet - /// initialized! Stuff from HTTP headers, including get[], is usable. - /// But, none of post[] is usable, and you cannot write here. That's - /// why this method is const - mutating the object won't do much anyway. - /// - /// My idea here was so you can output a progress bar or - /// something to a cooperative client (see arsd.rtud for a potential helper) - /// - /// The default is to do nothing. Subclass cgi and use the - /// CustomCgiMain mixin to do something here. - void onRequestBodyDataReceived(size_t receivedSoFar, size_t totalExpected) const { - // This space intentionally left blank. - } - - /// Initializes the cgi from completely raw HTTP data. The ir must have a Socket source. - /// *closeConnection will be set to true if you should close the connection after handling this request - this(BufferedInputRange ir, bool* closeConnection) { - isCalledWithCommandLineArguments = false; - import al = std.algorithm; - - immutable(ubyte)[] data; - - void rdo(const(ubyte)[] d) { - //import std.stdio; writeln(d); - sendAll(ir.source, d); - } - - auto ira = ir.source.remoteAddress(); - auto irLocalAddress = ir.source.localAddress(); - - ushort port = 80; - if(auto ia = cast(InternetAddress) irLocalAddress) { - port = ia.port; - } else if(auto ia = cast(Internet6Address) irLocalAddress) { - port = ia.port; - } - - // that check for UnixAddress is to work around a Phobos bug - // see: https://github.com/dlang/phobos/pull/7383 - // but this might be more useful anyway tbh for this case - version(Posix) - this(ir, ira is null ? null : cast(UnixAddress) ira ? "unix:" : ira.toString(), port, 0, false, &rdo, null, closeConnection); - else - this(ir, ira is null ? null : ira.toString(), port, 0, false, &rdo, null, closeConnection); - } - - /** - Initializes it from raw HTTP request data. GenericMain uses this when you compile with -version=embedded_httpd. - - NOTE: If you are behind a reverse proxy, the values here might not be what you expect.... it will use X-Forwarded-For for remote IP and X-Forwarded-Host for host - - Params: - inputData = the incoming data, including headers and other raw http data. - When the constructor exits, it will leave this range exactly at the start of - the next request on the connection (if there is one). - - address = the IP address of the remote user - _port = the port number of the connection - pathInfoStarts = the offset into the path component of the http header where the SCRIPT_NAME ends and the PATH_INFO begins. - _https = if this connection is encrypted (note that the input data must not actually be encrypted) - _rawDataOutput = delegate to accept response data. It should write to the socket or whatever; Cgi does all the needed processing to speak http. - _flush = if _rawDataOutput buffers, this delegate should flush the buffer down the wire - closeConnection = if the request asks to close the connection, *closeConnection == true. - */ - this( - BufferedInputRange inputData, -// string[] headers, immutable(ubyte)[] data, - string address, ushort _port, - int pathInfoStarts = 0, // use this if you know the script name, like if this is in a folder in a bigger web environment - bool _https = false, - void delegate(const(ubyte)[]) _rawDataOutput = null, - void delegate() _flush = null, - // this pointer tells if the connection is supposed to be closed after we handle this - bool* closeConnection = null) - { - // these are all set locally so the loop works - // without triggering errors in dmd 2.064 - // we go ahead and set them at the end of it to the this version - int port; - string referrer; - string remoteAddress; - string userAgent; - string authorization; - string origin; - string accept; - string lastEventId; - bool https; - string host; - RequestMethod requestMethod; - string requestUri; - string pathInfo; - string queryString; - string scriptName; - string[string] get; - string[][string] getArray; - bool keepAliveRequested; - bool acceptsGzip; - string cookie; - - - - environmentVariables = cast(const) environment.toAA; - - idlol = inputData; - - isCalledWithCommandLineArguments = false; - - https = _https; - port = _port; - - rawDataOutput = _rawDataOutput; - flushDelegate = _flush; - nph = true; - - remoteAddress = address; - - // streaming parser - import al = std.algorithm; - - // FIXME: tis cast is technically wrong, but Phobos deprecated al.indexOf... for some reason. - auto idx = indexOf(cast(string) inputData.front(), "\r\n\r\n"); - while(idx == -1) { - inputData.popFront(0); - idx = indexOf(cast(string) inputData.front(), "\r\n\r\n"); - } - - assert(idx != -1); - - - string contentType = ""; - string[string] requestHeadersHere; - - size_t contentLength; - - bool isChunked; - - { - import core.runtime; - scriptFileName = Runtime.args.length ? Runtime.args[0] : null; - } - - - int headerNumber = 0; - foreach(line; al.splitter(inputData.front()[0 .. idx], "\r\n")) - if(line.length) { - headerNumber++; - auto header = cast(string) line.idup; - if(headerNumber == 1) { - // request line - auto parts = al.splitter(header, " "); - requestMethod = to!RequestMethod(parts.front); - parts.popFront(); - requestUri = parts.front; - - // FIXME: the requestUri could be an absolute path!!! should I rename it or something? - scriptName = requestUri[0 .. pathInfoStarts]; - - auto question = requestUri.indexOf("?"); - if(question == -1) { - queryString = ""; - // FIXME: double check, this might be wrong since it could be url encoded - pathInfo = requestUri[pathInfoStarts..$]; - } else { - queryString = requestUri[question+1..$]; - pathInfo = requestUri[pathInfoStarts..question]; - } - - auto ugh = decodeVariables(queryString, "&", &allGetNamesInOrder, &allGetValuesInOrder); - getArray = cast(string[][string]) assumeUnique(ugh); - - if(header.indexOf("HTTP/1.0") != -1) { - http10 = true; - autoBuffer = true; - if(closeConnection) { - // on http 1.0, close is assumed (unlike http/1.1 where we assume keep alive) - *closeConnection = true; - } - } - } else { - // other header - auto colon = header.indexOf(":"); - if(colon == -1) - throw new Exception("HTTP headers should have a colon!"); - string name = header[0..colon].toLower; - string value = header[colon+2..$]; // skip the colon and the space - - requestHeadersHere[name] = value; - - if (name == "accept") { - accept = value; - } - else if (name == "origin") { - origin = value; - } - else if (name == "connection") { - if(value == "close" && closeConnection) - *closeConnection = true; - if(value.asLowerCase().canFind("keep-alive")) { - keepAliveRequested = true; - - // on http 1.0, the connection is closed by default, - // but not if they request keep-alive. then we don't close - // anymore - undoing the set above - if(http10 && closeConnection) { - *closeConnection = false; - } - } - } - else if (name == "transfer-encoding") { - if(value == "chunked") - isChunked = true; - } - else if (name == "last-event-id") { - lastEventId = value; - } - else if (name == "authorization") { - authorization = value; - } - else if (name == "content-type") { - contentType = value; - } - else if (name == "content-length") { - contentLength = to!size_t(value); - } - else if (name == "x-forwarded-for") { - remoteAddress = value; - } - else if (name == "x-forwarded-host" || name == "host") { - if(name != "host" || host is null) - host = value; - } - // FIXME: https://tools.ietf.org/html/rfc7239 - else if (name == "accept-encoding") { - if(value.indexOf("gzip") != -1) - acceptsGzip = true; - } - else if (name == "user-agent") { - userAgent = value; - } - else if (name == "referer") { - referrer = value; - } - else if (name == "cookie") { - cookie ~= value; - } else if(name == "expect") { - if(value == "100-continue") { - // FIXME we should probably give user code a chance - // to process and reject but that needs to be virtual, - // perhaps part of the CGI redesign. - - // FIXME: if size is > max content length it should - // also fail at this point. - _rawDataOutput(cast(ubyte[]) "HTTP/1.1 100 Continue\r\n\r\n"); - - // FIXME: let the user write out 103 early hints too - } - } - // else - // ignore it - - } - } - - inputData.consume(idx + 4); - // done - - requestHeaders = assumeUnique(requestHeadersHere); - - ByChunkRange dataByChunk; - - // reading Content-Length type data - // We need to read up the data we have, and write it out as a chunk. - if(!isChunked) { - dataByChunk = byChunk(inputData, contentLength); - } else { - // chunked requests happen, but not every day. Since we need to know - // the content length (for now, maybe that should change), we'll buffer - // the whole thing here instead of parse streaming. (I think this is what Apache does anyway in cgi modes) - auto data = dechunk(inputData); - - // set the range here - dataByChunk = byChunk(data); - contentLength = data.length; - } - - assert(dataByChunk !is null); - - if(contentLength) { - prepareForIncomingDataChunks(contentType, contentLength); - foreach(dataChunk; dataByChunk) { - handleIncomingDataChunk(dataChunk); - } - postArray = assumeUnique(pps._post); - filesArray = assumeUnique(pps._files); - files = keepLastOf(filesArray); - post = keepLastOf(postArray); - postBody = pps.postBody; - cleanUpPostDataState(); - } - - this.port = port; - this.referrer = referrer; - this.remoteAddress = remoteAddress; - this.userAgent = userAgent; - this.authorization = authorization; - this.origin = origin; - this.accept = accept; - this.lastEventId = lastEventId; - this.https = https; - this.host = host; - this.requestMethod = requestMethod; - this.requestUri = requestUri; - this.pathInfo = pathInfo; - this.queryString = queryString; - - this.scriptName = scriptName; - this.get = keepLastOf(getArray); - this.getArray = cast(immutable) getArray; - this.keepAliveRequested = keepAliveRequested; - this.acceptsGzip = acceptsGzip; - this.cookie = cookie; - - cookiesArray = getCookieArray(); - cookies = keepLastOf(cookiesArray); - - } - BufferedInputRange idlol; - - private immutable(string[string]) keepLastOf(in string[][string] arr) { - string[string] ca; - foreach(k, v; arr) - ca[k] = v[$-1]; - - return assumeUnique(ca); - } - - // FIXME duplication - private immutable(UploadedFile[string]) keepLastOf(in UploadedFile[][string] arr) { - UploadedFile[string] ca; - foreach(k, v; arr) - ca[k] = v[$-1]; - - return assumeUnique(ca); - } - - - private immutable(string[][string]) getCookieArray() { - auto forTheLoveOfGod = decodeVariables(cookie, "; "); - return assumeUnique(forTheLoveOfGod); - } - - /// Very simple method to require a basic auth username and password. - /// If the http request doesn't include the required credentials, it throws a - /// HTTP 401 error, and an exception. - /// - /// Note: basic auth does not provide great security, especially over unencrypted HTTP; - /// the user's credentials are sent in plain text on every request. - /// - /// If you are using Apache, the HTTP_AUTHORIZATION variable may not be sent to the - /// application. Either use Apache's built in methods for basic authentication, or add - /// something along these lines to your server configuration: - /// - /// RewriteEngine On - /// RewriteCond %{HTTP:Authorization} ^(.*) - /// RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1] - /// - /// To ensure the necessary data is available to cgi.d. - void requireBasicAuth(string user, string pass, string message = null) { - if(authorization != "Basic " ~ Base64.encode(cast(immutable(ubyte)[]) (user ~ ":" ~ pass))) { - setResponseStatus("401 Authorization Required"); - header ("WWW-Authenticate: Basic realm=\""~message~"\""); - close(); - throw new Exception("Not authorized; got " ~ authorization); - } - } - - /// Very simple caching controls - setCache(false) means it will never be cached. Good for rapidly updated or sensitive sites. - /// setCache(true) means it will always be cached for as long as possible. Best for static content. - /// Use setResponseExpires and updateResponseExpires for more control - void setCache(bool allowCaching) { - noCache = !allowCaching; - } - - /// Set to true and use cgi.write(data, true); to send a gzipped response to browsers - /// who can accept it - bool gzipResponse; - - immutable bool acceptsGzip; - immutable bool keepAliveRequested; - - /// Set to true if and only if this was initialized with command line arguments - immutable bool isCalledWithCommandLineArguments; - - /// This gets a full url for the current request, including port, protocol, host, path, and query - string getCurrentCompleteUri() const { - ushort defaultPort = https ? 443 : 80; - - string uri = "http"; - if(https) - uri ~= "s"; - uri ~= "://"; - uri ~= host; - /+ // the host has the port so p sure this never needed, cgi on apache and embedded http all do the right hting now - version(none) - if(!(!port || port == defaultPort)) { - uri ~= ":"; - uri ~= to!string(port); - } - +/ - uri ~= requestUri; - return uri; - } - - /// You can override this if your site base url isn't the same as the script name - string logicalScriptName() const { - return scriptName; - } - - /++ - Sets the HTTP status of the response. For example, "404 File Not Found" or "500 Internal Server Error". - It assumes "200 OK", and automatically changes to "302 Found" if you call setResponseLocation(). - Note setResponseStatus() must be called *before* you write() any data to the output. - - History: - The `int` overload was added on January 11, 2021. - +/ - void setResponseStatus(string status) { - assert(!outputtedResponseData); - responseStatus = status; - } - /// ditto - void setResponseStatus(int statusCode) { - setResponseStatus(getHttpCodeText(statusCode)); - } - private string responseStatus = null; - - /// Returns true if it is still possible to output headers - bool canOutputHeaders() { - return !isClosed && !outputtedResponseData; - } - - /// Sets the location header, which the browser will redirect the user to automatically. - /// Note setResponseLocation() must be called *before* you write() any data to the output. - /// The optional important argument is used if it's a default suggestion rather than something to insist upon. - void setResponseLocation(string uri, bool important = true, string status = null) { - if(!important && isCurrentResponseLocationImportant) - return; // important redirects always override unimportant ones - - if(uri is null) { - responseStatus = "200 OK"; - responseLocation = null; - isCurrentResponseLocationImportant = important; - return; // this just cancels the redirect - } - - assert(!outputtedResponseData); - if(status is null) - responseStatus = "302 Found"; - else - responseStatus = status; - - responseLocation = uri.strip; - isCurrentResponseLocationImportant = important; - } - protected string responseLocation = null; - private bool isCurrentResponseLocationImportant = false; - - /// Sets the Expires: http header. See also: updateResponseExpires, setPublicCaching - /// The parameter is in unix_timestamp * 1000. Try setResponseExpires(getUTCtime() + SOME AMOUNT) for normal use. - /// Note: the when parameter is different than setCookie's expire parameter. - void setResponseExpires(long when, bool isPublic = false) { - responseExpires = when; - setCache(true); // need to enable caching so the date has meaning - - responseIsPublic = isPublic; - responseExpiresRelative = false; - } - - /// Sets a cache-control max-age header for whenFromNow, in seconds. - void setResponseExpiresRelative(int whenFromNow, bool isPublic = false) { - responseExpires = whenFromNow; - setCache(true); // need to enable caching so the date has meaning - - responseIsPublic = isPublic; - responseExpiresRelative = true; - } - private long responseExpires = long.min; - private bool responseIsPublic = false; - private bool responseExpiresRelative = false; - - /// This is like setResponseExpires, but it can be called multiple times. The setting most in the past is the one kept. - /// If you have multiple functions, they all might call updateResponseExpires about their own return value. The program - /// output as a whole is as cacheable as the least cachable part in the chain. - - /// setCache(false) always overrides this - it is, by definition, the strictest anti-cache statement available. If your site outputs sensitive user data, you should probably call setCache(false) when you do, to ensure no other functions will cache the content, as it may be a privacy risk. - /// Conversely, setting here overrides setCache(true), since any expiration date is in the past of infinity. - void updateResponseExpires(long when, bool isPublic) { - if(responseExpires == long.min) - setResponseExpires(when, isPublic); - else if(when < responseExpires) - setResponseExpires(when, responseIsPublic && isPublic); // if any part of it is private, it all is - } - - /* - /// Set to true if you want the result to be cached publically - that is, is the content shared? - /// Should generally be false if the user is logged in. It assumes private cache only. - /// setCache(true) also turns on public caching, and setCache(false) sets to private. - void setPublicCaching(bool allowPublicCaches) { - publicCaching = allowPublicCaches; - } - private bool publicCaching = false; - */ - - /++ - History: - Added January 11, 2021 - +/ - enum SameSitePolicy { - Lax, - Strict, - None - } - - /++ - Sets an HTTP cookie, automatically encoding the data to the correct string. - expiresIn is how many milliseconds in the future the cookie will expire. - TIP: to make a cookie accessible from subdomains, set the domain to .yourdomain.com. - Note setCookie() must be called *before* you write() any data to the output. - - History: - Parameter `sameSitePolicy` was added on January 11, 2021. - +/ - void setCookie(string name, string data, long expiresIn = 0, string path = null, string domain = null, bool httpOnly = false, bool secure = false, SameSitePolicy sameSitePolicy = SameSitePolicy.Lax) { - assert(!outputtedResponseData); - string cookie = std.uri.encodeComponent(name) ~ "="; - cookie ~= std.uri.encodeComponent(data); - if(path !is null) - cookie ~= "; path=" ~ path; - // FIXME: should I just be using max-age here? (also in cache below) - if(expiresIn != 0) - cookie ~= "; expires=" ~ printDate(cast(DateTime) Clock.currTime(UTC()) + dur!"msecs"(expiresIn)); - if(domain !is null) - cookie ~= "; domain=" ~ domain; - if(secure == true) - cookie ~= "; Secure"; - if(httpOnly == true ) - cookie ~= "; HttpOnly"; - final switch(sameSitePolicy) { - case SameSitePolicy.Lax: - cookie ~= "; SameSite=Lax"; - break; - case SameSitePolicy.Strict: - cookie ~= "; SameSite=Strict"; - break; - case SameSitePolicy.None: - cookie ~= "; SameSite=None"; - assert(secure); // cookie spec requires this now, see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite - break; - } - - if(auto idx = name in cookieIndexes) { - responseCookies[*idx] = cookie; - } else { - cookieIndexes[name] = responseCookies.length; - responseCookies ~= cookie; - } - } - private string[] responseCookies; - private size_t[string] cookieIndexes; - - /// Clears a previously set cookie with the given name, path, and domain. - void clearCookie(string name, string path = null, string domain = null) { - assert(!outputtedResponseData); - setCookie(name, "", 1, path, domain); - } - - /// Sets the content type of the response, for example "text/html" (the default) for HTML, or "image/png" for a PNG image - void setResponseContentType(string ct) { - assert(!outputtedResponseData); - responseContentType = ct; - } - private string responseContentType = null; - - /// Adds a custom header. It should be the name: value, but without any line terminator. - /// For example: header("X-My-Header: Some value"); - /// Note you should use the specialized functions in this object if possible to avoid - /// duplicates in the output. - void header(string h) { - customHeaders ~= h; - } - - /++ - I named the original function `header` after PHP, but this pattern more fits - the rest of the Cgi object. - - Either name are allowed. - - History: - Alias added June 17, 2022. - +/ - alias setResponseHeader = header; - - private string[] customHeaders; - private bool websocketMode; - - void flushHeaders(const(void)[] t, bool isAll = false) { - StackBuffer buffer = StackBuffer(0); - - prepHeaders(t, isAll, &buffer); - - if(rawDataOutput !is null) - rawDataOutput(cast(const(ubyte)[]) buffer.get()); - else { - stdout.rawWrite(buffer.get()); - } - } - - private void prepHeaders(const(void)[] t, bool isAll, StackBuffer* buffer) { - string terminator = "\n"; - if(rawDataOutput !is null) - terminator = "\r\n"; - - if(responseStatus !is null) { - if(nph) { - if(http10) - buffer.add("HTTP/1.0 ", responseStatus, terminator); - else - buffer.add("HTTP/1.1 ", responseStatus, terminator); - } else - buffer.add("Status: ", responseStatus, terminator); - } else if (nph) { - if(http10) - buffer.add("HTTP/1.0 200 OK", terminator); - else - buffer.add("HTTP/1.1 200 OK", terminator); - } - - if(websocketMode) - goto websocket; - - if(nph) { // we're responsible for setting the date too according to http 1.1 - char[29] db = void; - printDateToBuffer(cast(DateTime) Clock.currTime(UTC()), db[]); - buffer.add("Date: ", db[], terminator); - } - - // FIXME: what if the user wants to set his own content-length? - // The custom header function can do it, so maybe that's best. - // Or we could reuse the isAll param. - if(responseLocation !is null) { - buffer.add("Location: ", responseLocation, terminator); - } - if(!noCache && responseExpires != long.min) { // an explicit expiration date is set - if(responseExpiresRelative) { - buffer.add("Cache-Control: ", responseIsPublic ? "public" : "private", ", max-age="); - buffer.add(responseExpires); - buffer.add(", no-cache=\"set-cookie, set-cookie2\"", terminator); - } else { - auto expires = SysTime(unixTimeToStdTime(cast(int)(responseExpires / 1000)), UTC()); - char[29] db = void; - printDateToBuffer(cast(DateTime) expires, db[]); - buffer.add("Expires: ", db[], terminator); - // FIXME: assuming everything is private unless you use nocache - generally right for dynamic pages, but not necessarily - buffer.add("Cache-Control: ", (responseIsPublic ? "public" : "private"), ", no-cache=\"set-cookie, set-cookie2\""); - buffer.add(terminator); - } - } - if(responseCookies !is null && responseCookies.length > 0) { - foreach(c; responseCookies) - buffer.add("Set-Cookie: ", c, terminator); - } - if(noCache) { // we specifically do not want caching (this is actually the default) - buffer.add("Cache-Control: private, no-cache=\"set-cookie\"", terminator); - buffer.add("Expires: 0", terminator); - buffer.add("Pragma: no-cache", terminator); - } else { - if(responseExpires == long.min) { // caching was enabled, but without a date set - that means assume cache forever - buffer.add("Cache-Control: public", terminator); - buffer.add("Expires: Tue, 31 Dec 2030 14:00:00 GMT", terminator); // FIXME: should not be more than one year in the future - } - } - if(responseContentType !is null) { - buffer.add("Content-Type: ", responseContentType, terminator); - } else - buffer.add("Content-Type: text/html; charset=utf-8", terminator); - - if(gzipResponse && acceptsGzip && isAll) { // FIXME: isAll really shouldn't be necessary - buffer.add("Content-Encoding: gzip", terminator); - } - - - if(!isAll) { - if(nph && !http10) { - buffer.add("Transfer-Encoding: chunked", terminator); - responseChunked = true; - } - } else { - buffer.add("Content-Length: "); - buffer.add(t.length); - buffer.add(terminator); - if(nph && keepAliveRequested) { - buffer.add("Connection: Keep-Alive", terminator); - } - } - - websocket: - - foreach(hd; customHeaders) - buffer.add(hd, terminator); - - // FIXME: what about duplicated headers? - - // end of header indicator - buffer.add(terminator); - - outputtedResponseData = true; - } - - /// Writes the data to the output, flushing headers if they have not yet been sent. - void write(const(void)[] t, bool isAll = false, bool maybeAutoClose = true) { - assert(!closed, "Output has already been closed"); - - StackBuffer buffer = StackBuffer(0); - - if(gzipResponse && acceptsGzip && isAll) { // FIXME: isAll really shouldn't be necessary - // actually gzip the data here - - auto c = new Compress(HeaderFormat.gzip); // want gzip - - auto data = c.compress(t); - data ~= c.flush(); - - // std.file.write("/tmp/last-item", data); - - t = data; - } - - if(!outputtedResponseData && (!autoBuffer || isAll)) { - prepHeaders(t, isAll, &buffer); - } - - if(requestMethod != RequestMethod.HEAD && t.length > 0) { - if (autoBuffer && !isAll) { - outputBuffer ~= cast(ubyte[]) t; - } - if(!autoBuffer || isAll) { - if(rawDataOutput !is null) - if(nph && responseChunked) { - //rawDataOutput(makeChunk(cast(const(ubyte)[]) t)); - // we're making the chunk here instead of in a function - // to avoid unneeded gc pressure - buffer.add(toHex(t.length)); - buffer.add("\r\n"); - buffer.add(cast(char[]) t, "\r\n"); - } else { - buffer.add(cast(char[]) t); - } - else - buffer.add(cast(char[]) t); - } - } - - if(rawDataOutput !is null) - rawDataOutput(cast(const(ubyte)[]) buffer.get()); - else - stdout.rawWrite(buffer.get()); - - if(maybeAutoClose && isAll) - close(); // if you say it is all, that means we're definitely done - // maybeAutoClose can be false though to avoid this (important if you call from inside close()! - } - - /++ - Convenience method to set content type to json and write the string as the complete response. - - History: - Added January 16, 2020 - +/ - void writeJson(string json) { - this.setResponseContentType("application/json"); - this.write(json, true); - } - - /// Flushes the pending buffer, leaving the connection open so you can send more. - void flush() { - if(rawDataOutput is null) - stdout.flush(); - else if(flushDelegate !is null) - flushDelegate(); - } - - version(autoBuffer) - bool autoBuffer = true; - else - bool autoBuffer = false; - ubyte[] outputBuffer; - - /// Flushes the buffers to the network, signifying that you are done. - /// You should always call this explicitly when you are done outputting data. - void close() { - if(closed) - return; // don't double close - - if(!outputtedResponseData) - write("", true, false); - - // writing auto buffered data - if(requestMethod != RequestMethod.HEAD && autoBuffer) { - if(!nph) - stdout.rawWrite(outputBuffer); - else - write(outputBuffer, true, false); // tell it this is everything - } - - // closing the last chunk... - if(nph && rawDataOutput !is null && responseChunked) - rawDataOutput(cast(const(ubyte)[]) "0\r\n\r\n"); - - if(flushDelegate) - flushDelegate(); - - closed = true; - } - - // Closes without doing anything, shouldn't be used often - void rawClose() { - closed = true; - } - - /++ - Gets a request variable as a specific type, or the default value of it isn't there - or isn't convertible to the request type. - - Checks both GET and POST variables, preferring the POST variable, if available. - - A nice trick is using the default value to choose the type: - - --- - /* - The return value will match the type of the default. - Here, I gave 10 as a default, so the return value will - be an int. - - If the user-supplied value cannot be converted to the - requested type, you will get the default value back. - */ - int a = cgi.request("number", 10); - - if(cgi.get["number"] == "11") - assert(a == 11); // conversion succeeds - - if("number" !in cgi.get) - assert(a == 10); // no value means you can't convert - give the default - - if(cgi.get["number"] == "twelve") - assert(a == 10); // conversion from string to int would fail, so we get the default - --- - - You can use an enum as an easy whitelist, too: - - --- - enum Operations { - add, remove, query - } - - auto op = cgi.request("op", Operations.query); - - if(cgi.get["op"] == "add") - assert(op == Operations.add); - if(cgi.get["op"] == "remove") - assert(op == Operations.remove); - if(cgi.get["op"] == "query") - assert(op == Operations.query); - - if(cgi.get["op"] == "random string") - assert(op == Operations.query); // the value can't be converted to the enum, so we get the default - --- - +/ - T request(T = string)(in string name, in T def = T.init) const nothrow { - try { - return - (name in post) ? to!T(post[name]) : - (name in get) ? to!T(get[name]) : - def; - } catch(Exception e) { return def; } - } - - /// Is the output already closed? - bool isClosed() const { - return closed; - } - - /++ - Gets a session object associated with the `cgi` request. You can use different type throughout your application. - +/ - Session!Data getSessionObject(Data)() { - if(testInProcess !is null) { - // test mode - auto obj = testInProcess.getSessionOverride(typeid(typeof(return))); - if(obj !is null) - return cast(typeof(return)) obj; - else { - auto o = new MockSession!Data(); - testInProcess.setSessionOverride(typeid(typeof(return)), o); - return o; - } - } else { - // normal operation - return new BasicDataServerSession!Data(this); - } - } - - // if it is in test mode; triggers mock sessions. Used by CgiTester - version(with_breaking_cgi_features) - private CgiTester testInProcess; - - /* Hooks for redirecting input and output */ - private void delegate(const(ubyte)[]) rawDataOutput = null; - private void delegate() flushDelegate = null; - - /* This info is used when handling a more raw HTTP protocol */ - private bool nph; - private bool http10; - private bool closed; - private bool responseChunked = false; - - version(preserveData) // note: this can eat lots of memory; don't use unless you're sure you need it. - immutable(ubyte)[] originalPostData; - - /++ - This holds the posted body data if it has not been parsed into [post] and [postArray]. - - It is intended to be used for JSON and XML request content types, but also may be used - for other content types your application can handle. But it will NOT be populated - for content types application/x-www-form-urlencoded or multipart/form-data, since those are - parsed into the post and postArray members. - - Remember that anything beyond your `maxContentLength` param when setting up [GenericMain], etc., - will be discarded to the client with an error. This helps keep this array from being exploded in size - and consuming all your server's memory (though it may still be possible to eat excess ram from a concurrent - client in certain build modes.) - - History: - Added January 5, 2021 - Documented February 21, 2023 (dub v11.0) - +/ - public immutable string postBody; - alias postJson = postBody; // old name - - /* Internal state flags */ - private bool outputtedResponseData; - private bool noCache = true; - - const(string[string]) environmentVariables; - - /** What follows is data gotten from the HTTP request. It is all fully immutable, - partially because it logically is (your code doesn't change what the user requested...) - and partially because I hate how bad programs in PHP change those superglobals to do - all kinds of hard to follow ugliness. I don't want that to ever happen in D. - - For some of these, you'll want to refer to the http or cgi specs for more details. - */ - immutable(string[string]) requestHeaders; /// All the raw headers in the request as name/value pairs. The name is stored as all lower case, but otherwise the same as it is in HTTP; words separated by dashes. For example, "cookie" or "accept-encoding". Many HTTP headers have specialized variables below for more convenience and static name checking; you should generally try to use them. - - immutable(char[]) host; /// The hostname in the request. If one program serves multiple domains, you can use this to differentiate between them. - immutable(char[]) origin; /// The origin header in the request, if present. Some HTML5 cross-domain apis set this and you should check it on those cross domain requests and websockets. - immutable(char[]) userAgent; /// The browser's user-agent string. Can be used to identify the browser. - immutable(char[]) pathInfo; /// This is any stuff sent after your program's name on the url, but before the query string. For example, suppose your program is named "app". If the user goes to site.com/app, pathInfo is empty. But, he can also go to site.com/app/some/sub/path; treating your program like a virtual folder. In this case, pathInfo == "/some/sub/path". - immutable(char[]) scriptName; /// The full base path of your program, as seen by the user. If your program is located at site.com/programs/apps, scriptName == "/programs/apps". - immutable(char[]) scriptFileName; /// The physical filename of your script - immutable(char[]) authorization; /// The full authorization string from the header, undigested. Useful for implementing auth schemes such as OAuth 1.0. Note that some web servers do not forward this to the app without taking extra steps. See requireBasicAuth's comment for more info. - immutable(char[]) accept; /// The HTTP accept header is the user agent telling what content types it is willing to accept. This is often */*; they accept everything, so it's not terribly useful. (The similar sounding Accept-Encoding header is handled automatically for chunking and gzipping. Simply set gzipResponse = true and cgi.d handles the details, zipping if the user's browser is willing to accept it.) - immutable(char[]) lastEventId; /// The HTML 5 draft includes an EventSource() object that connects to the server, and remains open to take a stream of events. My arsd.rtud module can help with the server side part of that. The Last-Event-Id http header is defined in the draft to help handle loss of connection. When the browser reconnects to you, it sets this header to the last event id it saw, so you can catch it up. This member has the contents of that header. - - immutable(RequestMethod) requestMethod; /// The HTTP request verb: GET, POST, etc. It is represented as an enum in cgi.d (which, like many enums, you can convert back to string with std.conv.to()). A HTTP GET is supposed to, according to the spec, not have side effects; a user can GET something over and over again and always have the same result. On all requests, the get[] and getArray[] members may be filled in. The post[] and postArray[] members are only filled in on POST methods. - immutable(char[]) queryString; /// The unparsed content of the request query string - the stuff after the ? in your URL. See get[] and getArray[] for a parse view of it. Sometimes, the unparsed string is useful though if you want a custom format of data up there (probably not a good idea, unless it is really simple, like "?username" perhaps.) - immutable(char[]) cookie; /// The unparsed content of the Cookie: header in the request. See also the cookies[string] member for a parsed view of the data. - /** The Referer header from the request. (It is misspelled in the HTTP spec, and thus the actual request and cgi specs too, but I spelled the word correctly here because that's sane. The spec's misspelling is an implementation detail.) It contains the site url that referred the user to your program; the site that linked to you, or if you're serving images, the site that has you as an image. Also, if you're in an iframe, the referrer is the site that is framing you. - - Important note: if the user copy/pastes your url, this is blank, and, just like with all other user data, their browsers can also lie to you. Don't rely on it for real security. - */ - immutable(char[]) referrer; - immutable(char[]) requestUri; /// The full url if the current request, excluding the protocol and host. requestUri == scriptName ~ pathInfo ~ (queryString.length ? "?" ~ queryString : ""); - - immutable(char[]) remoteAddress; /// The IP address of the user, as we see it. (Might not match the IP of the user's computer due to things like proxies and NAT.) - - immutable bool https; /// Was the request encrypted via https? - immutable int port; /// On what TCP port number did the server receive the request? - - /** Here come the parsed request variables - the things that come close to PHP's _GET, _POST, etc. superglobals in content. */ - - immutable(string[string]) get; /// The data from your query string in the url, only showing the last string of each name. If you want to handle multiple values with the same name, use getArray. This only works right if the query string is x-www-form-urlencoded; the default you see on the web with name=value pairs separated by the & character. - immutable(string[string]) post; /// The data from the request's body, on POST requests. It parses application/x-www-form-urlencoded data (used by most web requests, including typical forms), and multipart/form-data requests (used by file uploads on web forms) into the same container, so you can always access them the same way. It makes no attempt to parse other content types. If you want to accept an XML Post body (for a web api perhaps), you'll need to handle the raw data yourself. - immutable(string[string]) cookies; /// Separates out the cookie header into individual name/value pairs (which is how you set them!) - - /** - Represents user uploaded files. - - When making a file upload form, be sure to follow the standard: set method="POST" and enctype="multipart/form-data" in your html
tag attributes. The key into this array is the name attribute on your input tag, just like with other post variables. See the comments on the UploadedFile struct for more information about the data inside, including important notes on max size and content location. - */ - immutable(UploadedFile[][string]) filesArray; - immutable(UploadedFile[string]) files; - - /// Use these if you expect multiple items submitted with the same name. btw, assert(get[name] is getArray[name][$-1); should pass. Same for post and cookies. - /// the order of the arrays is the order the data arrives - immutable(string[][string]) getArray; /// like get, but an array of values per name - immutable(string[][string]) postArray; /// ditto for post - immutable(string[][string]) cookiesArray; /// ditto for cookies - - // convenience function for appending to a uri without extra ? - // matches the name and effect of javascript's location.search property - string search() const { - if(queryString.length) - return "?" ~ queryString; - return ""; - } - - // FIXME: what about multiple files with the same name? - private: - //RequestMethod _requestMethod; -} - -/// use this for testing or other isolated things when you want it to be no-ops -Cgi dummyCgi(Cgi.RequestMethod method = Cgi.RequestMethod.GET, string url = null, in ubyte[] data = null, void delegate(const(ubyte)[]) outputSink = null) { - // we want to ignore, not use stdout - if(outputSink is null) - outputSink = delegate void(const(ubyte)[]) { }; - - string[string] env; - env["REQUEST_METHOD"] = to!string(method); - env["CONTENT_LENGTH"] = to!string(data.length); - - auto cgi = new Cgi( - 0, - env, - { return data; }, - outputSink, - null); - - return cgi; -} - -/++ - A helper test class for request handler unittests. -+/ -version(with_breaking_cgi_features) -class CgiTester { - private { - SessionObject[TypeInfo] mockSessions; - SessionObject getSessionOverride(TypeInfo ti) { - if(auto o = ti in mockSessions) - return *o; - else - return null; - } - void setSessionOverride(TypeInfo ti, SessionObject so) { - mockSessions[ti] = so; - } - } - - /++ - Gets (and creates if necessary) a mock session object for this test. Note - it will be the same one used for any test operations through this CgiTester instance. - +/ - Session!Data getSessionObject(Data)() { - auto obj = getSessionOverride(typeid(typeof(return))); - if(obj !is null) - return cast(typeof(return)) obj; - else { - auto o = new MockSession!Data(); - setSessionOverride(typeid(typeof(return)), o); - return o; - } - } - - /++ - Pass a reference to your request handler when creating the tester. - +/ - this(void function(Cgi) requestHandler) { - this.requestHandler = requestHandler; - } - - /++ - You can check response information with these methods after you call the request handler. - +/ - struct Response { - int code; - string[string] headers; - string responseText; - ubyte[] responseBody; - } - - /++ - Executes a test request on your request handler, and returns the response. - - Params: - url = The URL to test. Should be an absolute path, but excluding domain. e.g. `"/test"`. - args = additional arguments. Same format as cgi's command line handler. - +/ - Response GET(string url, string[] args = null) { - return executeTest("GET", url, args); - } - /// ditto - Response POST(string url, string[] args = null) { - return executeTest("POST", url, args); - } - - /// ditto - Response executeTest(string method, string url, string[] args) { - ubyte[] outputtedRawData; - void outputSink(const(ubyte)[] data) { - outputtedRawData ~= data; - } - auto cgi = new Cgi(["test", method, url] ~ args, &outputSink); - cgi.testInProcess = this; - scope(exit) cgi.dispose(); - - requestHandler(cgi); - - cgi.close(); - - Response response; - - if(outputtedRawData.length) { - enum LINE = "\r\n"; - - auto idx = outputtedRawData.locationOf(LINE ~ LINE); - assert(idx != -1, to!string(outputtedRawData)); - auto headers = cast(string) outputtedRawData[0 .. idx]; - response.code = 200; - while(headers.length) { - auto i = headers.locationOf(LINE); - if(i == -1) i = cast(int) headers.length; - - auto header = headers[0 .. i]; - - auto c = header.locationOf(":"); - if(c != -1) { - auto name = header[0 .. c]; - auto value = header[c + 2 ..$]; - - if(name == "Status") - response.code = value[0 .. value.locationOf(" ")].to!int; - - response.headers[name] = value; - } else { - assert(0); - } - - if(i != headers.length) - i += 2; - headers = headers[i .. $]; - } - response.responseBody = outputtedRawData[idx + 4 .. $]; - response.responseText = cast(string) response.responseBody; - } - - return response; - } - - private void function(Cgi) requestHandler; -} - - -// should this be a separate module? Probably, but that's a hassle. - -/// Makes a data:// uri that can be used as links in most newer browsers (IE8+). -string makeDataUrl(string mimeType, in void[] data) { - auto data64 = Base64.encode(cast(const(ubyte[])) data); - return "data:" ~ mimeType ~ ";base64," ~ assumeUnique(data64); -} - -// FIXME: I don't think this class correctly decodes/encodes the individual parts -/// Represents a url that can be broken down or built up through properties -struct Uri { - alias toString this; // blargh idk a url really is a string, but should it be implicit? - - // scheme//userinfo@host:port/path?query#fragment - - string scheme; /// e.g. "http" in "http://example.com/" - string userinfo; /// the username (and possibly a password) in the uri - string host; /// the domain name - int port; /// port number, if given. Will be zero if a port was not explicitly given - string path; /// e.g. "/folder/file.html" in "http://example.com/folder/file.html" - string query; /// the stuff after the ? in a uri - string fragment; /// the stuff after the # in a uri. - - // idk if i want to keep these, since the functions they wrap are used many, many, many times in existing code, so this is either an unnecessary alias or a gratuitous break of compatibility - // the decode ones need to keep different names anyway because we can't overload on return values... - static string encode(string s) { return std.uri.encodeComponent(s); } - static string encode(string[string] s) { return encodeVariables(s); } - static string encode(string[][string] s) { return encodeVariables(s); } - - /// Breaks down a uri string to its components - this(string uri) { - reparse(uri); - } - - private void reparse(string uri) { - // from RFC 3986 - // the ctRegex triples the compile time and makes ugly errors for no real benefit - // it was a nice experiment but just not worth it. - // enum ctr = ctRegex!r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?"; - /* - Captures: - 0 = whole url - 1 = scheme, with : - 2 = scheme, no : - 3 = authority, with // - 4 = authority, no // - 5 = path - 6 = query string, with ? - 7 = query string, no ? - 8 = anchor, with # - 9 = anchor, no # - */ - // Yikes, even regular, non-CT regex is also unacceptably slow to compile. 1.9s on my computer! - // instead, I will DIY and cut that down to 0.6s on the same computer. - /* - - Note that authority is - user:password@domain:port - where the user:password@ part is optional, and the :port is optional. - - Regex translation: - - Scheme cannot have :, /, ?, or # in it, and must have one or more chars and end in a :. It is optional, but must be first. - Authority must start with //, but cannot have any other /, ?, or # in it. It is optional. - Path cannot have any ? or # in it. It is optional. - Query must start with ? and must not have # in it. It is optional. - Anchor must start with # and can have anything else in it to end of string. It is optional. - */ - - this = Uri.init; // reset all state - - // empty uri = nothing special - if(uri.length == 0) { - return; - } - - size_t idx; - - scheme_loop: foreach(char c; uri[idx .. $]) { - switch(c) { - case ':': - case '/': - case '?': - case '#': - break scheme_loop; - default: - } - idx++; - } - - if(idx == 0 && uri[idx] == ':') { - // this is actually a path! we skip way ahead - goto path_loop; - } - - if(idx == uri.length) { - // the whole thing is a path, apparently - path = uri; - return; - } - - if(idx > 0 && uri[idx] == ':') { - scheme = uri[0 .. idx]; - idx++; - } else { - // we need to rewind; it found a / but no :, so the whole thing is prolly a path... - idx = 0; - } - - if(idx + 2 < uri.length && uri[idx .. idx + 2] == "//") { - // we have an authority.... - idx += 2; - - auto authority_start = idx; - authority_loop: foreach(char c; uri[idx .. $]) { - switch(c) { - case '/': - case '?': - case '#': - break authority_loop; - default: - } - idx++; - } - - auto authority = uri[authority_start .. idx]; - - auto idx2 = authority.indexOf("@"); - if(idx2 != -1) { - userinfo = authority[0 .. idx2]; - authority = authority[idx2 + 1 .. $]; - } - - if(authority.length && authority[0] == '[') { - // ipv6 address special casing - idx2 = authority.indexOf(']'); - if(idx2 != -1) { - auto end = authority[idx2 + 1 .. $]; - if(end.length && end[0] == ':') - idx2 = idx2 + 1; - else - idx2 = -1; - } - } else { - idx2 = authority.indexOf(":"); - } - - if(idx2 == -1) { - port = 0; // 0 means not specified; we should use the default for the scheme - host = authority; - } else { - host = authority[0 .. idx2]; - if(idx2 + 1 < authority.length) - port = to!int(authority[idx2 + 1 .. $]); - else - port = 0; - } - } - - path_loop: - auto path_start = idx; - - foreach(char c; uri[idx .. $]) { - if(c == '?' || c == '#') - break; - idx++; - } - - path = uri[path_start .. idx]; - - if(idx == uri.length) - return; // nothing more to examine... - - if(uri[idx] == '?') { - idx++; - auto query_start = idx; - foreach(char c; uri[idx .. $]) { - if(c == '#') - break; - idx++; - } - query = uri[query_start .. idx]; - } - - if(idx < uri.length && uri[idx] == '#') { - idx++; - fragment = uri[idx .. $]; - } - - // uriInvalidated = false; - } - - private string rebuildUri() const { - string ret; - if(scheme.length) - ret ~= scheme ~ ":"; - if(userinfo.length || host.length) - ret ~= "//"; - if(userinfo.length) - ret ~= userinfo ~ "@"; - if(host.length) - ret ~= host; - if(port) - ret ~= ":" ~ to!string(port); - - ret ~= path; - - if(query.length) - ret ~= "?" ~ query; - - if(fragment.length) - ret ~= "#" ~ fragment; - - // uri = ret; - // uriInvalidated = false; - return ret; - } - - /// Converts the broken down parts back into a complete string - string toString() const { - // if(uriInvalidated) - return rebuildUri(); - } - - /// Returns a new absolute Uri given a base. It treats this one as - /// relative where possible, but absolute if not. (If protocol, domain, or - /// other info is not set, the new one inherits it from the base.) - /// - /// Browsers use a function like this to figure out links in html. - Uri basedOn(in Uri baseUrl) const { - Uri n = this; // copies - if(n.scheme == "data") - return n; - // n.uriInvalidated = true; // make sure we regenerate... - - // userinfo is not inherited... is this wrong? - - // if anything is given in the existing url, we don't use the base anymore. - if(n.scheme.empty) { - n.scheme = baseUrl.scheme; - if(n.host.empty) { - n.host = baseUrl.host; - if(n.port == 0) { - n.port = baseUrl.port; - if(n.path.length > 0 && n.path[0] != '/') { - auto b = baseUrl.path[0 .. baseUrl.path.lastIndexOf("/") + 1]; - if(b.length == 0) - b = "/"; - n.path = b ~ n.path; - } else if(n.path.length == 0) { - n.path = baseUrl.path; - } - } - } - } - - n.removeDots(); - - return n; - } - - void removeDots() { - auto parts = this.path.split("/"); - string[] toKeep; - foreach(part; parts) { - if(part == ".") { - continue; - } else if(part == "..") { - //if(toKeep.length > 1) - toKeep = toKeep[0 .. $-1]; - //else - //toKeep = [""]; - continue; - } else { - //if(toKeep.length && toKeep[$-1].length == 0 && part.length == 0) - //continue; // skip a `//` situation - toKeep ~= part; - } - } - - auto path = toKeep.join("/"); - if(path.length && path[0] != '/') - path = "/" ~ path; - - this.path = path; - } - - unittest { - auto uri = Uri("test.html"); - assert(uri.path == "test.html"); - uri = Uri("path/1/lol"); - assert(uri.path == "path/1/lol"); - uri = Uri("http://me@example.com"); - assert(uri.scheme == "http"); - assert(uri.userinfo == "me"); - assert(uri.host == "example.com"); - uri = Uri("http://example.com/#a"); - assert(uri.scheme == "http"); - assert(uri.host == "example.com"); - assert(uri.fragment == "a"); - uri = Uri("#foo"); - assert(uri.fragment == "foo"); - uri = Uri("?lol"); - assert(uri.query == "lol"); - uri = Uri("#foo?lol"); - assert(uri.fragment == "foo?lol"); - uri = Uri("?lol#foo"); - assert(uri.fragment == "foo"); - assert(uri.query == "lol"); - - uri = Uri("http://127.0.0.1/"); - assert(uri.host == "127.0.0.1"); - assert(uri.port == 0); - - uri = Uri("http://127.0.0.1:123/"); - assert(uri.host == "127.0.0.1"); - assert(uri.port == 123); - - uri = Uri("http://[ff:ff::0]/"); - assert(uri.host == "[ff:ff::0]"); - - uri = Uri("http://[ff:ff::0]:123/"); - assert(uri.host == "[ff:ff::0]"); - assert(uri.port == 123); - } - - // This can sometimes be a big pain in the butt for me, so lots of copy/paste here to cover - // the possibilities. - unittest { - auto url = Uri("cool.html"); // checking relative links - - assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/cool.html"); - assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/cool.html"); - assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/cool.html"); - assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/cool.html"); - assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html"); - assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/cool.html"); - assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/cool.html"); - assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/cool.html"); - assert(url.basedOn(Uri("http://test.com")) == "http://test.com/cool.html"); - - url = Uri("/something/cool.html"); // same server, different path - assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/something/cool.html"); - assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/something/cool.html"); - assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/something/cool.html"); - assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/something/cool.html"); - assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html"); - assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/something/cool.html"); - assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/something/cool.html"); - assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/something/cool.html"); - assert(url.basedOn(Uri("http://test.com")) == "http://test.com/something/cool.html"); - - url = Uri("?query=answer"); // same path. server, protocol, and port, just different query string and fragment - assert(url.basedOn(Uri("http://test.com/what/test.html")) == "http://test.com/what/test.html?query=answer"); - assert(url.basedOn(Uri("https://test.com/what/test.html")) == "https://test.com/what/test.html?query=answer"); - assert(url.basedOn(Uri("http://test.com/what/")) == "http://test.com/what/?query=answer"); - assert(url.basedOn(Uri("http://test.com/")) == "http://test.com/?query=answer"); - assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer"); - assert(url.basedOn(Uri("http://test.com/what/test.html?a=b")) == "http://test.com/what/test.html?query=answer"); - assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d")) == "http://test.com/what/test.html?query=answer"); - assert(url.basedOn(Uri("http://test.com/what/test.html?a=b&c=d#what")) == "http://test.com/what/test.html?query=answer"); - assert(url.basedOn(Uri("http://test.com")) == "http://test.com?query=answer"); - - url = Uri("/test/bar"); - assert(Uri("./").basedOn(url) == "/test/", Uri("./").basedOn(url)); - assert(Uri("../").basedOn(url) == "/"); - - url = Uri("http://example.com/"); - assert(Uri("../foo").basedOn(url) == "http://example.com/foo"); - - //auto uriBefore = url; - url = Uri("#anchor"); // everything should remain the same except the anchor - //uriBefore.anchor = "anchor"); - //assert(url == uriBefore); - - url = Uri("//example.com"); // same protocol, but different server. the path here should be blank. - - url = Uri("//example.com/example.html"); // same protocol, but different server and path - - url = Uri("http://example.com/test.html"); // completely absolute link should never be modified - - url = Uri("http://example.com"); // completely absolute link should never be modified, even if it has no path - - // FIXME: add something for port too - } - - // these are like javascript's location.search and location.hash - string search() const { - return query.length ? ("?" ~ query) : ""; - } - string hash() const { - return fragment.length ? ("#" ~ fragment) : ""; - } -} - - -/* - for session, see web.d -*/ - -/// breaks down a url encoded string -string[][string] decodeVariables(string data, string separator = "&", string[]* namesInOrder = null, string[]* valuesInOrder = null) { - auto vars = data.split(separator); - string[][string] _get; - foreach(var; vars) { - auto equal = var.indexOf("="); - string name; - string value; - if(equal == -1) { - name = decodeComponent(var); - value = ""; - } else { - //_get[decodeComponent(var[0..equal])] ~= decodeComponent(var[equal + 1 .. $].replace("+", " ")); - // stupid + -> space conversion. - name = decodeComponent(var[0..equal].replace("+", " ")); - value = decodeComponent(var[equal + 1 .. $].replace("+", " ")); - } - - _get[name] ~= value; - if(namesInOrder) - (*namesInOrder) ~= name; - if(valuesInOrder) - (*valuesInOrder) ~= value; - } - return _get; -} - -/// breaks down a url encoded string, but only returns the last value of any array -string[string] decodeVariablesSingle(string data) { - string[string] va; - auto varArray = decodeVariables(data); - foreach(k, v; varArray) - va[k] = v[$-1]; - - return va; -} - -/// url encodes the whole string -string encodeVariables(in string[string] data) { - string ret; - - bool outputted = false; - foreach(k, v; data) { - if(outputted) - ret ~= "&"; - else - outputted = true; - - ret ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v); - } - - return ret; -} - -/// url encodes a whole string -string encodeVariables(in string[][string] data) { - string ret; - - bool outputted = false; - foreach(k, arr; data) { - foreach(v; arr) { - if(outputted) - ret ~= "&"; - else - outputted = true; - ret ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v); - } - } - - return ret; -} - -/// Encodes all but the explicitly unreserved characters per rfc 3986 -/// Alphanumeric and -_.~ are the only ones left unencoded -/// name is borrowed from php -string rawurlencode(in char[] data) { - string ret; - ret.reserve(data.length * 2); - foreach(char c; data) { - if( - (c >= 'a' && c <= 'z') || - (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9') || - c == '-' || c == '_' || c == '.' || c == '~') - { - ret ~= c; - } else { - ret ~= '%'; - // since we iterate on char, this should give us the octets of the full utf8 string - ret ~= toHexUpper(c); - } - } - - return ret; -} - - -// http helper functions - -// for chunked responses (which embedded http does whenever possible) -version(none) // this is moved up above to avoid making a copy of the data -const(ubyte)[] makeChunk(const(ubyte)[] data) { - const(ubyte)[] ret; - - ret = cast(const(ubyte)[]) toHex(data.length); - ret ~= cast(const(ubyte)[]) "\r\n"; - ret ~= data; - ret ~= cast(const(ubyte)[]) "\r\n"; - - return ret; -} - -string toHex(long num) { - string ret; - while(num) { - int v = num % 16; - num /= 16; - char d = cast(char) ((v < 10) ? v + '0' : (v-10) + 'a'); - ret ~= d; - } - - return to!string(array(ret.retro)); -} - -string toHexUpper(long num) { - string ret; - while(num) { - int v = num % 16; - num /= 16; - char d = cast(char) ((v < 10) ? v + '0' : (v-10) + 'A'); - ret ~= d; - } - - if(ret.length == 1) - ret ~= "0"; // url encoding requires two digits and that's what this function is used for... - - return to!string(array(ret.retro)); -} - - -// the generic mixins - -/++ - Use this instead of writing your own main - - It ultimately calls [cgiMainImpl] which creates a [RequestServer] for you. -+/ -mixin template GenericMain(alias fun, long maxContentLength = defaultMaxContentLength) { - mixin CustomCgiMain!(Cgi, fun, maxContentLength); -} - -/++ - Boilerplate mixin for a main function that uses the [dispatcher] function. - - You can send `typeof(null)` as the `Presenter` argument to use a generic one. - - History: - Added July 9, 2021 -+/ -mixin template DispatcherMain(Presenter, DispatcherArgs...) { - /++ - Handler to the generated presenter you can use from your objects, etc. - +/ - Presenter activePresenter; - - /++ - Request handler that creates the presenter then forwards to the [dispatcher] function. - Renders 404 if the dispatcher did not handle the request. - - Will automatically serve the presenter.style and presenter.script as "style.css" and "script.js" - +/ - void handler(Cgi cgi) { - auto presenter = new Presenter; - activePresenter = presenter; - scope(exit) activePresenter = null; - - if(cgi.dispatcher!DispatcherArgs(presenter)) - return; - - switch(cgi.pathInfo) { - case "/style.css": - cgi.setCache(true); - cgi.setResponseContentType("text/css"); - cgi.write(presenter.style(), true); - break; - case "/script.js": - cgi.setCache(true); - cgi.setResponseContentType("application/javascript"); - cgi.write(presenter.script(), true); - break; - default: - presenter.renderBasicError(cgi, 404); - } - } - mixin GenericMain!handler; -} - -mixin template DispatcherMain(DispatcherArgs...) if(!is(DispatcherArgs[0] : WebPresenter!T, T)) { - class GenericPresenter : WebPresenter!GenericPresenter {} - mixin DispatcherMain!(GenericPresenter, DispatcherArgs); -} - -private string simpleHtmlEncode(string s) { - return s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\n", "
\n"); -} - -string messageFromException(Throwable t) { - string message; - if(t !is null) { - debug message = t.toString(); - else message = "An unexpected error has occurred."; - } else { - message = "Unknown error"; - } - return message; -} - -string plainHttpError(bool isCgi, string type, Throwable t) { - auto message = messageFromException(t); - message = simpleHtmlEncode(message); - - return format("%s %s\r\nContent-Length: %s\r\n\r\n%s", - isCgi ? "Status:" : "HTTP/1.0", - type, message.length, message); -} - -// returns true if we were able to recover reasonably -bool handleException(Cgi cgi, Throwable t) { - if(cgi.isClosed) { - // if the channel has been explicitly closed, we can't handle it here - return true; - } - - if(cgi.outputtedResponseData) { - // the headers are sent, but the channel is open... since it closes if all was sent, we can append an error message here. - return false; // but I don't want to, since I don't know what condition the output is in; I don't want to inject something (nor check the content-type for that matter. So we say it was not a clean handling. - } else { - // no headers are sent, we can send a full blown error and recover - cgi.setCache(false); - cgi.setResponseContentType("text/html"); - cgi.setResponseLocation(null); // cancel the redirect - cgi.setResponseStatus("500 Internal Server Error"); - cgi.write(simpleHtmlEncode(messageFromException(t))); - cgi.close(); - return true; - } -} - -bool isCgiRequestMethod(string s) { - s = s.toUpper(); - if(s == "COMMANDLINE") - return true; - foreach(member; __traits(allMembers, Cgi.RequestMethod)) - if(s == member) - return true; - return false; -} - -/// If you want to use a subclass of Cgi with generic main, use this mixin. -mixin template CustomCgiMain(CustomCgi, alias fun, long maxContentLength = defaultMaxContentLength) if(is(CustomCgi : Cgi)) { - // kinda hacky - the T... is passed to Cgi's constructor in standard cgi mode, and ignored elsewhere - void main(string[] args) { - cgiMainImpl!(fun, CustomCgi, maxContentLength)(args); - } -} - -version(embedded_httpd_processes) - __gshared int processPoolSize = 8; - -// Returns true if run. You should exit the program after that. -bool tryAddonServers(string[] args) { - if(args.length > 1) { - // run the special separate processes if needed - switch(args[1]) { - case "--websocket-server": - version(with_addon_servers) - websocketServers[args[2]](args[3 .. $]); - else - printf("Add-on servers not compiled in.\n"); - return true; - case "--websocket-servers": - import core.demangle; - version(with_addon_servers_connections) - foreach(k, v; websocketServers) - writeln(k, "\t", demangle(k)); - return true; - case "--session-server": - version(with_addon_servers) - runSessionServer(); - else - printf("Add-on servers not compiled in.\n"); - return true; - case "--event-server": - version(with_addon_servers) - runEventServer(); - else - printf("Add-on servers not compiled in.\n"); - return true; - case "--timer-server": - version(with_addon_servers) - runTimerServer(); - else - printf("Add-on servers not compiled in.\n"); - return true; - case "--timed-jobs": - import core.demangle; - version(with_addon_servers_connections) - foreach(k, v; scheduledJobHandlers) - writeln(k, "\t", demangle(k)); - return true; - case "--timed-job": - scheduledJobHandlers[args[2]](args[3 .. $]); - return true; - default: - // intentionally blank - do nothing and carry on to run normally - } - } - return false; -} - -/// Tries to simulate a request from the command line. Returns true if it does, false if it didn't find the args. -bool trySimulatedRequest(alias fun, CustomCgi = Cgi)(string[] args) if(is(CustomCgi : Cgi)) { - // we support command line thing for easy testing everywhere - // it needs to be called ./app method uri [other args...] - if(args.length >= 3 && isCgiRequestMethod(args[1])) { - Cgi cgi = new CustomCgi(args); - scope(exit) cgi.dispose(); - fun(cgi); - cgi.close(); - return true; - } - return false; -} - -/++ - A server control and configuration struct, as a potential alternative to calling [GenericMain] or [cgiMainImpl]. See the source of [cgiMainImpl] to an example of how you can use it. - - History: - Added Sept 26, 2020 (release version 8.5). -+/ -struct RequestServer { - /// - string listeningHost = defaultListeningHost(); - /// - ushort listeningPort = defaultListeningPort(); - - /++ - Uses a fork() call, if available, to provide additional crash resiliency and possibly improved performance. On the - other hand, if you fork, you must not assume any memory is shared between requests (you shouldn't be anyway though! But - if you have to, you probably want to set this to false and use an explicit threaded server with [serveEmbeddedHttp]) and - [stop] may not work as well. - - History: - Added August 12, 2022 (dub v10.9). Previously, this was only configurable through the `-version=cgi_no_fork` - argument to dmd. That version still defines the value of `cgi_use_fork_default`, used to initialize this, for - compatibility. - +/ - bool useFork = cgi_use_fork_default; - - /++ - Determines the number of worker threads to spawn per process, for server modes that use worker threads. 0 will use a - default based on the number of cpus modified by the server mode. - - History: - Added August 12, 2022 (dub v10.9) - +/ - int numberOfThreads = 0; - - /// - this(string defaultHost, ushort defaultPort) { - this.listeningHost = defaultHost; - this.listeningPort = defaultPort; - } - - /// - this(ushort defaultPort) { - listeningPort = defaultPort; - } - - /++ - Reads the command line arguments into the values here. - - Possible arguments are `--listening-host`, `--listening-port` (or `--port`), `--uid`, and `--gid`. - +/ - void configureFromCommandLine(string[] args) { - bool foundPort = false; - bool foundHost = false; - bool foundUid = false; - bool foundGid = false; - foreach(arg; args) { - if(foundPort) { - listeningPort = to!ushort(arg); - foundPort = false; - } - if(foundHost) { - listeningHost = arg; - foundHost = false; - } - if(foundUid) { - privilegesDropToUid = to!uid_t(arg); - foundUid = false; - } - if(foundGid) { - privilegesDropToGid = to!gid_t(arg); - foundGid = false; - } - if(arg == "--listening-host" || arg == "-h" || arg == "/listening-host") - foundHost = true; - else if(arg == "--port" || arg == "-p" || arg == "/port" || arg == "--listening-port") - foundPort = true; - else if(arg == "--uid") - foundUid = true; - else if(arg == "--gid") - foundGid = true; - } - } - - version(Windows) { - private alias uid_t = int; - private alias gid_t = int; - } - - /// user (uid) to drop privileges to - /// 0 … do nothing - uid_t privilegesDropToUid = 0; - /// group (gid) to drop privileges to - /// 0 … do nothing - gid_t privilegesDropToGid = 0; - - private void dropPrivileges() { - version(Posix) { - import core.sys.posix.unistd; - - if (privilegesDropToGid != 0 && setgid(privilegesDropToGid) != 0) - throw new Exception("Dropping privileges via setgid() failed."); - - if (privilegesDropToUid != 0 && setuid(privilegesDropToUid) != 0) - throw new Exception("Dropping privileges via setuid() failed."); - } - else { - // FIXME: Windows? - //pragma(msg, "Dropping privileges is not implemented for this platform"); - } - - // done, set zero - privilegesDropToGid = 0; - privilegesDropToUid = 0; - } - - /++ - Serves a single HTTP request on this thread, with an embedded server, then stops. Designed for cases like embedded oauth responders - - History: - Added Oct 10, 2020. - Example: - - --- - import arsd.cgi; - void main() { - RequestServer server = RequestServer("127.0.0.1", 6789); - string oauthCode; - string oauthScope; - server.serveHttpOnce!((cgi) { - oauthCode = cgi.request("code"); - oauthScope = cgi.request("scope"); - cgi.write("Thank you, please return to the application."); - }); - // use the code and scope given - } - --- - +/ - void serveHttpOnce(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { - import std.socket; - - bool tcp; - void delegate() cleanup; - auto socket = startListening(listeningHost, listeningPort, tcp, cleanup, 1, &dropPrivileges); - auto connection = socket.accept(); - doThreadHttpConnectionGuts!(CustomCgi, fun, true)(connection); - - if(cleanup) - cleanup(); - } - - /++ - Starts serving requests according to the current configuration. - +/ - void serve(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { - version(netman_httpd) { - // Obsolete! - - import arsd.httpd; - // what about forwarding the other constructor args? - // this probably needs a whole redoing... - serveHttp!CustomCgi(&fun, listeningPort);//5005); - return; - } else - version(embedded_httpd_processes) { - serveEmbeddedHttpdProcesses!(fun, CustomCgi)(this); - } else - version(embedded_httpd_threads) { - serveEmbeddedHttp!(fun, CustomCgi, maxContentLength)(); - } else - version(scgi) { - serveScgi!(fun, CustomCgi, maxContentLength)(); - } else - version(fastcgi) { - serveFastCgi!(fun, CustomCgi, maxContentLength)(this); - } else - version(stdio_http) { - serveSingleHttpConnectionOnStdio!(fun, CustomCgi, maxContentLength)(); - } else { - //version=plain_cgi; - handleCgiRequest!(fun, CustomCgi, maxContentLength)(); - } - } - - /++ - Runs the embedded HTTP thread server specifically, regardless of which build configuration you have. - - If you want the forking worker process server, you do need to compile with the embedded_httpd_processes config though. - +/ - void serveEmbeddedHttp(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(ThisFor!fun _this) { - globalStopFlag = false; - static if(__traits(isStaticFunction, fun)) - alias funToUse = fun; - else - void funToUse(CustomCgi cgi) { - static if(__VERSION__ > 2097) - __traits(child, _this, fun)(cgi); - else static assert(0, "Not implemented in your compiler version!"); - } - auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadHttpConnection!(CustomCgi, funToUse), null, useFork, numberOfThreads); - manager.listen(); - } - - /++ - Runs the embedded SCGI server specifically, regardless of which build configuration you have. - +/ - void serveScgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { - globalStopFlag = false; - auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadScgiConnection!(CustomCgi, fun, maxContentLength), null, useFork, numberOfThreads); - manager.listen(); - } - - /++ - Serves a single "connection", but the connection is spoken on stdin and stdout instead of on a socket. - - Intended for cases like working from systemd, like discussed here: [https://forum.dlang.org/post/avmkfdiitirnrenzljwc@forum.dlang.org] - - History: - Added May 29, 2021 - +/ - void serveSingleHttpConnectionOnStdio(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { - doThreadHttpConnectionGuts!(CustomCgi, fun, true)(new FakeSocketForStdin()); - } - - /++ - The [stop] function sets a flag that request handlers can (and should) check periodically. If a handler doesn't - respond to this flag, the library will force the issue. This determines when and how the issue will be forced. - +/ - enum ForceStop { - /++ - Stops accepting new requests, but lets ones already in the queue start and complete before exiting. - +/ - afterQueuedRequestsComplete, - /++ - Finishes requests already started their handlers, but drops any others in the queue. Streaming handlers - should cooperate and exit gracefully, but if they don't, it will continue waiting for them. - +/ - afterCurrentRequestsComplete, - /++ - Partial response writes will throw an exception, cancelling any streaming response, but complete - writes will continue to process. Request handlers that respect the stop token will also gracefully cancel. - +/ - cancelStreamingRequestsEarly, - /++ - All writes will throw. - +/ - cancelAllRequestsEarly, - /++ - Use OS facilities to forcibly kill running threads. The server process will be in an undefined state after this call (if this call ever returns). - +/ - forciblyTerminate, - } - - version(embedded_httpd_processes) {} else - /++ - Stops serving after the current requests are completed. - - Bugs: - Not implemented on version=embedded_httpd_processes, version=fastcgi on any system, or embedded_httpd on Windows (it does work on embedded_httpd_hybrid - on Windows however). Only partially implemented on non-Linux posix systems. - - You might also try SIGINT perhaps. - - The stopPriority is not yet fully implemented. - +/ - static void stop(ForceStop stopPriority = ForceStop.afterCurrentRequestsComplete) { - globalStopFlag = true; - - version(Posix) { - if(cancelfd > 0) { - ulong a = 1; - core.sys.posix.unistd.write(cancelfd, &a, a.sizeof); - } - } - version(Windows) { - if(iocp) { - foreach(i; 0 .. 16) // FIXME - PostQueuedCompletionStatus(iocp, 0, cast(ULONG_PTR) null, null); - } - } - } -} - -private alias AliasSeq(T...) = T; - -version(with_breaking_cgi_features) -mixin(q{ - template ThisFor(alias t) { - static if(__traits(isStaticFunction, t)) { - alias ThisFor = AliasSeq!(); - } else { - alias ThisFor = __traits(parent, t); - } - } -}); -else - alias ThisFor(alias t) = AliasSeq!(); - -private __gshared bool globalStopFlag = false; - -version(embedded_httpd_processes) -void serveEmbeddedHttpdProcesses(alias fun, CustomCgi = Cgi)(RequestServer params) { - import core.sys.posix.unistd; - import core.sys.posix.sys.socket; - import core.sys.posix.netinet.in_; - //import std.c.linux.socket; - - int sock = socket(AF_INET, SOCK_STREAM, 0); - if(sock == -1) - throw new Exception("socket"); - - cloexec(sock); - - { - - sockaddr_in addr; - addr.sin_family = AF_INET; - addr.sin_port = htons(params.listeningPort); - auto lh = params.listeningHost; - if(lh.length) { - if(inet_pton(AF_INET, lh.toStringz(), &addr.sin_addr.s_addr) != 1) - throw new Exception("bad listening host given, please use an IP address.\nExample: --listening-host 127.0.0.1 means listen only on Localhost.\nExample: --listening-host 0.0.0.0 means listen on all interfaces.\nOr you can pass any other single numeric IPv4 address."); - } else - addr.sin_addr.s_addr = INADDR_ANY; - - // HACKISH - int on = 1; - setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, on.sizeof); - // end hack - - - if(bind(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) { - close(sock); - throw new Exception("bind"); - } - - // FIXME: if this queue is full, it will just ignore it - // and wait for the client to retransmit it. This is an - // obnoxious timeout condition there. - if(sock.listen(128) == -1) { - close(sock); - throw new Exception("listen"); - } - params.dropPrivileges(); - } - - version(embedded_httpd_processes_accept_after_fork) {} else { - int pipeReadFd; - int pipeWriteFd; - - { - int[2] pipeFd; - if(socketpair(AF_UNIX, SOCK_DGRAM, 0, pipeFd)) { - import core.stdc.errno; - throw new Exception("pipe failed " ~ to!string(errno)); - } - - pipeReadFd = pipeFd[0]; - pipeWriteFd = pipeFd[1]; - } - } - - - int processCount; - pid_t newPid; - reopen: - while(processCount < processPoolSize) { - newPid = fork(); - if(newPid == 0) { - // start serving on the socket - //ubyte[4096] backingBuffer; - for(;;) { - bool closeConnection; - uint i; - sockaddr addr; - i = addr.sizeof; - version(embedded_httpd_processes_accept_after_fork) { - int s = accept(sock, &addr, &i); - int opt = 1; - import core.sys.posix.netinet.tcp; - // the Cgi class does internal buffering, so disabling this - // helps with latency in many cases... - setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof); - cloexec(s); - } else { - int s; - auto readret = read_fd(pipeReadFd, &s, s.sizeof, &s); - if(readret != s.sizeof) { - import core.stdc.errno; - throw new Exception("pipe read failed " ~ to!string(errno)); - } - - //writeln("process ", getpid(), " got socket ", s); - } - - try { - - if(s == -1) - throw new Exception("accept"); - - scope(failure) close(s); - //ubyte[__traits(classInstanceSize, BufferedInputRange)] bufferedRangeContainer; - auto ir = new BufferedInputRange(s); - //auto ir = emplace!BufferedInputRange(bufferedRangeContainer, s, backingBuffer); - - while(!ir.empty) { - //ubyte[__traits(classInstanceSize, CustomCgi)] cgiContainer; - - Cgi cgi; - try { - cgi = new CustomCgi(ir, &closeConnection); - cgi._outputFileHandle = cast(CgiConnectionHandle) s; - // if we have a single process and the browser tries to leave the connection open while concurrently requesting another, it will block everything an deadlock since there's no other server to accept it. By closing after each request in this situation, it tells the browser to serialize for us. - if(processPoolSize <= 1) - closeConnection = true; - //cgi = emplace!CustomCgi(cgiContainer, ir, &closeConnection); - } catch(Throwable t) { - // a construction error is either bad code or bad request; bad request is what it should be since this is bug free :P - // anyway let's kill the connection - version(CRuntime_Musl) { - // LockingTextWriter fails here - // so working around it - auto estr = t.toString(); - stderr.rawWrite(estr); - stderr.rawWrite("\n"); - } else - stderr.writeln(t.toString()); - sendAll(ir.source, plainHttpError(false, "400 Bad Request", t)); - closeConnection = true; - break; - } - assert(cgi !is null); - scope(exit) - cgi.dispose(); - - try { - fun(cgi); - cgi.close(); - if(cgi.websocketMode) - closeConnection = true; - } catch(ConnectionException ce) { - closeConnection = true; - } catch(Throwable t) { - // a processing error can be recovered from - version(CRuntime_Musl) { - // LockingTextWriter fails here - // so working around it - auto estr = t.toString(); - stderr.rawWrite(estr); - } else { - stderr.writeln(t.toString); - } - if(!handleException(cgi, t)) - closeConnection = true; - } - - if(closeConnection) { - ir.source.close(); - break; - } else { - if(!ir.empty) - ir.popFront(); // get the next - else if(ir.sourceClosed) { - ir.source.close(); - } - } - } - - ir.source.close(); - } catch(Throwable t) { - version(CRuntime_Musl) {} else - debug writeln(t); - // most likely cause is a timeout - } - } - } else if(newPid < 0) { - throw new Exception("fork failed"); - } else { - processCount++; - } - } - - // the parent should wait for its children... - if(newPid) { - import core.sys.posix.sys.wait; - - version(embedded_httpd_processes_accept_after_fork) {} else { - import core.sys.posix.sys.select; - int[] fdQueue; - while(true) { - // writeln("select call"); - int nfds = pipeWriteFd; - if(sock > pipeWriteFd) - nfds = sock; - nfds += 1; - fd_set read_fds; - fd_set write_fds; - FD_ZERO(&read_fds); - FD_ZERO(&write_fds); - FD_SET(sock, &read_fds); - if(fdQueue.length) - FD_SET(pipeWriteFd, &write_fds); - auto ret = select(nfds, &read_fds, &write_fds, null, null); - if(ret == -1) { - import core.stdc.errno; - if(errno == EINTR) - goto try_wait; - else - throw new Exception("wtf select"); - } - - int s = -1; - if(FD_ISSET(sock, &read_fds)) { - uint i; - sockaddr addr; - i = addr.sizeof; - s = accept(sock, &addr, &i); - cloexec(s); - import core.sys.posix.netinet.tcp; - int opt = 1; - setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, opt.sizeof); - } - - if(FD_ISSET(pipeWriteFd, &write_fds)) { - if(s == -1 && fdQueue.length) { - s = fdQueue[0]; - fdQueue = fdQueue[1 .. $]; // FIXME reuse buffer - } - write_fd(pipeWriteFd, &s, s.sizeof, s); - close(s); // we are done with it, let the other process take ownership - } else - fdQueue ~= s; - } - } - - try_wait: - - int status; - while(-1 != wait(&status)) { - version(CRuntime_Musl) {} else { - import std.stdio; writeln("Process died ", status); - } - processCount--; - goto reopen; - } - close(sock); - } -} - -version(fastcgi) -void serveFastCgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(RequestServer params) { - // SetHandler fcgid-script - FCGX_Stream* input, output, error; - FCGX_ParamArray env; - - - - const(ubyte)[] getFcgiChunk() { - const(ubyte)[] ret; - while(FCGX_HasSeenEOF(input) != -1) - ret ~= cast(ubyte) FCGX_GetChar(input); - return ret; - } - - void writeFcgi(const(ubyte)[] data) { - FCGX_PutStr(data.ptr, data.length, output); - } - - void doARequest() { - string[string] fcgienv; - - for(auto e = env; e !is null && *e !is null; e++) { - string cur = to!string(*e); - auto idx = cur.indexOf("="); - string name, value; - if(idx == -1) - name = cur; - else { - name = cur[0 .. idx]; - value = cur[idx + 1 .. $]; - } - - fcgienv[name] = value; - } - - void flushFcgi() { - FCGX_FFlush(output); - } - - Cgi cgi; - try { - cgi = new CustomCgi(maxContentLength, fcgienv, &getFcgiChunk, &writeFcgi, &flushFcgi); - } catch(Throwable t) { - FCGX_PutStr(cast(ubyte*) t.msg.ptr, t.msg.length, error); - writeFcgi(cast(const(ubyte)[]) plainHttpError(true, "400 Bad Request", t)); - return; //continue; - } - assert(cgi !is null); - scope(exit) cgi.dispose(); - try { - fun(cgi); - cgi.close(); - } catch(Throwable t) { - // log it to the error stream - FCGX_PutStr(cast(ubyte*) t.msg.ptr, t.msg.length, error); - // handle it for the user, if we can - if(!handleException(cgi, t)) - return; // continue; - } - } - - auto lp = params.listeningPort; - auto host = params.listeningHost; - - FCGX_Request request; - if(lp || !host.empty) { - // if a listening port was specified on the command line, we want to spawn ourself - // (needed for nginx without spawn-fcgi, e.g. on Windows) - FCGX_Init(); - - int sock; - - if(host.startsWith("unix:")) { - sock = FCGX_OpenSocket(toStringz(params.listeningHost["unix:".length .. $]), 12); - } else if(host.startsWith("abstract:")) { - sock = FCGX_OpenSocket(toStringz("\0" ~ params.listeningHost["abstract:".length .. $]), 12); - } else { - sock = FCGX_OpenSocket(toStringz(params.listeningHost ~ ":" ~ to!string(lp)), 12); - } - - if(sock < 0) - throw new Exception("Couldn't listen on the port"); - FCGX_InitRequest(&request, sock, 0); - while(FCGX_Accept_r(&request) >= 0) { - input = request.inStream; - output = request.outStream; - error = request.errStream; - env = request.envp; - doARequest(); - } - } else { - // otherwise, assume the httpd is doing it (the case for Apache, IIS, and Lighttpd) - // using the version with a global variable since we are separate processes anyway - while(FCGX_Accept(&input, &output, &error, &env) >= 0) { - doARequest(); - } - } -} - -/// Returns the default listening port for the current cgi configuration. 8085 for embedded httpd, 4000 for scgi, irrelevant for others. -ushort defaultListeningPort() { - version(netman_httpd) - return 8080; - else version(embedded_httpd_processes) - return 8085; - else version(embedded_httpd_threads) - return 8085; - else version(scgi) - return 4000; - else - return 0; -} - -/// Default host for listening. 127.0.0.1 for scgi, null (aka all interfaces) for all others. If you want the server directly accessible from other computers on the network, normally use null. If not, 127.0.0.1 is a bit better. Settable with default handlers with --listening-host command line argument. -string defaultListeningHost() { - version(netman_httpd) - return null; - else version(embedded_httpd_processes) - return null; - else version(embedded_httpd_threads) - return null; - else version(scgi) - return "127.0.0.1"; - else - return null; - -} - -/++ - This is the function [GenericMain] calls. View its source for some simple boilerplate you can copy/paste and modify, or you can call it yourself from your `main`. - - Please note that this may spawn other helper processes that will call `main` again. It does this currently for the timer server and event source server (and the quasi-deprecated web socket server). - - Params: - fun = Your request handler - CustomCgi = a subclass of Cgi, if you wise to customize it further - maxContentLength = max POST size you want to allow - args = command-line arguments - - History: - Documented Sept 26, 2020. -+/ -void cgiMainImpl(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(string[] args) if(is(CustomCgi : Cgi)) { - if(tryAddonServers(args)) - return; - - if(trySimulatedRequest!(fun, CustomCgi)(args)) - return; - - RequestServer server; - // you can change the port here if you like - // server.listeningPort = 9000; - - // then call this to let the command line args override your default - server.configureFromCommandLine(args); - - // and serve the request(s). - server.serve!(fun, CustomCgi, maxContentLength)(); -} - -//version(plain_cgi) -void handleCgiRequest(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { - // standard CGI is the default version - - - // Set stdin to binary mode if necessary to avoid mangled newlines - // the fact that stdin is global means this could be trouble but standard cgi request - // handling is one per process anyway so it shouldn't actually be threaded here or anything. - version(Windows) { - version(Win64) - _setmode(std.stdio.stdin.fileno(), 0x8000); - else - setmode(std.stdio.stdin.fileno(), 0x8000); - } - - Cgi cgi; - try { - cgi = new CustomCgi(maxContentLength); - version(Posix) - cgi._outputFileHandle = cast(CgiConnectionHandle) 1; // stdout - else version(Windows) - cgi._outputFileHandle = cast(CgiConnectionHandle) GetStdHandle(STD_OUTPUT_HANDLE); - else static assert(0); - } catch(Throwable t) { - version(CRuntime_Musl) { - // LockingTextWriter fails here - // so working around it - auto s = t.toString(); - stderr.rawWrite(s); - stdout.rawWrite(plainHttpError(true, "400 Bad Request", t)); - } else { - stderr.writeln(t.msg); - // the real http server will probably handle this; - // most likely, this is a bug in Cgi. But, oh well. - stdout.write(plainHttpError(true, "400 Bad Request", t)); - } - return; - } - assert(cgi !is null); - scope(exit) cgi.dispose(); - - try { - fun(cgi); - cgi.close(); - } catch (Throwable t) { - version(CRuntime_Musl) { - // LockingTextWriter fails here - // so working around it - auto s = t.msg; - stderr.rawWrite(s); - } else { - stderr.writeln(t.msg); - } - if(!handleException(cgi, t)) - return; - } -} - -private __gshared int cancelfd = -1; - -/+ - The event loop for embedded_httpd_threads will prolly fiber dispatch - cgi constructors too, so slow posts will not monopolize a worker thread. - - May want to provide the worker task system just need to ensure all the fibers - has a big enough stack for real work... would also ideally like to reuse them. - - - So prolly bir would switch it to nonblocking. If it would block, it epoll - registers one shot with this existing fiber to take it over. - - new connection comes in. it picks a fiber off the free list, - or if there is none, it creates a new one. this fiber handles - this connection the whole time. - - epoll triggers the fiber when something comes in. it is called by - a random worker thread, it might change at any time. at least during - the constructor. maybe into the main body it will stay tied to a thread - just so TLS stuff doesn't randomly change in the middle. but I could - specify if you yield all bets are off. - - when the request is finished, if there's more data buffered, it just - keeps going. if there is no more data buffered, it epoll ctls to - get triggered when more data comes in. all one shot. - - when a connection is closed, the fiber returns and is then reset - and added to the free list. if the free list is full, the fiber is - just freed, this means it will balloon to a certain size but not generally - grow beyond that unless the activity keeps going. - - 256 KB stack i thnk per fiber. 4,000 active fibers per gigabyte of memory. - - So the fiber has its own magic methods to read and write. if they would block, it registers - for epoll and yields. when it returns, it read/writes and then returns back normal control. - - basically you issue the command and it tells you when it is done - - it needs to DEL the epoll thing when it is closed. add it when opened. mod it when anther thing issued - -+/ - -/++ - The stack size when a fiber is created. You can set this from your main or from a shared static constructor - to optimize your memory use if you know you don't need this much space. Be careful though, some functions use - more stack space than you realize and a recursive function (including ones like in dom.d) can easily grow fast! - - History: - Added July 10, 2021. Previously, it used the druntime default of 16 KB. -+/ -version(cgi_use_fiber) -__gshared size_t fiberStackSize = 4096 * 100; - -version(cgi_use_fiber) -class CgiFiber : Fiber { - private void function(Socket) f_handler; - private void f_handler_dg(Socket s) { // to avoid extra allocation w/ function - f_handler(s); - } - this(void function(Socket) handler) { - this.f_handler = handler; - this(&f_handler_dg); - } - - this(void delegate(Socket) handler) { - this.handler = handler; - super(&run, fiberStackSize); - } - - Socket connection; - void delegate(Socket) handler; - - void run() { - handler(connection); - } - - void delegate() postYield; - - private void setPostYield(scope void delegate() py) @nogc { - postYield = cast(void delegate()) py; - } - - void proceed() { - try { - call(); - auto py = postYield; - postYield = null; - if(py !is null) - py(); - } catch(Exception e) { - if(connection) - connection.close(); - goto terminate; - } - - if(state == State.TERM) { - terminate: - import core.memory; - GC.removeRoot(cast(void*) this); - } - } -} - -version(cgi_use_fiber) -version(Windows) { - -extern(Windows) private { - - import core.sys.windows.mswsock; - - alias GROUP=uint; - alias LPWSAPROTOCOL_INFOW = void*; - SOCKET WSASocketW(int af, int type, int protocol, LPWSAPROTOCOL_INFOW lpProtocolInfo, GROUP g, DWORD dwFlags); - int WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine); - int WSARecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine); - - struct WSABUF { - ULONG len; - CHAR *buf; - } - alias LPWSABUF = WSABUF*; - - alias WSAOVERLAPPED = OVERLAPPED; - alias LPWSAOVERLAPPED = LPOVERLAPPED; - /+ - - alias LPFN_ACCEPTEX = - BOOL - function( - SOCKET sListenSocket, - SOCKET sAcceptSocket, - //_Out_writes_bytes_(dwReceiveDataLength+dwLocalAddressLength+dwRemoteAddressLength) PVOID lpOutputBuffer, - void* lpOutputBuffer, - WORD dwReceiveDataLength, - WORD dwLocalAddressLength, - WORD dwRemoteAddressLength, - LPDWORD lpdwBytesReceived, - LPOVERLAPPED lpOverlapped - ); - - enum WSAID_ACCEPTEX = GUID([0xb5367df1,0xcbac,0x11cf,[0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92]]); - +/ - - enum WSAID_GETACCEPTEXSOCKADDRS = GUID(0xb5367df2,0xcbac,0x11cf,[0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92]); -} - -private class PseudoblockingOverlappedSocket : Socket { - SOCKET handle; - - CgiFiber fiber; - - this(AddressFamily af, SocketType st) { - auto handle = WSASocketW(af, st, 0, null, 0, 1 /*WSA_FLAG_OVERLAPPED*/); - if(!handle) - throw new Exception("WSASocketW"); - this.handle = handle; - - iocp = CreateIoCompletionPort(cast(HANDLE) handle, iocp, cast(ULONG_PTR) cast(void*) this, 0); - - if(iocp is null) { - writeln(GetLastError()); - throw new Exception("CreateIoCompletionPort"); - } - - super(cast(socket_t) handle, af); - } - this() pure nothrow @trusted { assert(0); } - - override void blocking(bool) {} // meaningless to us, just ignore it. - - protected override Socket accepting() pure nothrow { - assert(0); - } - - bool addressesParsed; - Address la; - Address ra; - - private void populateAddresses() { - if(addressesParsed) - return; - addressesParsed = true; - - int lalen, ralen; - - sockaddr_in* la; - sockaddr_in* ra; - - lpfnGetAcceptExSockaddrs( - scratchBuffer.ptr, - 0, // same as in the AcceptEx call! - sockaddr_in.sizeof + 16, - sockaddr_in.sizeof + 16, - cast(sockaddr**) &la, - &lalen, - cast(sockaddr**) &ra, - &ralen - ); - - if(la) - this.la = new InternetAddress(*la); - if(ra) - this.ra = new InternetAddress(*ra); - - } - - override @property @trusted Address localAddress() { - populateAddresses(); - return la; - } - override @property @trusted Address remoteAddress() { - populateAddresses(); - return ra; - } - - PseudoblockingOverlappedSocket accepted; - - __gshared static LPFN_ACCEPTEX lpfnAcceptEx; - __gshared static typeof(&GetAcceptExSockaddrs) lpfnGetAcceptExSockaddrs; - - override Socket accept() @trusted { - __gshared static LPFN_ACCEPTEX lpfnAcceptEx; - - if(lpfnAcceptEx is null) { - DWORD dwBytes; - GUID GuidAcceptEx = WSAID_ACCEPTEX; - - auto iResult = WSAIoctl(handle, 0xc8000006 /*SIO_GET_EXTENSION_FUNCTION_POINTER*/, - &GuidAcceptEx, GuidAcceptEx.sizeof, - &lpfnAcceptEx, lpfnAcceptEx.sizeof, - &dwBytes, null, null); - - GuidAcceptEx = WSAID_GETACCEPTEXSOCKADDRS; - iResult = WSAIoctl(handle, 0xc8000006 /*SIO_GET_EXTENSION_FUNCTION_POINTER*/, - &GuidAcceptEx, GuidAcceptEx.sizeof, - &lpfnGetAcceptExSockaddrs, lpfnGetAcceptExSockaddrs.sizeof, - &dwBytes, null, null); - - } - - auto pfa = new PseudoblockingOverlappedSocket(AddressFamily.INET, SocketType.STREAM); - accepted = pfa; - - SOCKET pendingForAccept = pfa.handle; - DWORD ignored; - - auto ret = lpfnAcceptEx(handle, - pendingForAccept, - // buffer to receive up front - pfa.scratchBuffer.ptr, - 0, - // size of local and remote addresses. normally + 16. - sockaddr_in.sizeof + 16, - sockaddr_in.sizeof + 16, - &ignored, // bytes would be given through the iocp instead but im not even requesting the thing - &overlapped - ); - - return pfa; - } - - override void connect(Address to) { assert(0); } - - DWORD lastAnswer; - ubyte[1024] scratchBuffer; - static assert(scratchBuffer.length > sockaddr_in.sizeof * 2 + 32); - - WSABUF[1] buffer; - OVERLAPPED overlapped; - override ptrdiff_t send(scope const(void)[] buf, SocketFlags flags) @trusted { - overlapped = overlapped.init; - buffer[0].len = cast(DWORD) buf.length; - buffer[0].buf = cast(CHAR*) buf.ptr; - fiber.setPostYield( () { - if(!WSASend(handle, buffer.ptr, cast(DWORD) buffer.length, null, 0, &overlapped, null)) { - if(GetLastError() != 997) { - //throw new Exception("WSASend fail"); - } - } - }); - - Fiber.yield(); - return lastAnswer; - } - override ptrdiff_t receive(scope void[] buf, SocketFlags flags) @trusted { - overlapped = overlapped.init; - buffer[0].len = cast(DWORD) buf.length; - buffer[0].buf = cast(CHAR*) buf.ptr; - - DWORD flags2 = 0; - - fiber.setPostYield(() { - if(!WSARecv(handle, buffer.ptr, cast(DWORD) buffer.length, null, &flags2 /* flags */, &overlapped, null)) { - if(GetLastError() != 997) { - //writeln("WSARecv ", WSAGetLastError()); - //throw new Exception("WSARecv fail"); - } - } - }); - - Fiber.yield(); - return lastAnswer; - } - - // I might go back and implement these for udp things. - override ptrdiff_t receiveFrom(scope void[] buf, SocketFlags flags, ref Address from) @trusted { - assert(0); - } - override ptrdiff_t receiveFrom(scope void[] buf, SocketFlags flags) @trusted { - assert(0); - } - override ptrdiff_t sendTo(scope const(void)[] buf, SocketFlags flags, Address to) @trusted { - assert(0); - } - override ptrdiff_t sendTo(scope const(void)[] buf, SocketFlags flags) @trusted { - assert(0); - } - - // lol overload sets - alias send = typeof(super).send; - alias receive = typeof(super).receive; - alias sendTo = typeof(super).sendTo; - alias receiveFrom = typeof(super).receiveFrom; - -} -} - -void doThreadHttpConnection(CustomCgi, alias fun)(Socket connection) { - assert(connection !is null); - version(cgi_use_fiber) { - auto fiber = new CgiFiber(&doThreadHttpConnectionGuts!(CustomCgi, fun)); - - version(Windows) { - (cast(PseudoblockingOverlappedSocket) connection).fiber = fiber; - } - - import core.memory; - GC.addRoot(cast(void*) fiber); - fiber.connection = connection; - fiber.proceed(); - } else { - doThreadHttpConnectionGuts!(CustomCgi, fun)(connection); - } -} - -void doThreadHttpConnectionGuts(CustomCgi, alias fun, bool alwaysCloseConnection = false)(Socket connection) { - scope(failure) { - // catch all for other errors - try { - sendAll(connection, plainHttpError(false, "500 Internal Server Error", null)); - connection.close(); - } catch(Exception e) {} // swallow it, we're aborting anyway. - } - - bool closeConnection = alwaysCloseConnection; - - /+ - ubyte[4096] inputBuffer = void; - ubyte[__traits(classInstanceSize, BufferedInputRange)] birBuffer = void; - ubyte[__traits(classInstanceSize, CustomCgi)] cgiBuffer = void; - - birBuffer[] = cast(ubyte[]) typeid(BufferedInputRange).initializer()[]; - BufferedInputRange ir = cast(BufferedInputRange) cast(void*) birBuffer.ptr; - ir.__ctor(connection, inputBuffer[], true); - +/ - - auto ir = new BufferedInputRange(connection); - - while(!ir.empty) { - - if(ir.view.length == 0) { - ir.popFront(); - if(ir.sourceClosed) { - connection.close(); - closeConnection = true; - break; - } - } - - Cgi cgi; - try { - cgi = new CustomCgi(ir, &closeConnection); - // There's a bunch of these casts around because the type matches up with - // the -version=.... specifiers, just you can also create a RequestServer - // and instantiate the things where the types don't match up. It isn't exactly - // correct but I also don't care rn. Might FIXME and either remove it later or something. - cgi._outputFileHandle = cast(CgiConnectionHandle) connection.handle; - } catch(ConnectionClosedException ce) { - closeConnection = true; - break; - } catch(ConnectionException ce) { - // broken pipe or something, just abort the connection - closeConnection = true; - break; - } catch(Throwable t) { - // a construction error is either bad code or bad request; bad request is what it should be since this is bug free :P - // anyway let's kill the connection - version(CRuntime_Musl) { - stderr.rawWrite(t.toString()); - stderr.rawWrite("\n"); - } else { - stderr.writeln(t.toString()); - } - sendAll(connection, plainHttpError(false, "400 Bad Request", t)); - closeConnection = true; - break; - } - assert(cgi !is null); - scope(exit) - cgi.dispose(); - - try { - fun(cgi); - cgi.close(); - if(cgi.websocketMode) - closeConnection = true; - } catch(ConnectionException ce) { - // broken pipe or something, just abort the connection - closeConnection = true; - } catch(ConnectionClosedException ce) { - // broken pipe or something, just abort the connection - closeConnection = true; - } catch(Throwable t) { - // a processing error can be recovered from - version(CRuntime_Musl) {} else - stderr.writeln(t.toString); - if(!handleException(cgi, t)) - closeConnection = true; - } - - if(globalStopFlag) - closeConnection = true; - - if(closeConnection || alwaysCloseConnection) { - connection.shutdown(SocketShutdown.BOTH); - connection.close(); - ir.dispose(); - closeConnection = false; // don't reclose after loop - break; - } else { - if(ir.front.length) { - ir.popFront(); // we can't just discard the buffer, so get the next bit and keep chugging along - } else if(ir.sourceClosed) { - ir.source.shutdown(SocketShutdown.BOTH); - ir.source.close(); - ir.dispose(); - closeConnection = false; - } else { - continue; - // break; // this was for a keepalive experiment - } - } - } - - if(closeConnection) { - connection.shutdown(SocketShutdown.BOTH); - connection.close(); - ir.dispose(); - } - - // I am otherwise NOT closing it here because the parent thread might still be able to make use of the keep-alive connection! -} - -void doThreadScgiConnection(CustomCgi, alias fun, long maxContentLength)(Socket connection) { - // and now we can buffer - scope(failure) - connection.close(); - - import al = std.algorithm; - - size_t size; - - string[string] headers; - - auto range = new BufferedInputRange(connection); - more_data: - auto chunk = range.front(); - // waiting for colon for header length - auto idx = indexOf(cast(string) chunk, ':'); - if(idx == -1) { - try { - range.popFront(); - } catch(Exception e) { - // it is just closed, no big deal - connection.close(); - return; - } - goto more_data; - } - - size = to!size_t(cast(string) chunk[0 .. idx]); - chunk = range.consume(idx + 1); - // reading headers - if(chunk.length < size) - range.popFront(0, size + 1); - // we are now guaranteed to have enough - chunk = range.front(); - assert(chunk.length > size); - - idx = 0; - string key; - string value; - foreach(part; al.splitter(chunk, '\0')) { - if(idx & 1) { // odd is value - value = cast(string)(part.idup); - headers[key] = value; // commit - } else - key = cast(string)(part.idup); - idx++; - } - - enforce(chunk[size] == ','); // the terminator - - range.consume(size + 1); - // reading data - // this will be done by Cgi - - const(ubyte)[] getScgiChunk() { - // we are already primed - auto data = range.front(); - if(data.length == 0 && !range.sourceClosed) { - range.popFront(0); - data = range.front(); - } else if (range.sourceClosed) - range.source.close(); - - return data; - } - - void writeScgi(const(ubyte)[] data) { - sendAll(connection, data); - } - - void flushScgi() { - // I don't *think* I have to do anything.... - } - - Cgi cgi; - try { - cgi = new CustomCgi(maxContentLength, headers, &getScgiChunk, &writeScgi, &flushScgi); - cgi._outputFileHandle = cast(CgiConnectionHandle) connection.handle; - } catch(Throwable t) { - sendAll(connection, plainHttpError(true, "400 Bad Request", t)); - connection.close(); - return; // this connection is dead - } - assert(cgi !is null); - scope(exit) cgi.dispose(); - try { - fun(cgi); - cgi.close(); - connection.close(); - } catch(Throwable t) { - // no std err - if(!handleException(cgi, t)) { - connection.close(); - return; - } else { - connection.close(); - return; - } - } -} - -string printDate(DateTime date) { - char[29] buffer = void; - printDateToBuffer(date, buffer[]); - return buffer.idup; -} - -int printDateToBuffer(DateTime date, char[] buffer) @nogc { - assert(buffer.length >= 29); - // 29 static length ? - - static immutable daysOfWeek = [ - "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" - ]; - - static immutable months = [ - null, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" - ]; - - buffer[0 .. 3] = daysOfWeek[date.dayOfWeek]; - buffer[3 .. 5] = ", "; - buffer[5] = date.day / 10 + '0'; - buffer[6] = date.day % 10 + '0'; - buffer[7] = ' '; - buffer[8 .. 11] = months[date.month]; - buffer[11] = ' '; - auto y = date.year; - buffer[12] = cast(char) (y / 1000 + '0'); y %= 1000; - buffer[13] = cast(char) (y / 100 + '0'); y %= 100; - buffer[14] = cast(char) (y / 10 + '0'); y %= 10; - buffer[15] = cast(char) (y + '0'); - buffer[16] = ' '; - buffer[17] = date.hour / 10 + '0'; - buffer[18] = date.hour % 10 + '0'; - buffer[19] = ':'; - buffer[20] = date.minute / 10 + '0'; - buffer[21] = date.minute % 10 + '0'; - buffer[22] = ':'; - buffer[23] = date.second / 10 + '0'; - buffer[24] = date.second % 10 + '0'; - buffer[25 .. $] = " GMT"; - - return 29; -} - - -// Referencing this gigantic typeid seems to remind the compiler -// to actually put the symbol in the object file. I guess the immutable -// assoc array array isn't actually included in druntime -void hackAroundLinkerError() { - stdout.rawWrite(typeid(const(immutable(char)[][])[immutable(char)[]]).toString()); - stdout.rawWrite(typeid(immutable(char)[][][immutable(char)[]]).toString()); - stdout.rawWrite(typeid(Cgi.UploadedFile[immutable(char)[]]).toString()); - stdout.rawWrite(typeid(Cgi.UploadedFile[][immutable(char)[]]).toString()); - stdout.rawWrite(typeid(immutable(Cgi.UploadedFile)[immutable(char)[]]).toString()); - stdout.rawWrite(typeid(immutable(Cgi.UploadedFile[])[immutable(char)[]]).toString()); - stdout.rawWrite(typeid(immutable(char[])[immutable(char)[]]).toString()); - // this is getting kinda ridiculous btw. Moving assoc arrays - // to the library is the pain that keeps on coming. - - // eh this broke the build on the work server - // stdout.rawWrite(typeid(immutable(char)[][immutable(string[])])); - stdout.rawWrite(typeid(immutable(string[])[immutable(char)[]]).toString()); -} - - - - - -version(fastcgi) { - pragma(lib, "fcgi"); - - static if(size_t.sizeof == 8) // 64 bit - alias long c_int; - else - alias int c_int; - - extern(C) { - struct FCGX_Stream { - ubyte* rdNext; - ubyte* wrNext; - ubyte* stop; - ubyte* stopUnget; - c_int isReader; - c_int isClosed; - c_int wasFCloseCalled; - c_int FCGI_errno; - void* function(FCGX_Stream* stream) fillBuffProc; - void* function(FCGX_Stream* stream, c_int doClose) emptyBuffProc; - void* data; - } - - // note: this is meant to be opaque, so don't access it directly - struct FCGX_Request { - int requestId; - int role; - FCGX_Stream* inStream; - FCGX_Stream* outStream; - FCGX_Stream* errStream; - char** envp; - void* paramsPtr; - int ipcFd; - int isBeginProcessed; - int keepConnection; - int appStatus; - int nWriters; - int flags; - int listen_sock; - } - - int FCGX_InitRequest(FCGX_Request *request, int sock, int flags); - void FCGX_Init(); - - int FCGX_Accept_r(FCGX_Request *request); - - - alias char** FCGX_ParamArray; - - c_int FCGX_Accept(FCGX_Stream** stdin, FCGX_Stream** stdout, FCGX_Stream** stderr, FCGX_ParamArray* envp); - c_int FCGX_GetChar(FCGX_Stream* stream); - c_int FCGX_PutStr(const ubyte* str, c_int n, FCGX_Stream* stream); - int FCGX_HasSeenEOF(FCGX_Stream* stream); - c_int FCGX_FFlush(FCGX_Stream *stream); - - int FCGX_OpenSocket(in char*, int); - } -} - - -/* This might go int a separate module eventually. It is a network input helper class. */ - -import std.socket; - -version(cgi_use_fiber) { - import core.thread; - - version(linux) { - import core.sys.linux.epoll; - - int epfd = -1; // thread local because EPOLLEXCLUSIVE works much better this way... weirdly. - } else version(Windows) { - // declaring the iocp thing below... - } else static assert(0, "The hybrid fiber server is not implemented on your OS."); -} - -version(Windows) - __gshared HANDLE iocp; - -version(cgi_use_fiber) { - version(linux) - private enum WakeupEvent { - Read = EPOLLIN, - Write = EPOLLOUT - } - else version(Windows) - private enum WakeupEvent { - Read, Write - } - else static assert(0); -} - -version(cgi_use_fiber) -private void registerEventWakeup(bool* registered, Socket source, WakeupEvent e) @nogc { - - // static cast since I know what i have in here and don't want to pay for dynamic cast - auto f = cast(CgiFiber) cast(void*) Fiber.getThis(); - - version(linux) { - f.setPostYield = () { - if(*registered) { - // rearm - epoll_event evt; - evt.events = e | EPOLLONESHOT; - evt.data.ptr = cast(void*) f; - if(epoll_ctl(epfd, EPOLL_CTL_MOD, source.handle, &evt) == -1) - throw new Exception("epoll_ctl"); - } else { - // initial registration - *registered = true ; - int fd = source.handle; - epoll_event evt; - evt.events = e | EPOLLONESHOT; - evt.data.ptr = cast(void*) f; - if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &evt) == -1) - throw new Exception("epoll_ctl"); - } - }; - - Fiber.yield(); - - f.setPostYield(null); - } else version(Windows) { - Fiber.yield(); - } - else static assert(0); -} - -version(cgi_use_fiber) -void unregisterSource(Socket s) { - version(linux) { - epoll_event evt; - epoll_ctl(epfd, EPOLL_CTL_DEL, s.handle(), &evt); - } else version(Windows) { - // intentionally blank - } - else static assert(0); -} - -// it is a class primarily for reference semantics -// I might change this interface -/// This is NOT ACTUALLY an input range! It is too different. Historical mistake kinda. -class BufferedInputRange { - version(Posix) - this(int source, ubyte[] buffer = null) { - this(new Socket(cast(socket_t) source, AddressFamily.INET), buffer); - } - - this(Socket source, ubyte[] buffer = null, bool allowGrowth = true) { - // if they connect but never send stuff to us, we don't want it wasting the process - // so setting a time out - version(cgi_use_fiber) - source.blocking = false; - else - source.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(3)); - - this.source = source; - if(buffer is null) { - underlyingBuffer = new ubyte[4096]; - this.allowGrowth = true; - } else { - underlyingBuffer = buffer; - this.allowGrowth = allowGrowth; - } - - assert(underlyingBuffer.length); - - // we assume view.ptr is always inside underlyingBuffer - view = underlyingBuffer[0 .. 0]; - - popFront(); // prime - } - - version(cgi_use_fiber) { - bool registered; - } - - void dispose() { - version(cgi_use_fiber) { - if(registered) - unregisterSource(source); - } - } - - /** - A slight difference from regular ranges is you can give it the maximum - number of bytes to consume. - - IMPORTANT NOTE: the default is to consume nothing, so if you don't call - consume() yourself and use a regular foreach, it will infinitely loop! - - The default is to do what a normal range does, and consume the whole buffer - and wait for additional input. - - You can also specify 0, to append to the buffer, or any other number - to remove the front n bytes and wait for more. - */ - void popFront(size_t maxBytesToConsume = 0 /*size_t.max*/, size_t minBytesToSettleFor = 0, bool skipConsume = false) { - if(sourceClosed) - throw new ConnectionClosedException("can't get any more data from a closed source"); - if(!skipConsume) - consume(maxBytesToConsume); - - // we might have to grow the buffer - if(minBytesToSettleFor > underlyingBuffer.length || view.length == underlyingBuffer.length) { - if(allowGrowth) { - //import std.stdio; writeln("growth"); - auto viewStart = view.ptr - underlyingBuffer.ptr; - size_t growth = 4096; - // make sure we have enough for what we're being asked for - if(minBytesToSettleFor > 0 && minBytesToSettleFor - underlyingBuffer.length > growth) - growth = minBytesToSettleFor - underlyingBuffer.length; - //import std.stdio; writeln(underlyingBuffer.length, " ", viewStart, " ", view.length, " ", growth, " ", minBytesToSettleFor, " ", minBytesToSettleFor - underlyingBuffer.length); - underlyingBuffer.length += growth; - view = underlyingBuffer[viewStart .. view.length]; - } else - throw new Exception("No room left in the buffer"); - } - - do { - auto freeSpace = underlyingBuffer[view.ptr - underlyingBuffer.ptr + view.length .. $]; - try_again: - auto ret = source.receive(freeSpace); - if(ret == Socket.ERROR) { - if(wouldHaveBlocked()) { - version(cgi_use_fiber) { - registerEventWakeup(®istered, source, WakeupEvent.Read); - goto try_again; - } else { - // gonna treat a timeout here as a close - sourceClosed = true; - return; - } - } - version(Posix) { - import core.stdc.errno; - if(errno == EINTR || errno == EAGAIN) { - goto try_again; - } - if(errno == ECONNRESET) { - sourceClosed = true; - return; - } - } - throw new Exception(lastSocketError); // FIXME - } - if(ret == 0) { - sourceClosed = true; - return; - } - - //import std.stdio; writeln(view.ptr); writeln(underlyingBuffer.ptr); writeln(view.length, " ", ret, " = ", view.length + ret); - view = underlyingBuffer[view.ptr - underlyingBuffer.ptr .. view.length + ret]; - //import std.stdio; writeln(cast(string) view); - } while(view.length < minBytesToSettleFor); - } - - /// Removes n bytes from the front of the buffer, and returns the new buffer slice. - /// You might want to idup the data you are consuming if you store it, since it may - /// be overwritten on the new popFront. - /// - /// You do not need to call this if you always want to wait for more data when you - /// consume some. - ubyte[] consume(size_t bytes) { - //import std.stdio; writeln("consuime ", bytes, "/", view.length); - view = view[bytes > $ ? $ : bytes .. $]; - if(view.length == 0) { - view = underlyingBuffer[0 .. 0]; // go ahead and reuse the beginning - /* - writeln("HERE"); - popFront(0, 0, true); // try to load more if we can, checks if the source is closed - writeln(cast(string)front); - writeln("DONE"); - */ - } - return front; - } - - bool empty() { - return sourceClosed && view.length == 0; - } - - ubyte[] front() { - return view; - } - - invariant() { - assert(view.ptr >= underlyingBuffer.ptr); - // it should never be equal, since if that happens view ought to be empty, and thus reusing the buffer - assert(view.ptr < underlyingBuffer.ptr + underlyingBuffer.length); - } - - ubyte[] underlyingBuffer; - bool allowGrowth; - ubyte[] view; - Socket source; - bool sourceClosed; -} - -private class FakeSocketForStdin : Socket { - import std.stdio; - - this() { - - } - - private bool closed; - - override ptrdiff_t receive(scope void[] buffer, std.socket.SocketFlags) @trusted { - if(closed) - throw new Exception("Closed"); - return stdin.rawRead(buffer).length; - } - - override ptrdiff_t send(const scope void[] buffer, std.socket.SocketFlags) @trusted { - if(closed) - throw new Exception("Closed"); - stdout.rawWrite(buffer); - return buffer.length; - } - - override void close() @trusted scope { - (cast(void delegate() @nogc nothrow) &realClose)(); - } - - override void shutdown(SocketShutdown s) { - // FIXME - } - - override void setOption(SocketOptionLevel, SocketOption, scope void[]) {} - override void setOption(SocketOptionLevel, SocketOption, Duration) {} - - override @property @trusted Address remoteAddress() { return null; } - override @property @trusted Address localAddress() { return null; } - - void realClose() { - closed = true; - try { - stdin.close(); - stdout.close(); - } catch(Exception e) { - - } - } -} - -import core.sync.semaphore; -import core.atomic; - -/** - To use this thing: - - --- - void handler(Socket s) { do something... } - auto manager = new ListeningConnectionManager("127.0.0.1", 80, &handler, &delegateThatDropsPrivileges); - manager.listen(); - --- - - The 4th parameter is optional. - - I suggest you use BufferedInputRange(connection) to handle the input. As a packet - comes in, you will get control. You can just continue; though to fetch more. - - - FIXME: should I offer an event based async thing like netman did too? Yeah, probably. -*/ -class ListeningConnectionManager { - Semaphore semaphore; - Socket[256] queue; - shared(ubyte) nextIndexFront; - ubyte nextIndexBack; - shared(int) queueLength; - - Socket acceptCancelable() { - version(Posix) { - import core.sys.posix.sys.select; - fd_set read_fds; - FD_ZERO(&read_fds); - FD_SET(listener.handle, &read_fds); - if(cancelfd != -1) - FD_SET(cancelfd, &read_fds); - auto max = listener.handle > cancelfd ? listener.handle : cancelfd; - auto ret = select(max + 1, &read_fds, null, null, null); - if(ret == -1) { - import core.stdc.errno; - if(errno == EINTR) - return null; - else - throw new Exception("wtf select"); - } - - if(cancelfd != -1 && FD_ISSET(cancelfd, &read_fds)) { - return null; - } - - if(FD_ISSET(listener.handle, &read_fds)) - return listener.accept(); - - return null; - } else { - - Socket socket = listener; - - auto check = new SocketSet(); - - keep_looping: - check.reset(); - check.add(socket); - - // just to check the stop flag on a kinda busy loop. i hate this FIXME - auto got = Socket.select(check, null, null, 3.seconds); - if(got > 0) - return listener.accept(); - if(globalStopFlag) - return null; - else - goto keep_looping; - } - } - - int defaultNumberOfThreads() { - import std.parallelism; - version(cgi_use_fiber) { - return totalCPUs * 1 + 1; - } else { - // I times 4 here because there's a good chance some will be blocked on i/o. - return totalCPUs * 4; - } - - } - - void listen() { - shared(int) loopBroken; - - version(Posix) { - import core.sys.posix.signal; - signal(SIGPIPE, SIG_IGN); - } - - version(linux) { - if(cancelfd == -1) - cancelfd = eventfd(0, 0); - } - - version(cgi_no_threads) { - // NEVER USE THIS - // it exists only for debugging and other special occasions - - // the thread mode is faster and less likely to stall the whole - // thing when a request is slow - while(!loopBroken && !globalStopFlag) { - auto sn = acceptCancelable(); - if(sn is null) continue; - cloexec(sn); - try { - handler(sn); - } catch(Exception e) { - // if a connection goes wrong, we want to just say no, but try to carry on unless it is an Error of some sort (in which case, we'll die. You might want an external helper program to revive the server when it dies) - sn.close(); - } - } - } else { - - if(useFork) { - version(linux) { - //asm { int 3; } - fork(); - } - } - - version(cgi_use_fiber) { - - version(Windows) { - listener.accept(); - } - - WorkerThread[] threads = new WorkerThread[](numberOfThreads); - foreach(i, ref thread; threads) { - thread = new WorkerThread(this, handler, cast(int) i); - thread.start(); - } - - bool fiber_crash_check() { - bool hasAnyRunning; - foreach(thread; threads) { - if(!thread.isRunning) { - thread.join(); - } else hasAnyRunning = true; - } - - return (!hasAnyRunning); - } - - - while(!globalStopFlag) { - Thread.sleep(1.seconds); - if(fiber_crash_check()) - break; - } - - } else { - semaphore = new Semaphore(); - - ConnectionThread[] threads = new ConnectionThread[](numberOfThreads); - foreach(i, ref thread; threads) { - thread = new ConnectionThread(this, handler, cast(int) i); - thread.start(); - } - - while(!loopBroken && !globalStopFlag) { - Socket sn; - - bool crash_check() { - bool hasAnyRunning; - foreach(thread; threads) { - if(!thread.isRunning) { - thread.join(); - } else hasAnyRunning = true; - } - - return (!hasAnyRunning); - } - - - void accept_new_connection() { - sn = acceptCancelable(); - if(sn is null) return; - cloexec(sn); - if(tcp) { - // disable Nagle's algorithm to avoid a 40ms delay when we send/recv - // on the socket because we do some buffering internally. I think this helps, - // certainly does for small requests, and I think it does for larger ones too - sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); - - sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); - } - } - - void existing_connection_new_data() { - // wait until a slot opens up - //int waited = 0; - while(queueLength >= queue.length) { - Thread.sleep(1.msecs); - //waited ++; - } - //if(waited) {import std.stdio; writeln(waited);} - synchronized(this) { - queue[nextIndexBack] = sn; - nextIndexBack++; - atomicOp!"+="(queueLength, 1); - } - semaphore.notify(); - } - - - accept_new_connection(); - if(sn !is null) - existing_connection_new_data(); - else if(sn is null && globalStopFlag) { - foreach(thread; threads) { - semaphore.notify(); - } - Thread.sleep(50.msecs); - } - - if(crash_check()) - break; - } - } - - // FIXME: i typically stop this with ctrl+c which never - // actually gets here. i need to do a sigint handler. - if(cleanup) - cleanup(); - } - } - - //version(linux) - //int epoll_fd; - - bool tcp; - void delegate() cleanup; - - private void function(Socket) fhandler; - private void dg_handler(Socket s) { - fhandler(s); - } - this(string host, ushort port, void function(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { - fhandler = handler; - this(host, port, &dg_handler, dropPrivs, useFork, numberOfThreads); - } - - this(string host, ushort port, void delegate(Socket) handler, void delegate() dropPrivs = null, bool useFork = cgi_use_fork_default, int numberOfThreads = 0) { - this.handler = handler; - this.useFork = useFork; - this.numberOfThreads = numberOfThreads ? numberOfThreads : defaultNumberOfThreads(); - - listener = startListening(host, port, tcp, cleanup, 128, dropPrivs); - - version(cgi_use_fiber) - if(useFork) - listener.blocking = false; - - // this is the UI control thread and thus gets more priority - Thread.getThis.priority = Thread.PRIORITY_MAX; - } - - Socket listener; - void delegate(Socket) handler; - - immutable bool useFork; - int numberOfThreads; -} - -Socket startListening(string host, ushort port, ref bool tcp, ref void delegate() cleanup, int backQueue, void delegate() dropPrivs) { - Socket listener; - if(host.startsWith("unix:")) { - version(Posix) { - listener = new Socket(AddressFamily.UNIX, SocketType.STREAM); - cloexec(listener); - string filename = host["unix:".length .. $].idup; - listener.bind(new UnixAddress(filename)); - cleanup = delegate() { - listener.close(); - import std.file; - remove(filename); - }; - tcp = false; - } else { - throw new Exception("unix sockets not supported on this system"); - } - } else if(host.startsWith("abstract:")) { - version(linux) { - listener = new Socket(AddressFamily.UNIX, SocketType.STREAM); - cloexec(listener); - string filename = "\0" ~ host["abstract:".length .. $]; - import std.stdio; stderr.writeln("Listening to abstract unix domain socket: ", host["abstract:".length .. $]); - listener.bind(new UnixAddress(filename)); - tcp = false; - } else { - throw new Exception("abstract unix sockets not supported on this system"); - } - } else { - version(cgi_use_fiber) { - version(Windows) - listener = new PseudoblockingOverlappedSocket(AddressFamily.INET, SocketType.STREAM); - else - listener = new TcpSocket(); - } else { - listener = new TcpSocket(); - } - cloexec(listener); - listener.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true); - listener.bind(host.length ? parseAddress(host, port) : new InternetAddress(port)); - cleanup = delegate() { - listener.close(); - }; - tcp = true; - } - - listener.listen(backQueue); - - if (dropPrivs !is null) // can be null, backwards compatibility - dropPrivs(); - - return listener; -} - -// helper function to send a lot to a socket. Since this blocks for the buffer (possibly several times), you should probably call it in a separate thread or something. -void sendAll(Socket s, const(void)[] data, string file = __FILE__, size_t line = __LINE__) { - if(data.length == 0) return; - ptrdiff_t amount; - //import std.stdio; writeln("***",cast(string) data,"///"); - do { - amount = s.send(data); - if(amount == Socket.ERROR) { - version(cgi_use_fiber) { - if(wouldHaveBlocked()) { - bool registered = true; - registerEventWakeup(®istered, s, WakeupEvent.Write); - continue; - } - } - throw new ConnectionException(s, lastSocketError, file, line); - } - assert(amount > 0); - - data = data[amount .. $]; - } while(data.length); -} - -class ConnectionException : Exception { - Socket socket; - this(Socket s, string msg, string file = __FILE__, size_t line = __LINE__) { - this.socket = s; - super(msg, file, line); - } -} - -alias void delegate(Socket) CMT; - -import core.thread; -/+ - cgi.d now uses a hybrid of event i/o and threads at the top level. - - Top level thread is responsible for accepting sockets and selecting on them. - - It then indicates to a child that a request is pending, and any random worker - thread that is free handles it. It goes into blocking mode and handles that - http request to completion. - - At that point, it goes back into the waiting queue. - - - This concept is only implemented on Linux. On all other systems, it still - uses the worker threads and semaphores (which is perfectly fine for a lot of - things! Just having a great number of keep-alive connections will break that.) - - - So the algorithm is: - - select(accept, event, pending) - if accept -> send socket to free thread, if any. if not, add socket to queue - if event -> send the signaling thread a socket from the queue, if not, mark it free - - event might block until it can be *written* to. it is a fifo sending socket fds! - - A worker only does one http request at a time, then signals its availability back to the boss. - - The socket the worker was just doing should be added to the one-off epoll read. If it is closed, - great, we can get rid of it. Otherwise, it is considered `pending`. The *kernel* manages that; the - actual FD will not be kept out here. - - So: - queue = sockets we know are ready to read now, but no worker thread is available - idle list = worker threads not doing anything else. they signal back and forth - - the workers all read off the event fd. This is the semaphore wait - - the boss waits on accept or other sockets read events (one off! and level triggered). If anything happens wrt ready read, - it puts it in the queue and writes to the event fd. - - The child could put the socket back in the epoll thing itself. - - The child needs to be able to gracefully handle being given a socket that just closed with no work. -+/ -class ConnectionThread : Thread { - this(ListeningConnectionManager lcm, CMT dg, int myThreadNumber) { - this.lcm = lcm; - this.dg = dg; - this.myThreadNumber = myThreadNumber; - super(&run); - } - - void run() { - while(true) { - // so if there's a bunch of idle keep-alive connections, it can - // consume all the worker threads... just sitting there. - lcm.semaphore.wait(); - if(globalStopFlag) - return; - Socket socket; - synchronized(lcm) { - auto idx = lcm.nextIndexFront; - socket = lcm.queue[idx]; - lcm.queue[idx] = null; - atomicOp!"+="(lcm.nextIndexFront, 1); - atomicOp!"-="(lcm.queueLength, 1); - } - try { - //import std.stdio; writeln(myThreadNumber, " taking it"); - dg(socket); - /+ - if(socket.isAlive) { - // process it more later - version(linux) { - import core.sys.linux.epoll; - epoll_event ev; - ev.events = EPOLLIN | EPOLLONESHOT | EPOLLET; - ev.data.fd = socket.handle; - import std.stdio; writeln("adding"); - if(epoll_ctl(lcm.epoll_fd, EPOLL_CTL_ADD, socket.handle, &ev) == -1) { - if(errno == EEXIST) { - ev.events = EPOLLIN | EPOLLONESHOT | EPOLLET; - ev.data.fd = socket.handle; - if(epoll_ctl(lcm.epoll_fd, EPOLL_CTL_MOD, socket.handle, &ev) == -1) - throw new Exception("epoll_ctl " ~ to!string(errno)); - } else - throw new Exception("epoll_ctl " ~ to!string(errno)); - } - //import std.stdio; writeln("keep alive"); - // writing to this private member is to prevent the GC from closing my precious socket when I'm trying to use it later - __traits(getMember, socket, "sock") = cast(socket_t) -1; - } else { - continue; // hope it times out in a reasonable amount of time... - } - } - +/ - } catch(ConnectionClosedException e) { - // can just ignore this, it is fairly normal - socket.close(); - } catch(Throwable e) { - import std.stdio; stderr.rawWrite(e.toString); stderr.rawWrite("\n"); - socket.close(); - } - } - } - - ListeningConnectionManager lcm; - CMT dg; - int myThreadNumber; -} - -version(cgi_use_fiber) -class WorkerThread : Thread { - this(ListeningConnectionManager lcm, CMT dg, int myThreadNumber) { - this.lcm = lcm; - this.dg = dg; - this.myThreadNumber = myThreadNumber; - super(&run); - } - - version(Windows) - void run() { - auto timeout = INFINITE; - PseudoblockingOverlappedSocket key; - OVERLAPPED* overlapped; - DWORD bytes; - while(!globalStopFlag && GetQueuedCompletionStatus(iocp, &bytes, cast(PULONG_PTR) &key, &overlapped, timeout)) { - if(key is null) - continue; - key.lastAnswer = bytes; - if(key.fiber) { - key.fiber.proceed(); - } else { - // we have a new connection, issue the first receive on it and issue the next accept - - auto sn = key.accepted; - - key.accept(); - - cloexec(sn); - if(lcm.tcp) { - // disable Nagle's algorithm to avoid a 40ms delay when we send/recv - // on the socket because we do some buffering internally. I think this helps, - // certainly does for small requests, and I think it does for larger ones too - sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); - - sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); - } - - dg(sn); - } - } - //SleepEx(INFINITE, TRUE); - } - - version(linux) - void run() { - - import core.sys.linux.epoll; - epfd = epoll_create1(EPOLL_CLOEXEC); - if(epfd == -1) - throw new Exception("epoll_create1 " ~ to!string(errno)); - scope(exit) { - import core.sys.posix.unistd; - close(epfd); - } - - { - epoll_event ev; - ev.events = EPOLLIN; - ev.data.fd = cancelfd; - epoll_ctl(epfd, EPOLL_CTL_ADD, cancelfd, &ev); - } - - epoll_event ev; - ev.events = EPOLLIN | EPOLLEXCLUSIVE; // EPOLLEXCLUSIVE is only available on kernels since like 2017 but that's prolly good enough. - ev.data.fd = lcm.listener.handle; - if(epoll_ctl(epfd, EPOLL_CTL_ADD, lcm.listener.handle, &ev) == -1) - throw new Exception("epoll_ctl " ~ to!string(errno)); - - - - while(!globalStopFlag) { - Socket sn; - - epoll_event[64] events; - auto nfds = epoll_wait(epfd, events.ptr, events.length, -1); - if(nfds == -1) { - if(errno == EINTR) - continue; - throw new Exception("epoll_wait " ~ to!string(errno)); - } - - foreach(idx; 0 .. nfds) { - auto flags = events[idx].events; - - if(cast(size_t) events[idx].data.ptr == cast(size_t) cancelfd) { - globalStopFlag = true; - //import std.stdio; writeln("exit heard"); - break; - } else if(cast(size_t) events[idx].data.ptr == cast(size_t) lcm.listener.handle) { - //import std.stdio; writeln(myThreadNumber, " woken up ", flags); - // this try/catch is because it is set to non-blocking mode - // and Phobos' stupid api throws an exception instead of returning - // if it would block. Why would it block? because a forked process - // might have beat us to it, but the wakeup event thundered our herds. - try - sn = lcm.listener.accept(); // don't need to do the acceptCancelable here since the epoll checks it better - catch(SocketAcceptException e) { continue; } - - cloexec(sn); - if(lcm.tcp) { - // disable Nagle's algorithm to avoid a 40ms delay when we send/recv - // on the socket because we do some buffering internally. I think this helps, - // certainly does for small requests, and I think it does for larger ones too - sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); - - sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); - } - - dg(sn); - } else { - if(cast(size_t) events[idx].data.ptr < 1024) { - throw new Exception("this doesn't look like a fiber pointer..."); - } - auto fiber = cast(CgiFiber) events[idx].data.ptr; - fiber.proceed(); - } - } - } - } - - ListeningConnectionManager lcm; - CMT dg; - int myThreadNumber; -} - - -/* Done with network helper */ - -/* Helpers for doing temporary files. Used both here and in web.d */ - -version(Windows) { - import core.sys.windows.windows; - extern(Windows) DWORD GetTempPathW(DWORD, LPWSTR); - alias GetTempPathW GetTempPath; -} - -version(Posix) { - static import linux = core.sys.posix.unistd; -} - -string getTempDirectory() { - string path; - version(Windows) { - wchar[1024] buffer; - auto len = GetTempPath(1024, buffer.ptr); - if(len == 0) - throw new Exception("couldn't find a temporary path"); - - auto b = buffer[0 .. len]; - - path = to!string(b); - } else - path = "/tmp/"; - - return path; -} - - -// I like std.date. These functions help keep my old code and data working with phobos changing. - -long sysTimeToDTime(in SysTime sysTime) { - return convert!("hnsecs", "msecs")(sysTime.stdTime - 621355968000000000L); -} - -long dateTimeToDTime(in DateTime dt) { - return sysTimeToDTime(cast(SysTime) dt); -} - -long getUtcTime() { // renamed primarily to avoid conflict with std.date itself - return sysTimeToDTime(Clock.currTime(UTC())); -} - -// NOTE: new SimpleTimeZone(minutes); can perhaps work with the getTimezoneOffset() JS trick -SysTime dTimeToSysTime(long dTime, immutable TimeZone tz = null) { - immutable hnsecs = convert!("msecs", "hnsecs")(dTime) + 621355968000000000L; - return SysTime(hnsecs, tz); -} - - - -// this is a helper to read HTTP transfer-encoding: chunked responses -immutable(ubyte[]) dechunk(BufferedInputRange ir) { - immutable(ubyte)[] ret; - - another_chunk: - // If here, we are at the beginning of a chunk. - auto a = ir.front(); - int chunkSize; - int loc = locationOf(a, "\r\n"); - while(loc == -1) { - ir.popFront(); - a = ir.front(); - loc = locationOf(a, "\r\n"); - } - - string hex; - hex = ""; - for(int i = 0; i < loc; i++) { - char c = a[i]; - if(c >= 'A' && c <= 'Z') - c += 0x20; - if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')) { - hex ~= c; - } else { - break; - } - } - - assert(hex.length); - - int power = 1; - int size = 0; - foreach(cc1; retro(hex)) { - dchar cc = cc1; - if(cc >= 'a' && cc <= 'z') - cc -= 0x20; - int val = 0; - if(cc >= '0' && cc <= '9') - val = cc - '0'; - else - val = cc - 'A' + 10; - - size += power * val; - power *= 16; - } - - chunkSize = size; - assert(size >= 0); - - if(loc + 2 > a.length) { - ir.popFront(0, a.length + loc + 2); - a = ir.front(); - } - - a = ir.consume(loc + 2); - - if(chunkSize == 0) { // we're done with the response - // if we got here, will change must be true.... - more_footers: - loc = locationOf(a, "\r\n"); - if(loc == -1) { - ir.popFront(); - a = ir.front; - goto more_footers; - } else { - assert(loc == 0); - ir.consume(loc + 2); - goto finish; - } - } else { - // if we got here, will change must be true.... - if(a.length < chunkSize + 2) { - ir.popFront(0, chunkSize + 2); - a = ir.front(); - } - - ret ~= (a[0..chunkSize]); - - if(!(a.length > chunkSize + 2)) { - ir.popFront(0, chunkSize + 2); - a = ir.front(); - } - assert(a[chunkSize] == 13); - assert(a[chunkSize+1] == 10); - a = ir.consume(chunkSize + 2); - chunkSize = 0; - goto another_chunk; - } - - finish: - return ret; -} - -// I want to be able to get data from multiple sources the same way... -interface ByChunkRange { - bool empty(); - void popFront(); - const(ubyte)[] front(); -} - -ByChunkRange byChunk(const(ubyte)[] data) { - return new class ByChunkRange { - override bool empty() { - return !data.length; - } - - override void popFront() { - if(data.length > 4096) - data = data[4096 .. $]; - else - data = null; - } - - override const(ubyte)[] front() { - return data[0 .. $ > 4096 ? 4096 : $]; - } - }; -} - -ByChunkRange byChunk(BufferedInputRange ir, size_t atMost) { - const(ubyte)[] f; - - f = ir.front; - if(f.length > atMost) - f = f[0 .. atMost]; - - return new class ByChunkRange { - override bool empty() { - return atMost == 0; - } - - override const(ubyte)[] front() { - return f; - } - - override void popFront() { - ir.consume(f.length); - atMost -= f.length; - auto a = ir.front(); - - if(a.length <= atMost) { - f = a; - atMost -= a.length; - a = ir.consume(a.length); - if(atMost != 0) - ir.popFront(); - if(f.length == 0) { - f = ir.front(); - } - } else { - // we actually have *more* here than we need.... - f = a[0..atMost]; - atMost = 0; - ir.consume(atMost); - } - } - }; -} - -version(cgi_with_websocket) { - // http://tools.ietf.org/html/rfc6455 - - /** - WEBSOCKET SUPPORT: - - Full example: - --- - import arsd.cgi; - - void websocketEcho(Cgi cgi) { - if(cgi.websocketRequested()) { - if(cgi.origin != "http://arsdnet.net") - throw new Exception("bad origin"); - auto websocket = cgi.acceptWebsocket(); - - websocket.send("hello"); - websocket.send(" world!"); - - auto msg = websocket.recv(); - while(msg.opcode != WebSocketOpcode.close) { - if(msg.opcode == WebSocketOpcode.text) { - websocket.send(msg.textData); - } else if(msg.opcode == WebSocketOpcode.binary) { - websocket.send(msg.data); - } - - msg = websocket.recv(); - } - - websocket.close(); - } else assert(0, "i want a web socket!"); - } - - mixin GenericMain!websocketEcho; - --- - */ - - class WebSocket { - Cgi cgi; - - private this(Cgi cgi) { - this.cgi = cgi; - - Socket socket = cgi.idlol.source; - socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"minutes"(5)); - } - - // returns true if data available, false if it timed out - bool recvAvailable(Duration timeout = dur!"msecs"(0)) { - if(!waitForNextMessageWouldBlock()) - return true; - if(isDataPending(timeout)) - return true; // this is kinda a lie. - - return false; - } - - public bool lowLevelReceive() { - auto bfr = cgi.idlol; - top: - auto got = bfr.front; - if(got.length) { - if(receiveBuffer.length < receiveBufferUsedLength + got.length) - receiveBuffer.length += receiveBufferUsedLength + got.length; - - receiveBuffer[receiveBufferUsedLength .. receiveBufferUsedLength + got.length] = got[]; - receiveBufferUsedLength += got.length; - bfr.consume(got.length); - - return true; - } - - if(bfr.sourceClosed) - return false; - - bfr.popFront(0); - if(bfr.sourceClosed) - return false; - goto top; - } - - - bool isDataPending(Duration timeout = 0.seconds) { - Socket socket = cgi.idlol.source; - - auto check = new SocketSet(); - check.add(socket); - - auto got = Socket.select(check, null, null, timeout); - if(got > 0) - return true; - return false; - } - - // note: this blocks - WebSocketFrame recv() { - return waitForNextMessage(); - } - - - - - private void llclose() { - cgi.close(); - } - - private void llsend(ubyte[] data) { - cgi.write(data); - cgi.flush(); - } - - void unregisterActiveSocket(WebSocket) {} - - /* copy/paste section { */ - - private int readyState_; - private ubyte[] receiveBuffer; - private size_t receiveBufferUsedLength; - - private Config config; - - enum CONNECTING = 0; /// Socket has been created. The connection is not yet open. - enum OPEN = 1; /// The connection is open and ready to communicate. - enum CLOSING = 2; /// The connection is in the process of closing. - enum CLOSED = 3; /// The connection is closed or couldn't be opened. - - /++ - - +/ - /// Group: foundational - static struct Config { - /++ - These control the size of the receive buffer. - - It starts at the initial size, will temporarily - balloon up to the maximum size, and will reuse - a buffer up to the likely size. - - Anything larger than the maximum size will cause - the connection to be aborted and an exception thrown. - This is to protect you against a peer trying to - exhaust your memory, while keeping the user-level - processing simple. - +/ - size_t initialReceiveBufferSize = 4096; - size_t likelyReceiveBufferSize = 4096; /// ditto - size_t maximumReceiveBufferSize = 10 * 1024 * 1024; /// ditto - - /++ - Maximum combined size of a message. - +/ - size_t maximumMessageSize = 10 * 1024 * 1024; - - string[string] cookies; /// Cookies to send with the initial request. cookies[name] = value; - string origin; /// Origin URL to send with the handshake, if desired. - string protocol; /// the protocol header, if desired. - - int pingFrequency = 5000; /// Amount of time (in msecs) of idleness after which to send an automatic ping - } - - /++ - Returns one of [CONNECTING], [OPEN], [CLOSING], or [CLOSED]. - +/ - int readyState() { - return readyState_; - } - - /++ - Closes the connection, sending a graceful teardown message to the other side. - +/ - /// Group: foundational - void close(int code = 0, string reason = null) - //in (reason.length < 123) - in { assert(reason.length < 123); } do - { - if(readyState_ != OPEN) - return; // it cool, we done - WebSocketFrame wss; - wss.fin = true; - wss.opcode = WebSocketOpcode.close; - wss.data = cast(ubyte[]) reason.dup; - wss.send(&llsend); - - readyState_ = CLOSING; - - llclose(); - } - - /++ - Sends a ping message to the server. This is done automatically by the library if you set a non-zero [Config.pingFrequency], but you can also send extra pings explicitly as well with this function. - +/ - /// Group: foundational - void ping() { - WebSocketFrame wss; - wss.fin = true; - wss.opcode = WebSocketOpcode.ping; - wss.send(&llsend); - } - - // automatically handled.... - void pong() { - WebSocketFrame wss; - wss.fin = true; - wss.opcode = WebSocketOpcode.pong; - wss.send(&llsend); - } - - /++ - Sends a text message through the websocket. - +/ - /// Group: foundational - void send(in char[] textData) { - WebSocketFrame wss; - wss.fin = true; - wss.opcode = WebSocketOpcode.text; - wss.data = cast(ubyte[]) textData.dup; - wss.send(&llsend); - } - - /++ - Sends a binary message through the websocket. - +/ - /// Group: foundational - void send(in ubyte[] binaryData) { - WebSocketFrame wss; - wss.fin = true; - wss.opcode = WebSocketOpcode.binary; - wss.data = cast(ubyte[]) binaryData.dup; - wss.send(&llsend); - } - - /++ - Waits for and returns the next complete message on the socket. - - Note that the onmessage function is still called, right before - this returns. - +/ - /// Group: blocking_api - public WebSocketFrame waitForNextMessage() { - do { - auto m = processOnce(); - if(m.populated) - return m; - } while(lowLevelReceive()); - - throw new ConnectionClosedException("Websocket receive timed out"); - //return WebSocketFrame.init; // FIXME? maybe. - } - - /++ - Tells if [waitForNextMessage] would block. - +/ - /// Group: blocking_api - public bool waitForNextMessageWouldBlock() { - checkAgain: - if(isMessageBuffered()) - return false; - if(!isDataPending()) - return true; - while(isDataPending()) - lowLevelReceive(); - goto checkAgain; - } - - /++ - Is there a message in the buffer already? - If `true`, [waitForNextMessage] is guaranteed to return immediately. - If `false`, check [isDataPending] as the next step. - +/ - /// Group: blocking_api - public bool isMessageBuffered() { - ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength]; - auto s = d; - if(d.length) { - auto orig = d; - auto m = WebSocketFrame.read(d); - // that's how it indicates that it needs more data - if(d !is orig) - return true; - } - - return false; - } - - private ubyte continuingType; - private ubyte[] continuingData; - //private size_t continuingDataLength; - - private WebSocketFrame processOnce() { - ubyte[] d = receiveBuffer[0 .. receiveBufferUsedLength]; - auto s = d; - // FIXME: handle continuation frames more efficiently. it should really just reuse the receive buffer. - WebSocketFrame m; - if(d.length) { - auto orig = d; - m = WebSocketFrame.read(d); - // that's how it indicates that it needs more data - if(d is orig) - return WebSocketFrame.init; - m.unmaskInPlace(); - switch(m.opcode) { - case WebSocketOpcode.continuation: - if(continuingData.length + m.data.length > config.maximumMessageSize) - throw new Exception("message size exceeded"); - - continuingData ~= m.data; - if(m.fin) { - if(ontextmessage) - ontextmessage(cast(char[]) continuingData); - if(onbinarymessage) - onbinarymessage(continuingData); - - continuingData = null; - } - break; - case WebSocketOpcode.text: - if(m.fin) { - if(ontextmessage) - ontextmessage(m.textData); - } else { - continuingType = m.opcode; - //continuingDataLength = 0; - continuingData = null; - continuingData ~= m.data; - } - break; - case WebSocketOpcode.binary: - if(m.fin) { - if(onbinarymessage) - onbinarymessage(m.data); - } else { - continuingType = m.opcode; - //continuingDataLength = 0; - continuingData = null; - continuingData ~= m.data; - } - break; - case WebSocketOpcode.close: - readyState_ = CLOSED; - if(onclose) - onclose(); - - unregisterActiveSocket(this); - break; - case WebSocketOpcode.ping: - pong(); - break; - case WebSocketOpcode.pong: - // just really references it is still alive, nbd. - break; - default: // ignore though i could and perhaps should throw too - } - } - - // the recv thing can be invalidated so gotta copy it over ugh - if(d.length) { - m.data = m.data.dup(); - } - - import core.stdc.string; - memmove(receiveBuffer.ptr, d.ptr, d.length); - receiveBufferUsedLength = d.length; - - return m; - } - - private void autoprocess() { - // FIXME - do { - processOnce(); - } while(lowLevelReceive()); - } - - - void delegate() onclose; /// - void delegate() onerror; /// - void delegate(in char[]) ontextmessage; /// - void delegate(in ubyte[]) onbinarymessage; /// - void delegate() onopen; /// - - /++ - - +/ - /// Group: browser_api - void onmessage(void delegate(in char[]) dg) { - ontextmessage = dg; - } - - /// ditto - void onmessage(void delegate(in ubyte[]) dg) { - onbinarymessage = dg; - } - - /* } end copy/paste */ - - - } - - bool websocketRequested(Cgi cgi) { - return - "sec-websocket-key" in cgi.requestHeaders - && - "connection" in cgi.requestHeaders && - cgi.requestHeaders["connection"].asLowerCase().canFind("upgrade") - && - "upgrade" in cgi.requestHeaders && - cgi.requestHeaders["upgrade"].asLowerCase().equal("websocket") - ; - } - - WebSocket acceptWebsocket(Cgi cgi) { - assert(!cgi.closed); - assert(!cgi.outputtedResponseData); - cgi.setResponseStatus("101 Switching Protocols"); - cgi.header("Upgrade: WebSocket"); - cgi.header("Connection: upgrade"); - - string key = cgi.requestHeaders["sec-websocket-key"]; - key ~= "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // the defined guid from the websocket spec - - import std.digest.sha; - auto hash = sha1Of(key); - auto accept = Base64.encode(hash); - - cgi.header(("Sec-WebSocket-Accept: " ~ accept).idup); - - cgi.websocketMode = true; - cgi.write(""); - - cgi.flush(); - - return new WebSocket(cgi); - } - - // FIXME get websocket to work on other modes, not just embedded_httpd - - /* copy/paste in http2.d { */ - enum WebSocketOpcode : ubyte { - continuation = 0, - text = 1, - binary = 2, - // 3, 4, 5, 6, 7 RESERVED - close = 8, - ping = 9, - pong = 10, - // 11,12,13,14,15 RESERVED - } - - public struct WebSocketFrame { - private bool populated; - bool fin; - bool rsv1; - bool rsv2; - bool rsv3; - WebSocketOpcode opcode; // 4 bits - bool masked; - ubyte lengthIndicator; // don't set this when building one to send - ulong realLength; // don't use when sending - ubyte[4] maskingKey; // don't set this when sending - ubyte[] data; - - static WebSocketFrame simpleMessage(WebSocketOpcode opcode, void[] data) { - WebSocketFrame msg; - msg.fin = true; - msg.opcode = opcode; - msg.data = cast(ubyte[]) data.dup; - - return msg; - } - - private void send(scope void delegate(ubyte[]) llsend) { - ubyte[64] headerScratch; - int headerScratchPos = 0; - - realLength = data.length; - - { - ubyte b1; - b1 |= cast(ubyte) opcode; - b1 |= rsv3 ? (1 << 4) : 0; - b1 |= rsv2 ? (1 << 5) : 0; - b1 |= rsv1 ? (1 << 6) : 0; - b1 |= fin ? (1 << 7) : 0; - - headerScratch[0] = b1; - headerScratchPos++; - } - - { - headerScratchPos++; // we'll set header[1] at the end of this - auto rlc = realLength; - ubyte b2; - b2 |= masked ? (1 << 7) : 0; - - assert(headerScratchPos == 2); - - if(realLength > 65535) { - // use 64 bit length - b2 |= 0x7f; - - // FIXME: double check endinaness - foreach(i; 0 .. 8) { - headerScratch[2 + 7 - i] = rlc & 0x0ff; - rlc >>>= 8; - } - - headerScratchPos += 8; - } else if(realLength > 125) { - // use 16 bit length - b2 |= 0x7e; - - // FIXME: double check endinaness - foreach(i; 0 .. 2) { - headerScratch[2 + 1 - i] = rlc & 0x0ff; - rlc >>>= 8; - } - - headerScratchPos += 2; - } else { - // use 7 bit length - b2 |= realLength & 0b_0111_1111; - } - - headerScratch[1] = b2; - } - - //assert(!masked, "masking key not properly implemented"); - if(masked) { - // FIXME: randomize this - headerScratch[headerScratchPos .. headerScratchPos + 4] = maskingKey[]; - headerScratchPos += 4; - - // we'll just mask it in place... - int keyIdx = 0; - foreach(i; 0 .. data.length) { - data[i] = data[i] ^ maskingKey[keyIdx]; - if(keyIdx == 3) - keyIdx = 0; - else - keyIdx++; - } - } - - //writeln("SENDING ", headerScratch[0 .. headerScratchPos], data); - llsend(headerScratch[0 .. headerScratchPos]); - llsend(data); - } - - static WebSocketFrame read(ref ubyte[] d) { - WebSocketFrame msg; - - auto orig = d; - - WebSocketFrame needsMoreData() { - d = orig; - return WebSocketFrame.init; - } - - if(d.length < 2) - return needsMoreData(); - - ubyte b = d[0]; - - msg.populated = true; - - msg.opcode = cast(WebSocketOpcode) (b & 0x0f); - b >>= 4; - msg.rsv3 = b & 0x01; - b >>= 1; - msg.rsv2 = b & 0x01; - b >>= 1; - msg.rsv1 = b & 0x01; - b >>= 1; - msg.fin = b & 0x01; - - b = d[1]; - msg.masked = (b & 0b1000_0000) ? true : false; - msg.lengthIndicator = b & 0b0111_1111; - - d = d[2 .. $]; - - if(msg.lengthIndicator == 0x7e) { - // 16 bit length - msg.realLength = 0; - - if(d.length < 2) return needsMoreData(); - - foreach(i; 0 .. 2) { - msg.realLength |= d[0] << ((1-i) * 8); - d = d[1 .. $]; - } - } else if(msg.lengthIndicator == 0x7f) { - // 64 bit length - msg.realLength = 0; - - if(d.length < 8) return needsMoreData(); - - foreach(i; 0 .. 8) { - msg.realLength |= ulong(d[0]) << ((7-i) * 8); - d = d[1 .. $]; - } - } else { - // 7 bit length - msg.realLength = msg.lengthIndicator; - } - - if(msg.masked) { - - if(d.length < 4) return needsMoreData(); - - msg.maskingKey = d[0 .. 4]; - d = d[4 .. $]; - } - - if(msg.realLength > d.length) { - return needsMoreData(); - } - - msg.data = d[0 .. cast(size_t) msg.realLength]; - d = d[cast(size_t) msg.realLength .. $]; - - return msg; - } - - void unmaskInPlace() { - if(this.masked) { - int keyIdx = 0; - foreach(i; 0 .. this.data.length) { - this.data[i] = this.data[i] ^ this.maskingKey[keyIdx]; - if(keyIdx == 3) - keyIdx = 0; - else - keyIdx++; - } - } - } - - char[] textData() { - return cast(char[]) data; - } - } - /* } */ -} - - -version(Windows) -{ - version(CRuntime_DigitalMars) - { - extern(C) int setmode(int, int) nothrow @nogc; - } - else version(CRuntime_Microsoft) - { - extern(C) int _setmode(int, int) nothrow @nogc; - alias setmode = _setmode; - } - else static assert(0); -} - -version(Posix) { - import core.sys.posix.unistd; - version(CRuntime_Musl) {} else { - private extern(C) int posix_spawn(pid_t*, const char*, void*, void*, const char**, const char**); - } -} - - -// FIXME: these aren't quite public yet. -//private: - -// template for laziness -void startAddonServer()(string arg) { - version(OSX) { - assert(0, "Not implemented"); - } else version(linux) { - import core.sys.posix.unistd; - pid_t pid; - const(char)*[16] args; - args[0] = "ARSD_CGI_ADDON_SERVER"; - args[1] = arg.ptr; - posix_spawn(&pid, "/proc/self/exe", - null, - null, - args.ptr, - null // env - ); - } else version(Windows) { - wchar[2048] filename; - auto len = GetModuleFileNameW(null, filename.ptr, cast(DWORD) filename.length); - if(len == 0 || len == filename.length) - throw new Exception("could not get process name to start helper server"); - - STARTUPINFOW startupInfo; - startupInfo.cb = cast(DWORD) startupInfo.sizeof; - PROCESS_INFORMATION processInfo; - - import std.utf; - - // I *MIGHT* need to run it as a new job or a service... - auto ret = CreateProcessW( - filename.ptr, - toUTF16z(arg), - null, // process attributes - null, // thread attributes - false, // inherit handles - 0, // creation flags - null, // environment - null, // working directory - &startupInfo, - &processInfo - ); - - if(!ret) - throw new Exception("create process failed"); - - // when done with those, if we set them - /* - CloseHandle(hStdInput); - CloseHandle(hStdOutput); - CloseHandle(hStdError); - */ - - } else static assert(0, "Websocket server not implemented on this system yet (email me, i can prolly do it if you need it)"); -} - -// template for laziness -/* - The websocket server is a single-process, single-thread, event - I/O thing. It is passed websockets from other CGI processes - and is then responsible for handling their messages and responses. - Note that the CGI process is responsible for websocket setup, - including authentication, etc. - - It also gets data sent to it by other processes and is responsible - for distributing that, as necessary. -*/ -void runWebsocketServer()() { - assert(0, "not implemented"); -} - -void sendToWebsocketServer(WebSocket ws, string group) { - assert(0, "not implemented"); -} - -void sendToWebsocketServer(string content, string group) { - assert(0, "not implemented"); -} - - -void runEventServer()() { - runAddonServer("/tmp/arsd_cgi_event_server", new EventSourceServerImplementation()); -} - -void runTimerServer()() { - runAddonServer("/tmp/arsd_scheduled_job_server", new ScheduledJobServerImplementation()); -} - -version(Posix) { - alias LocalServerConnectionHandle = int; - alias CgiConnectionHandle = int; - alias SocketConnectionHandle = int; - - enum INVALID_CGI_CONNECTION_HANDLE = -1; -} else version(Windows) { - alias LocalServerConnectionHandle = HANDLE; - version(embedded_httpd_threads) { - alias CgiConnectionHandle = SOCKET; - enum INVALID_CGI_CONNECTION_HANDLE = INVALID_SOCKET; - } else version(fastcgi) { - alias CgiConnectionHandle = void*; // Doesn't actually work! But I don't want compile to fail pointlessly at this point. - enum INVALID_CGI_CONNECTION_HANDLE = null; - } else version(scgi) { - alias CgiConnectionHandle = SOCKET; - enum INVALID_CGI_CONNECTION_HANDLE = INVALID_SOCKET; - } else { /* version(plain_cgi) */ - alias CgiConnectionHandle = HANDLE; - enum INVALID_CGI_CONNECTION_HANDLE = null; - } - alias SocketConnectionHandle = SOCKET; -} - -version(with_addon_servers_connections) -LocalServerConnectionHandle openLocalServerConnection()(string name, string arg) { - version(Posix) { - import core.sys.posix.unistd; - import core.sys.posix.sys.un; - - int sock = socket(AF_UNIX, SOCK_STREAM, 0); - if(sock == -1) - throw new Exception("socket " ~ to!string(errno)); - - scope(failure) - close(sock); - - cloexec(sock); - - // add-on server processes are assumed to be local, and thus will - // use unix domain sockets. Besides, I want to pass sockets to them, - // so it basically must be local (except for the session server, but meh). - sockaddr_un addr; - addr.sun_family = AF_UNIX; - version(linux) { - // on linux, we will use the abstract namespace - addr.sun_path[0] = 0; - addr.sun_path[1 .. name.length + 1] = cast(typeof(addr.sun_path[])) name[]; - } else { - // but otherwise, just use a file cuz we must. - addr.sun_path[0 .. name.length] = cast(typeof(addr.sun_path[])) name[]; - } - - bool alreadyTried; - - try_again: - - if(connect(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) { - if(!alreadyTried && errno == ECONNREFUSED) { - // try auto-spawning the server, then attempt connection again - startAddonServer(arg); - import core.thread; - Thread.sleep(50.msecs); - alreadyTried = true; - goto try_again; - } else - throw new Exception("connect " ~ to!string(errno)); - } - - return sock; - } else version(Windows) { - return null; // FIXME - } -} - -version(with_addon_servers_connections) -void closeLocalServerConnection(LocalServerConnectionHandle handle) { - version(Posix) { - import core.sys.posix.unistd; - close(handle); - } else version(Windows) - CloseHandle(handle); -} - -void runSessionServer()() { - runAddonServer("/tmp/arsd_session_server", new BasicDataServerImplementation()); -} - -version(Posix) -private void makeNonBlocking(int fd) { - import core.sys.posix.fcntl; - auto flags = fcntl(fd, F_GETFL, 0); - if(flags == -1) - throw new Exception("fcntl get"); - flags |= O_NONBLOCK; - auto s = fcntl(fd, F_SETFL, flags); - if(s == -1) - throw new Exception("fcntl set"); -} - -import core.stdc.errno; - -struct IoOp { - @disable this(); - @disable this(this); - - /* - So we want to be able to eventually handle generic sockets too. - */ - - enum Read = 1; - enum Write = 2; - enum Accept = 3; - enum ReadSocketHandle = 4; - - // Your handler may be called in a different thread than the one that initiated the IO request! - // It is also possible to have multiple io requests being called simultaneously. Use proper thread safety caution. - private bool delegate(IoOp*, int) handler; // returns true if you are done and want it to be closed - private void delegate(IoOp*) closeHandler; - private void delegate(IoOp*) completeHandler; - private int internalFd; - private int operation; - private int bufferLengthAllocated; - private int bufferLengthUsed; - private ubyte[1] internalBuffer; // it can be overallocated! - - ubyte[] allocatedBuffer() return { - return internalBuffer.ptr[0 .. bufferLengthAllocated]; - } - - ubyte[] usedBuffer() return { - return allocatedBuffer[0 .. bufferLengthUsed]; - } - - void reset() { - bufferLengthUsed = 0; - } - - int fd() { - return internalFd; - } -} - -IoOp* allocateIoOp(int fd, int operation, int bufferSize, bool delegate(IoOp*, int) handler) { - import core.stdc.stdlib; - - auto ptr = calloc(IoOp.sizeof + bufferSize, 1); - if(ptr is null) - assert(0); // out of memory! - - auto op = cast(IoOp*) ptr; - - op.handler = handler; - op.internalFd = fd; - op.operation = operation; - op.bufferLengthAllocated = bufferSize; - op.bufferLengthUsed = 0; - - import core.memory; - - GC.addRoot(ptr); - - return op; -} - -void freeIoOp(ref IoOp* ptr) { - - import core.memory; - GC.removeRoot(ptr); - - import core.stdc.stdlib; - free(ptr); - ptr = null; -} - -version(Posix) -version(with_addon_servers_connections) -void nonBlockingWrite(EventIoServer eis, int connection, const void[] data) { - - //import std.stdio : writeln; writeln(cast(string) data); - - import core.sys.posix.unistd; - - auto ret = write(connection, data.ptr, data.length); - if(ret != data.length) { - if(ret == 0 || (ret == -1 && (errno == EPIPE || errno == ETIMEDOUT))) { - // the file is closed, remove it - eis.fileClosed(connection); - } else - throw new Exception("alas " ~ to!string(ret) ~ " " ~ to!string(errno)); // FIXME - } -} -version(Windows) -version(with_addon_servers_connections) -void nonBlockingWrite(EventIoServer eis, int connection, const void[] data) { - // FIXME -} - -bool isInvalidHandle(CgiConnectionHandle h) { - return h == INVALID_CGI_CONNECTION_HANDLE; -} - -/+ -https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsarecv -https://support.microsoft.com/en-gb/help/181611/socket-overlapped-i-o-versus-blocking-nonblocking-mode -https://stackoverflow.com/questions/18018489/should-i-use-iocps-or-overlapped-wsasend-receive -https://docs.microsoft.com/en-us/windows/desktop/fileio/i-o-completion-ports -https://docs.microsoft.com/en-us/windows/desktop/fileio/createiocompletionport -https://docs.microsoft.com/en-us/windows/desktop/api/mswsock/nf-mswsock-acceptex -https://docs.microsoft.com/en-us/windows/desktop/Sync/waitable-timer-objects -https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-setwaitabletimer -https://docs.microsoft.com/en-us/windows/desktop/Sync/using-a-waitable-timer-with-an-asynchronous-procedure-call -https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsagetoverlappedresult - -+/ - -/++ - You can customize your server by subclassing the appropriate server. Then, register your - subclass at compile time with the [registerEventIoServer] template, or implement your own - main function and call it yourself. - - $(TIP If you make your subclass a `final class`, there is a slight performance improvement.) -+/ -version(with_addon_servers_connections) -interface EventIoServer { - bool handleLocalConnectionData(IoOp* op, int receivedFd); - void handleLocalConnectionClose(IoOp* op); - void handleLocalConnectionComplete(IoOp* op); - void wait_timeout(); - void fileClosed(int fd); - - void epoll_fd(int fd); -} - -// the sink should buffer it -private void serialize(T)(scope void delegate(scope ubyte[]) sink, T t) { - static if(is(T == struct)) { - foreach(member; __traits(allMembers, T)) - serialize(sink, __traits(getMember, t, member)); - } else static if(is(T : int)) { - // no need to think of endianness just because this is only used - // for local, same-machine stuff anyway. thanks private lol - sink((cast(ubyte*) &t)[0 .. t.sizeof]); - } else static if(is(T == string) || is(T : const(ubyte)[])) { - // these are common enough to optimize - int len = cast(int) t.length; // want length consistent size tho, in case 32 bit program sends to 64 bit server, etc. - sink((cast(ubyte*) &len)[0 .. int.sizeof]); - sink(cast(ubyte[]) t[]); - } else static if(is(T : A[], A)) { - // generic array is less optimal but still prolly ok - int len = cast(int) t.length; - sink((cast(ubyte*) &len)[0 .. int.sizeof]); - foreach(item; t) - serialize(sink, item); - } else static assert(0, T.stringof); -} - -// all may be stack buffers, so use cautio -private void deserialize(T)(scope ubyte[] delegate(int sz) get, scope void delegate(T) dg) { - static if(is(T == struct)) { - T t; - foreach(member; __traits(allMembers, T)) - deserialize!(typeof(__traits(getMember, T, member)))(get, (mbr) { __traits(getMember, t, member) = mbr; }); - dg(t); - } else static if(is(T : int)) { - // no need to think of endianness just because this is only used - // for local, same-machine stuff anyway. thanks private lol - T t; - auto data = get(t.sizeof); - t = (cast(T[]) data)[0]; - dg(t); - } else static if(is(T == string) || is(T : const(ubyte)[])) { - // these are common enough to optimize - int len; - auto data = get(len.sizeof); - len = (cast(int[]) data)[0]; - - /* - typeof(T[0])[2000] stackBuffer; - T buffer; - - if(len < stackBuffer.length) - buffer = stackBuffer[0 .. len]; - else - buffer = new T(len); - - data = get(len * typeof(T[0]).sizeof); - */ - - T t = cast(T) get(len * cast(int) typeof(T.init[0]).sizeof); - - dg(t); - } else static if(is(T == E[], E)) { - T t; - int len; - auto data = get(len.sizeof); - len = (cast(int[]) data)[0]; - t.length = len; - foreach(ref e; t) { - deserialize!E(get, (ele) { e = ele; }); - } - dg(t); - } else static assert(0, T.stringof); -} - -unittest { - serialize((ubyte[] b) { - deserialize!int( sz => b[0 .. sz], (t) { assert(t == 1); }); - }, 1); - serialize((ubyte[] b) { - deserialize!int( sz => b[0 .. sz], (t) { assert(t == 56674); }); - }, 56674); - ubyte[1000] buffer; - int bufferPoint; - void add(ubyte[] b) { - buffer[bufferPoint .. bufferPoint + b.length] = b[]; - bufferPoint += b.length; - } - ubyte[] get(int sz) { - auto b = buffer[bufferPoint .. bufferPoint + sz]; - bufferPoint += sz; - return b; - } - serialize(&add, "test here"); - bufferPoint = 0; - deserialize!string(&get, (t) { assert(t == "test here"); }); - bufferPoint = 0; - - struct Foo { - int a; - ubyte c; - string d; - } - serialize(&add, Foo(403, 37, "amazing")); - bufferPoint = 0; - deserialize!Foo(&get, (t) { - assert(t.a == 403); - assert(t.c == 37); - assert(t.d == "amazing"); - }); - bufferPoint = 0; -} - -/* - Here's the way the RPC interface works: - - You define the interface that lists the functions you can call on the remote process. - The interface may also have static methods for convenience. These forward to a singleton - instance of an auto-generated class, which actually sends the args over the pipe. - - An impl class actually implements it. A receiving server deserializes down the pipe and - calls methods on the class. - - I went with the interface to get some nice compiler checking and documentation stuff. - - I could have skipped the interface and just implemented it all from the server class definition - itself, but then the usage may call the method instead of rpcing it; I just like having the user - interface and the implementation separate so you aren't tempted to `new impl` to call the methods. - - - I fiddled with newlines in the mixin string to ensure the assert line numbers matched up to the source code line number. Idk why dmd didn't do this automatically, but it was important to me. - - Realistically though the bodies would just be - connection.call(this.mangleof, args...) sooooo. - - FIXME: overloads aren't supported -*/ - -/// Base for storing sessions in an array. Exists primarily for internal purposes and you should generally not use this. -interface SessionObject {} - -private immutable void delegate(string[])[string] scheduledJobHandlers; -private immutable void delegate(string[])[string] websocketServers; - -version(with_breaking_cgi_features) -mixin(q{ - -mixin template ImplementRpcClientInterface(T, string serverPath, string cmdArg) { - static import std.traits; - - // derivedMembers on an interface seems to give exactly what I want: the virtual functions we need to implement. so I am just going to use it directly without more filtering. - static foreach(idx, member; __traits(derivedMembers, T)) { - static if(__traits(isVirtualMethod, __traits(getMember, T, member))) - mixin( q{ - std.traits.ReturnType!(__traits(getMember, T, member)) - } ~ member ~ q{(std.traits.Parameters!(__traits(getMember, T, member)) params) - { - SerializationBuffer buffer; - auto i = cast(ushort) idx; - serialize(&buffer.sink, i); - serialize(&buffer.sink, __traits(getMember, T, member).mangleof); - foreach(param; params) - serialize(&buffer.sink, param); - - auto sendable = buffer.sendable; - - version(Posix) {{ - auto ret = send(connectionHandle, sendable.ptr, sendable.length, 0); - - if(ret == -1) { - throw new Exception("send returned -1, errno: " ~ to!string(errno)); - } else if(ret == 0) { - throw new Exception("Connection to addon server lost"); - } if(ret < sendable.length) - throw new Exception("Send failed to send all"); - assert(ret == sendable.length); - }} // FIXME Windows impl - - static if(!is(typeof(return) == void)) { - // there is a return value; we need to wait for it too - version(Posix) { - ubyte[3000] revBuffer; - auto ret = recv(connectionHandle, revBuffer.ptr, revBuffer.length, 0); - auto got = revBuffer[0 .. ret]; - - int dataLocation; - ubyte[] grab(int sz) { - auto dataLocation1 = dataLocation; - dataLocation += sz; - return got[dataLocation1 .. dataLocation]; - } - - typeof(return) retu; - deserialize!(typeof(return))(&grab, (a) { retu = a; }); - return retu; - } else { - // FIXME Windows impl - return typeof(return).init; - } - - } - }}); - } - - private static typeof(this) singletonInstance; - private LocalServerConnectionHandle connectionHandle; - - static typeof(this) connection() { - if(singletonInstance is null) { - singletonInstance = new typeof(this)(); - singletonInstance.connect(); - } - return singletonInstance; - } - - void connect() { - connectionHandle = openLocalServerConnection(serverPath, cmdArg); - } - - void disconnect() { - closeLocalServerConnection(connectionHandle); - } -} - -void dispatchRpcServer(Interface, Class)(Class this_, ubyte[] data, int fd) if(is(Class : Interface)) { - ushort calledIdx; - string calledFunction; - - int dataLocation; - ubyte[] grab(int sz) { - if(sz == 0) assert(0); - auto d = data[dataLocation .. dataLocation + sz]; - dataLocation += sz; - return d; - } - - again: - - deserialize!ushort(&grab, (a) { calledIdx = a; }); - deserialize!string(&grab, (a) { calledFunction = a; }); - - import std.traits; - - sw: switch(calledIdx) { - foreach(idx, memberName; __traits(derivedMembers, Interface)) - static if(__traits(isVirtualMethod, __traits(getMember, Interface, memberName))) { - case idx: - assert(calledFunction == __traits(getMember, Interface, memberName).mangleof); - - Parameters!(__traits(getMember, Interface, memberName)) params; - foreach(ref param; params) - deserialize!(typeof(param))(&grab, (a) { param = a; }); - - static if(is(ReturnType!(__traits(getMember, Interface, memberName)) == void)) { - __traits(getMember, this_, memberName)(params); - } else { - auto ret = __traits(getMember, this_, memberName)(params); - SerializationBuffer buffer; - serialize(&buffer.sink, ret); - - auto sendable = buffer.sendable; - - version(Posix) { - auto r = send(fd, sendable.ptr, sendable.length, 0); - if(r == -1) { - throw new Exception("send returned -1, errno: " ~ to!string(errno)); - } else if(r == 0) { - throw new Exception("Connection to addon client lost"); - } if(r < sendable.length) - throw new Exception("Send failed to send all"); - - } // FIXME Windows impl - } - break sw; - } - default: assert(0); - } - - if(dataLocation != data.length) - goto again; -} - - -private struct SerializationBuffer { - ubyte[2048] bufferBacking; - int bufferLocation; - void sink(scope ubyte[] data) { - bufferBacking[bufferLocation .. bufferLocation + data.length] = data[]; - bufferLocation += data.length; - } - - ubyte[] sendable() return { - return bufferBacking[0 .. bufferLocation]; - } -} - -/* - FIXME: - add a version command line arg - version data in the library - management gui as external program - - at server with event_fd for each run - use .mangleof in the at function name - - i think the at server will have to: - pipe args to the child - collect child output for logging - get child return value for logging - - on windows timers work differently. idk how to best combine with the io stuff. - - will have to have dump and restore too, so i can restart without losing stuff. -*/ - -/++ - A convenience object for talking to the [BasicDataServer] from a higher level. - See: [Cgi.getSessionObject]. - - You pass it a `Data` struct describing the data you want saved in the session. - Then, this class will generate getter and setter properties that allow access - to that data. - - Note that each load and store will be done as-accessed; it doesn't front-load - mutable data nor does it batch updates out of fear of read-modify-write race - conditions. (In fact, right now it does this for everything, but in the future, - I might batch load `immutable` members of the Data struct.) - - At some point in the future, I might also let it do different backends, like - a client-side cookie store too, but idk. - - Note that the plain-old-data members of your `Data` struct are wrapped by this - interface via a static foreach to make property functions. - - See_Also: [MockSession] -+/ -interface Session(Data) : SessionObject { - @property string sessionId() const; - - /++ - Starts a new session. Note that a session is also - implicitly started as soon as you write data to it, - so if you need to alter these parameters from their - defaults, be sure to explicitly call this BEFORE doing - any writes to session data. - - Params: - idleLifetime = How long, in seconds, the session - should remain in memory when not being read from - or written to. The default is one day. - - NOT IMPLEMENTED - - useExtendedLifetimeCookie = The session ID is always - stored in a HTTP cookie, and by default, that cookie - is discarded when the user closes their browser. - - But if you set this to true, it will use a non-perishable - cookie for the given idleLifetime. - - NOT IMPLEMENTED - +/ - void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false); - - /++ - Regenerates the session ID and updates the associated - cookie. - - This is also your chance to change immutable data - (not yet implemented). - +/ - void regenerateId(); - - /++ - Terminates this session, deleting all saved data. - +/ - void terminate(); - - /++ - Plain-old-data members of your `Data` struct are wrapped here via - the property getters and setters. - - If the member is a non-string array, it returns a magical array proxy - object which allows for atomic appends and replaces via overloaded operators. - You can slice this to get a range representing a $(B const) view of the array. - This is to protect you against read-modify-write race conditions. - +/ - static foreach(memberName; __traits(allMembers, Data)) - static if(is(typeof(__traits(getMember, Data, memberName)))) - mixin(q{ - @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout; - @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value); - }); - -} - -/++ - An implementation of [Session] that works on real cgi connections utilizing the - [BasicDataServer]. - - As opposed to a [MockSession] which is made for testing purposes. - - You will not construct one of these directly. See [Cgi.getSessionObject] instead. -+/ -class BasicDataServerSession(Data) : Session!Data { - private Cgi cgi; - private string sessionId_; - - public @property string sessionId() const { - return sessionId_; - } - - protected @property string sessionId(string s) { - return this.sessionId_ = s; - } - - private this(Cgi cgi) { - this.cgi = cgi; - if(auto ptr = "sessionId" in cgi.cookies) - sessionId = (*ptr).length ? *ptr : null; - } - - void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) { - assert(sessionId is null); - - // FIXME: what if there is a session ID cookie, but no corresponding session on the server? - - import std.random, std.conv; - sessionId = to!string(uniform(1, long.max)); - - BasicDataServer.connection.createSession(sessionId, idleLifetime); - setCookie(); - } - - protected void setCookie() { - cgi.setCookie( - "sessionId", sessionId, - 0 /* expiration */, - "/" /* path */, - null /* domain */, - true /* http only */, - cgi.https /* if the session is started on https, keep it there, otherwise, be flexible */); - } - - void regenerateId() { - if(sessionId is null) { - start(); - return; - } - import std.random, std.conv; - auto oldSessionId = sessionId; - sessionId = to!string(uniform(1, long.max)); - BasicDataServer.connection.renameSession(oldSessionId, sessionId); - setCookie(); - } - - void terminate() { - BasicDataServer.connection.destroySession(sessionId); - sessionId = null; - setCookie(); - } - - static foreach(memberName; __traits(allMembers, Data)) - static if(is(typeof(__traits(getMember, Data, memberName)))) - mixin(q{ - @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout { - if(sessionId is null) - return typeof(return).init; - - import std.traits; - auto v = BasicDataServer.connection.getSessionData(sessionId, fullyQualifiedName!Data ~ "." ~ memberName); - if(v.length == 0) - return typeof(return).init; - import std.conv; - // why this cast? to doesn't like being given an inout argument. so need to do it without that, then - // we need to return it and that needed the cast. It should be fine since we basically respect constness.. - // basically. Assuming the session is POD this should be fine. - return cast(typeof(return)) to!(typeof(__traits(getMember, Data, memberName)))(v); - } - @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value) { - if(sessionId is null) - start(); - import std.conv; - import std.traits; - BasicDataServer.connection.setSessionData(sessionId, fullyQualifiedName!Data ~ "." ~ memberName, to!string(value)); - return value; - } - }); -} - -/++ - A mock object that works like the real session, but doesn't actually interact with any actual database or http connection. - Simply stores the data in its instance members. -+/ -class MockSession(Data) : Session!Data { - pure { - @property string sessionId() const { return "mock"; } - void start(int idleLifetime = 2600 * 24, bool useExtendedLifetimeCookie = false) {} - void regenerateId() {} - void terminate() {} - - private Data store_; - - static foreach(memberName; __traits(allMembers, Data)) - static if(is(typeof(__traits(getMember, Data, memberName)))) - mixin(q{ - @property inout(typeof(__traits(getMember, Data, memberName))) } ~ memberName ~ q{ () inout { - return __traits(getMember, store_, memberName); - } - @property typeof(__traits(getMember, Data, memberName)) } ~ memberName ~ q{ (typeof(__traits(getMember, Data, memberName)) value) { - return __traits(getMember, store_, memberName) = value; - } - }); - } -} - -/++ - Direct interface to the basic data add-on server. You can - typically use [Cgi.getSessionObject] as a more convenient interface. -+/ -version(with_addon_servers_connections) -interface BasicDataServer { - /// - void createSession(string sessionId, int lifetime); - /// - void renewSession(string sessionId, int lifetime); - /// - void destroySession(string sessionId); - /// - void renameSession(string oldSessionId, string newSessionId); - - /// - void setSessionData(string sessionId, string dataKey, string dataValue); - /// - string getSessionData(string sessionId, string dataKey); - - /// - static BasicDataServerConnection connection() { - return BasicDataServerConnection.connection(); - } -} - -version(with_addon_servers_connections) -class BasicDataServerConnection : BasicDataServer { - mixin ImplementRpcClientInterface!(BasicDataServer, "/tmp/arsd_session_server", "--session-server"); -} - -version(with_addon_servers) -final class BasicDataServerImplementation : BasicDataServer, EventIoServer { - - void createSession(string sessionId, int lifetime) { - sessions[sessionId.idup] = Session(lifetime); - } - void destroySession(string sessionId) { - sessions.remove(sessionId); - } - void renewSession(string sessionId, int lifetime) { - sessions[sessionId].lifetime = lifetime; - } - void renameSession(string oldSessionId, string newSessionId) { - sessions[newSessionId.idup] = sessions[oldSessionId]; - sessions.remove(oldSessionId); - } - void setSessionData(string sessionId, string dataKey, string dataValue) { - if(sessionId !in sessions) - createSession(sessionId, 3600); // FIXME? - sessions[sessionId].values[dataKey.idup] = dataValue.idup; - } - string getSessionData(string sessionId, string dataKey) { - if(auto session = sessionId in sessions) { - if(auto data = dataKey in (*session).values) - return *data; - else - return null; // no such data - - } else { - return null; // no session - } - } - - - protected: - - struct Session { - int lifetime; - - string[string] values; - } - - Session[string] sessions; - - bool handleLocalConnectionData(IoOp* op, int receivedFd) { - auto data = op.usedBuffer; - dispatchRpcServer!BasicDataServer(this, data, op.fd); - return false; - } - - void handleLocalConnectionClose(IoOp* op) {} // doesn't really matter, this is a fairly stateless go - void handleLocalConnectionComplete(IoOp* op) {} // again, irrelevant - void wait_timeout() {} - void fileClosed(int fd) {} // stateless so irrelevant - void epoll_fd(int fd) {} -} - -/++ - See [schedule] to make one of these. You then call one of the methods here to set it up: - - --- - schedule!fn(args).at(DateTime(2019, 8, 7, 12, 00, 00)); // run the function at August 7, 2019, 12 noon UTC - schedule!fn(args).delay(6.seconds); // run it after waiting 6 seconds - schedule!fn(args).asap(); // run it in the background as soon as the event loop gets around to it - --- -+/ -version(with_addon_servers_connections) -struct ScheduledJobHelper { - private string func; - private string[] args; - private bool consumed; - - private this(string func, string[] args) { - this.func = func; - this.args = args; - } - - ~this() { - assert(consumed); - } - - /++ - Schedules the job to be run at the given time. - +/ - void at(DateTime when, immutable TimeZone timezone = UTC()) { - consumed = true; - - auto conn = ScheduledJobServerConnection.connection; - import std.file; - auto st = SysTime(when, timezone); - auto jobId = conn.scheduleJob(1, cast(int) st.toUnixTime(), thisExePath, func, args); - } - - /++ - Schedules the job to run at least after the specified delay. - +/ - void delay(Duration delay) { - consumed = true; - - auto conn = ScheduledJobServerConnection.connection; - import std.file; - auto jobId = conn.scheduleJob(0, cast(int) delay.total!"seconds", thisExePath, func, args); - } - - /++ - Runs the job in the background ASAP. - - $(NOTE It may run in a background thread. Don't segfault!) - +/ - void asap() { - consumed = true; - - auto conn = ScheduledJobServerConnection.connection; - import std.file; - auto jobId = conn.scheduleJob(0, 1, thisExePath, func, args); - } - - /+ - /++ - Schedules the job to recur on the given pattern. - +/ - void recur(string spec) { - - } - +/ -} - -/++ - First step to schedule a job on the scheduled job server. - - The scheduled job needs to be a top-level function that doesn't read any - variables from outside its arguments because it may be run in a new process, - without any context existing later. - - You MUST set details on the returned object to actually do anything! -+/ -template schedule(alias fn, T...) if(is(typeof(fn) == function)) { - /// - ScheduledJobHelper schedule(T args) { - // this isn't meant to ever be called, but instead just to - // get the compiler to type check the arguments passed for us - auto sample = delegate() { - fn(args); - }; - string[] sargs; - foreach(arg; args) - sargs ~= to!string(arg); - return ScheduledJobHelper(fn.mangleof, sargs); - } - - shared static this() { - scheduledJobHandlers[fn.mangleof] = delegate(string[] sargs) { - import std.traits; - Parameters!fn args; - foreach(idx, ref arg; args) - arg = to!(typeof(arg))(sargs[idx]); - fn(args); - }; - } -} - -/// -interface ScheduledJobServer { - /// Use the [schedule] function for a higher-level interface. - int scheduleJob(int whenIs, int when, string executable, string func, string[] args); - /// - void cancelJob(int jobId); -} - -version(with_addon_servers_connections) -class ScheduledJobServerConnection : ScheduledJobServer { - mixin ImplementRpcClientInterface!(ScheduledJobServer, "/tmp/arsd_scheduled_job_server", "--timer-server"); -} - -version(with_addon_servers) -final class ScheduledJobServerImplementation : ScheduledJobServer, EventIoServer { - // FIXME: we need to handle SIGCHLD in this somehow - // whenIs is 0 for relative, 1 for absolute - protected int scheduleJob(int whenIs, int when, string executable, string func, string[] args) { - auto nj = nextJobId; - nextJobId++; - - version(linux) { - import core.sys.linux.timerfd; - import core.sys.linux.epoll; - import core.sys.posix.unistd; - - - auto fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC); - if(fd == -1) - throw new Exception("fd timer create failed"); - - foreach(ref arg; args) - arg = arg.idup; - auto job = Job(executable.idup, func.idup, .dup(args), fd, nj); - - itimerspec value; - value.it_value.tv_sec = when; - value.it_value.tv_nsec = 0; - - value.it_interval.tv_sec = 0; - value.it_interval.tv_nsec = 0; - - if(timerfd_settime(fd, whenIs == 1 ? TFD_TIMER_ABSTIME : 0, &value, null) == -1) - throw new Exception("couldn't set fd timer"); - - auto op = allocateIoOp(fd, IoOp.Read, 16, (IoOp* op, int fd) { - jobs.remove(nj); - epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, null); - close(fd); - - - spawnProcess([job.executable, "--timed-job", job.func] ~ job.args); - - return true; - }); - scope(failure) - freeIoOp(op); - - epoll_event ev; - ev.events = EPOLLIN | EPOLLET; - ev.data.ptr = op; - if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) - throw new Exception("epoll_ctl " ~ to!string(errno)); - - jobs[nj] = job; - return nj; - } else assert(0); - } - - protected void cancelJob(int jobId) { - version(linux) { - auto job = jobId in jobs; - if(job is null) - return; - - jobs.remove(jobId); - - version(linux) { - import core.sys.linux.timerfd; - import core.sys.linux.epoll; - import core.sys.posix.unistd; - epoll_ctl(epoll_fd, EPOLL_CTL_DEL, job.timerfd, null); - close(job.timerfd); - } - } - jobs.remove(jobId); - } - - int nextJobId = 1; - static struct Job { - string executable; - string func; - string[] args; - int timerfd; - int id; - } - Job[int] jobs; - - - // event io server methods below - - bool handleLocalConnectionData(IoOp* op, int receivedFd) { - auto data = op.usedBuffer; - dispatchRpcServer!ScheduledJobServer(this, data, op.fd); - return false; - } - - void handleLocalConnectionClose(IoOp* op) {} // doesn't really matter, this is a fairly stateless go - void handleLocalConnectionComplete(IoOp* op) {} // again, irrelevant - void wait_timeout() {} - void fileClosed(int fd) {} // stateless so irrelevant - - int epoll_fd_; - void epoll_fd(int fd) {this.epoll_fd_ = fd; } - int epoll_fd() { return epoll_fd_; } -} - -/// -version(with_addon_servers_connections) -interface EventSourceServer { - /++ - sends this cgi request to the event server so it will be fed events. You should not do anything else with the cgi object after this. - - $(WARNING This API is extremely unstable. I might change it or remove it without notice.) - - See_Also: - [sendEvent] - +/ - public static void adoptConnection(Cgi cgi, in char[] eventUrl) { - /* - If lastEventId is missing or empty, you just get new events as they come. - - If it is set from something else, it sends all since then (that are still alive) - down the pipe immediately. - - The reason it can come from the header is that's what the standard defines for - browser reconnects. The reason it can come from a query string is just convenience - in catching up in a user-defined manner. - - The reason the header overrides the query string is if the browser tries to reconnect, - it will send the header AND the query (it reconnects to the same url), so we just - want to do the restart thing. - - Note that if you ask for "0" as the lastEventId, it will get ALL still living events. - */ - string lastEventId = cgi.lastEventId; - if(lastEventId.length == 0 && "lastEventId" in cgi.get) - lastEventId = cgi.get["lastEventId"]; - - cgi.setResponseContentType("text/event-stream"); - cgi.write(":\n", false); // to initialize the chunking and send headers before keeping the fd for later - cgi.flush(); - - cgi.closed = true; - auto s = openLocalServerConnection("/tmp/arsd_cgi_event_server", "--event-server"); - scope(exit) - closeLocalServerConnection(s); - - version(fastcgi) - throw new Exception("sending fcgi connections not supported"); - else { - auto fd = cgi.getOutputFileHandle(); - if(isInvalidHandle(fd)) - throw new Exception("bad fd from cgi!"); - - EventSourceServerImplementation.SendableEventConnection sec; - sec.populate(cgi.responseChunked, eventUrl, lastEventId); - - version(Posix) { - auto res = write_fd(s, cast(void*) &sec, sec.sizeof, fd); - assert(res == sec.sizeof); - } else version(Windows) { - // FIXME - } - } - } - - /++ - Sends an event to the event server, starting it if necessary. The event server will distribute it to any listening clients, and store it for `lifetime` seconds for any later listening clients to catch up later. - - $(WARNING This API is extremely unstable. I might change it or remove it without notice.) - - Params: - url = A string identifying this event "bucket". Listening clients must also connect to this same string. I called it `url` because I envision it being just passed as the url of the request. - event = the event type string, which is used in the Javascript addEventListener API on EventSource - data = the event data. Available in JS as `event.data`. - lifetime = the amount of time to keep this event for replaying on the event server. - - See_Also: - [sendEventToEventServer] - +/ - public static void sendEvent(string url, string event, string data, int lifetime) { - auto s = openLocalServerConnection("/tmp/arsd_cgi_event_server", "--event-server"); - scope(exit) - closeLocalServerConnection(s); - - EventSourceServerImplementation.SendableEvent sev; - sev.populate(url, event, data, lifetime); - - version(Posix) { - auto ret = send(s, &sev, sev.sizeof, 0); - assert(ret == sev.sizeof); - } else version(Windows) { - // FIXME - } - } - - /++ - Messages sent to `url` will also be sent to anyone listening on `forwardUrl`. - - See_Also: [disconnect] - +/ - void connect(string url, string forwardUrl); - - /++ - Disconnects `forwardUrl` from `url` - - See_Also: [connect] - +/ - void disconnect(string url, string forwardUrl); -} - -/// -version(with_addon_servers) -final class EventSourceServerImplementation : EventSourceServer, EventIoServer { - - protected: - - void connect(string url, string forwardUrl) { - pipes[url] ~= forwardUrl; - } - void disconnect(string url, string forwardUrl) { - auto t = url in pipes; - if(t is null) - return; - foreach(idx, n; (*t)) - if(n == forwardUrl) { - (*t)[idx] = (*t)[$-1]; - (*t) = (*t)[0 .. $-1]; - break; - } - } - - bool handleLocalConnectionData(IoOp* op, int receivedFd) { - if(receivedFd != -1) { - //writeln("GOT FD ", receivedFd, " -- ", op.usedBuffer); - - //core.sys.posix.unistd.write(receivedFd, "hello".ptr, 5); - - SendableEventConnection* got = cast(SendableEventConnection*) op.usedBuffer.ptr; - - auto url = got.url.idup; - eventConnectionsByUrl[url] ~= EventConnection(receivedFd, got.responseChunked > 0 ? true : false); - - // FIXME: catch up on past messages here - } else { - auto data = op.usedBuffer; - auto event = cast(SendableEvent*) data.ptr; - - if(event.magic == 0xdeadbeef) { - handleInputEvent(event); - - if(event.url in pipes) - foreach(pipe; pipes[event.url]) { - event.url = pipe; - handleInputEvent(event); - } - } else { - dispatchRpcServer!EventSourceServer(this, data, op.fd); - } - } - return false; - } - void handleLocalConnectionClose(IoOp* op) { - fileClosed(op.fd); - } - void handleLocalConnectionComplete(IoOp* op) {} - - void wait_timeout() { - // just keeping alive - foreach(url, connections; eventConnectionsByUrl) - foreach(connection; connections) - if(connection.needsChunking) - nonBlockingWrite(this, connection.fd, "1b\r\nevent: keepalive\ndata: ok\n\n\r\n"); - else - nonBlockingWrite(this, connection.fd, "event: keepalive\ndata: ok\n\n\r\n"); - } - - void fileClosed(int fd) { - outer: foreach(url, ref connections; eventConnectionsByUrl) { - foreach(idx, conn; connections) { - if(fd == conn.fd) { - connections[idx] = connections[$-1]; - connections = connections[0 .. $ - 1]; - continue outer; - } - } - } - } - - void epoll_fd(int fd) {} - - - private: - - - struct SendableEventConnection { - ubyte responseChunked; - - int urlLength; - char[256] urlBuffer = 0; - - int lastEventIdLength; - char[32] lastEventIdBuffer = 0; - - char[] url() return { - return urlBuffer[0 .. urlLength]; - } - void url(in char[] u) { - urlBuffer[0 .. u.length] = u[]; - urlLength = cast(int) u.length; - } - char[] lastEventId() return { - return lastEventIdBuffer[0 .. lastEventIdLength]; - } - void populate(bool responseChunked, in char[] url, in char[] lastEventId) - in { - assert(url.length < this.urlBuffer.length); - assert(lastEventId.length < this.lastEventIdBuffer.length); - } - do { - this.responseChunked = responseChunked ? 1 : 0; - this.urlLength = cast(int) url.length; - this.lastEventIdLength = cast(int) lastEventId.length; - - this.urlBuffer[0 .. url.length] = url[]; - this.lastEventIdBuffer[0 .. lastEventId.length] = lastEventId[]; - } - } - - struct SendableEvent { - int magic = 0xdeadbeef; - int urlLength; - char[256] urlBuffer = 0; - int typeLength; - char[32] typeBuffer = 0; - int messageLength; - char[2048 * 4] messageBuffer = 0; // this is an arbitrary limit, it needs to fit comfortably in stack (including in a fiber) and be a single send on the kernel side cuz of the impl... i think this is ok for a unix socket. - int _lifetime; - - char[] message() return { - return messageBuffer[0 .. messageLength]; - } - char[] type() return { - return typeBuffer[0 .. typeLength]; - } - char[] url() return { - return urlBuffer[0 .. urlLength]; - } - void url(in char[] u) { - urlBuffer[0 .. u.length] = u[]; - urlLength = cast(int) u.length; - } - int lifetime() { - return _lifetime; - } - - /// - void populate(string url, string type, string message, int lifetime) - in { - assert(url.length < this.urlBuffer.length); - assert(type.length < this.typeBuffer.length); - assert(message.length < this.messageBuffer.length); - } - do { - this.urlLength = cast(int) url.length; - this.typeLength = cast(int) type.length; - this.messageLength = cast(int) message.length; - this._lifetime = lifetime; - - this.urlBuffer[0 .. url.length] = url[]; - this.typeBuffer[0 .. type.length] = type[]; - this.messageBuffer[0 .. message.length] = message[]; - } - } - - struct EventConnection { - int fd; - bool needsChunking; - } - - private EventConnection[][string] eventConnectionsByUrl; - private string[][string] pipes; - - private void handleInputEvent(scope SendableEvent* event) { - static int eventId; - - static struct StoredEvent { - int id; - string type; - string message; - int lifetimeRemaining; - } - - StoredEvent[][string] byUrl; - - int thisId = ++eventId; - - if(event.lifetime) - byUrl[event.url.idup] ~= StoredEvent(thisId, event.type.idup, event.message.idup, event.lifetime); - - auto connectionsPtr = event.url in eventConnectionsByUrl; - EventConnection[] connections; - if(connectionsPtr is null) - return; - else - connections = *connectionsPtr; - - char[4096] buffer; - char[] formattedMessage; - - void append(const char[] a) { - // the 6's here are to leave room for a HTTP chunk header, if it proves necessary - buffer[6 + formattedMessage.length .. 6 + formattedMessage.length + a.length] = a[]; - formattedMessage = buffer[6 .. 6 + formattedMessage.length + a.length]; - } - - import std.algorithm.iteration; - - if(connections.length) { - append("id: "); - append(to!string(thisId)); - append("\n"); - - append("event: "); - append(event.type); - append("\n"); - - foreach(line; event.message.splitter("\n")) { - append("data: "); - append(line); - append("\n"); - } - - append("\n"); - } - - // chunk it for HTTP! - auto len = toHex(formattedMessage.length); - buffer[4 .. 6] = "\r\n"[]; - buffer[4 - len.length .. 4] = len[]; - buffer[6 + formattedMessage.length] = '\r'; - buffer[6 + formattedMessage.length + 1] = '\n'; - - auto chunkedMessage = buffer[4 - len.length .. 6 + formattedMessage.length +2]; - // done - - // FIXME: send back requests when needed - // FIXME: send a single ":\n" every 15 seconds to keep alive - - foreach(connection; connections) { - if(connection.needsChunking) { - nonBlockingWrite(this, connection.fd, chunkedMessage); - } else { - nonBlockingWrite(this, connection.fd, formattedMessage); - } - } - } -} - -void runAddonServer(EIS)(string localListenerName, EIS eis) if(is(EIS : EventIoServer)) { - version(Posix) { - - import core.sys.posix.unistd; - import core.sys.posix.fcntl; - import core.sys.posix.sys.un; - - import core.sys.posix.signal; - signal(SIGPIPE, SIG_IGN); - - static extern(C) void sigchldhandler(int) { - int status; - import w = core.sys.posix.sys.wait; - w.wait(&status); - } - signal(SIGCHLD, &sigchldhandler); - - int sock = socket(AF_UNIX, SOCK_STREAM, 0); - if(sock == -1) - throw new Exception("socket " ~ to!string(errno)); - - scope(failure) - close(sock); - - cloexec(sock); - - // add-on server processes are assumed to be local, and thus will - // use unix domain sockets. Besides, I want to pass sockets to them, - // so it basically must be local (except for the session server, but meh). - sockaddr_un addr; - addr.sun_family = AF_UNIX; - version(linux) { - // on linux, we will use the abstract namespace - addr.sun_path[0] = 0; - addr.sun_path[1 .. localListenerName.length + 1] = cast(typeof(addr.sun_path[])) localListenerName[]; - } else { - // but otherwise, just use a file cuz we must. - addr.sun_path[0 .. localListenerName.length] = cast(typeof(addr.sun_path[])) localListenerName[]; - } - - if(bind(sock, cast(sockaddr*) &addr, addr.sizeof) == -1) - throw new Exception("bind " ~ to!string(errno)); - - if(listen(sock, 128) == -1) - throw new Exception("listen " ~ to!string(errno)); - - makeNonBlocking(sock); - - version(linux) { - import core.sys.linux.epoll; - auto epoll_fd = epoll_create1(EPOLL_CLOEXEC); - if(epoll_fd == -1) - throw new Exception("epoll_create1 " ~ to!string(errno)); - scope(failure) - close(epoll_fd); - } else { - import core.sys.posix.poll; - } - - version(linux) - eis.epoll_fd = epoll_fd; - - auto acceptOp = allocateIoOp(sock, IoOp.Read, 0, null); - scope(exit) - freeIoOp(acceptOp); - - version(linux) { - epoll_event ev; - ev.events = EPOLLIN | EPOLLET; - ev.data.ptr = acceptOp; - if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &ev) == -1) - throw new Exception("epoll_ctl " ~ to!string(errno)); - - epoll_event[64] events; - } else { - pollfd[] pollfds; - IoOp*[int] ioops; - pollfds ~= pollfd(sock, POLLIN); - ioops[sock] = acceptOp; - } - - import core.time : MonoTime, seconds; - - MonoTime timeout = MonoTime.currTime + 15.seconds; - - while(true) { - - // FIXME: it should actually do a timerfd that runs on any thing that hasn't been run recently - - int timeout_milliseconds = 0; // -1; // infinite - - timeout_milliseconds = cast(int) (timeout - MonoTime.currTime).total!"msecs"; - if(timeout_milliseconds < 0) - timeout_milliseconds = 0; - - //writeln("waiting for ", name); - - version(linux) { - auto nfds = epoll_wait(epoll_fd, events.ptr, events.length, timeout_milliseconds); - if(nfds == -1) { - if(errno == EINTR) - continue; - throw new Exception("epoll_wait " ~ to!string(errno)); - } - } else { - int nfds = poll(pollfds.ptr, cast(int) pollfds.length, timeout_milliseconds); - size_t lastIdx = 0; - } - - if(nfds == 0) { - eis.wait_timeout(); - timeout += 15.seconds; - } - - foreach(idx; 0 .. nfds) { - version(linux) { - auto flags = events[idx].events; - auto ioop = cast(IoOp*) events[idx].data.ptr; - } else { - IoOp* ioop; - foreach(tidx, thing; pollfds[lastIdx .. $]) { - if(thing.revents) { - ioop = ioops[thing.fd]; - lastIdx += tidx + 1; - break; - } - } - } - - //writeln(flags, " ", ioop.fd); - - void newConnection() { - // on edge triggering, it is important that we get it all - while(true) { - version(Android) { - auto size = cast(int) addr.sizeof; - } else { - auto size = cast(uint) addr.sizeof; - } - auto ns = accept(sock, cast(sockaddr*) &addr, &size); - if(ns == -1) { - if(errno == EAGAIN || errno == EWOULDBLOCK) { - // all done, got it all - break; - } - throw new Exception("accept " ~ to!string(errno)); - } - cloexec(ns); - - makeNonBlocking(ns); - auto niop = allocateIoOp(ns, IoOp.ReadSocketHandle, 4096 * 4, &eis.handleLocalConnectionData); - niop.closeHandler = &eis.handleLocalConnectionClose; - niop.completeHandler = &eis.handleLocalConnectionComplete; - scope(failure) freeIoOp(niop); - - version(linux) { - epoll_event nev; - nev.events = EPOLLIN | EPOLLET; - nev.data.ptr = niop; - if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, ns, &nev) == -1) - throw new Exception("epoll_ctl " ~ to!string(errno)); - } else { - bool found = false; - foreach(ref pfd; pollfds) { - if(pfd.fd < 0) { - pfd.fd = ns; - found = true; - } - } - if(!found) - pollfds ~= pollfd(ns, POLLIN); - ioops[ns] = niop; - } - } - } - - bool newConnectionCondition() { - version(linux) - return ioop.fd == sock && (flags & EPOLLIN); - else - return pollfds[idx].fd == sock && (pollfds[idx].revents & POLLIN); - } - - if(newConnectionCondition()) { - newConnection(); - } else if(ioop.operation == IoOp.ReadSocketHandle) { - while(true) { - int in_fd; - auto got = read_fd(ioop.fd, ioop.allocatedBuffer.ptr, ioop.allocatedBuffer.length, &in_fd); - if(got == -1) { - if(errno == EAGAIN || errno == EWOULDBLOCK) { - // all done, got it all - if(ioop.completeHandler) - ioop.completeHandler(ioop); - break; - } - throw new Exception("recv " ~ to!string(errno)); - } - - if(got == 0) { - if(ioop.closeHandler) { - ioop.closeHandler(ioop); - version(linux) {} // nothing needed - else { - foreach(ref pfd; pollfds) { - if(pfd.fd == ioop.fd) - pfd.fd = -1; - } - } - } - close(ioop.fd); - freeIoOp(ioop); - break; - } - - ioop.bufferLengthUsed = cast(int) got; - ioop.handler(ioop, in_fd); - } - } else if(ioop.operation == IoOp.Read) { - while(true) { - auto got = read(ioop.fd, ioop.allocatedBuffer.ptr, ioop.allocatedBuffer.length); - if(got == -1) { - if(errno == EAGAIN || errno == EWOULDBLOCK) { - // all done, got it all - if(ioop.completeHandler) - ioop.completeHandler(ioop); - break; - } - throw new Exception("recv " ~ to!string(ioop.fd) ~ " errno " ~ to!string(errno)); - } - - if(got == 0) { - if(ioop.closeHandler) - ioop.closeHandler(ioop); - close(ioop.fd); - freeIoOp(ioop); - break; - } - - ioop.bufferLengthUsed = cast(int) got; - if(ioop.handler(ioop, ioop.fd)) { - close(ioop.fd); - freeIoOp(ioop); - break; - } - } - } - - // EPOLLHUP? - } - } - } else version(Windows) { - - // set up a named pipe - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx - // https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-wsaduplicatesocketw - // https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-getnamedpipeserverprocessid - - } else static assert(0); -} - - -version(with_sendfd) -// copied from the web and ported from C -// see https://stackoverflow.com/questions/2358684/can-i-share-a-file-descriptor-to-another-process-on-linux-or-are-they-local-to-t -ssize_t write_fd(int fd, void *ptr, size_t nbytes, int sendfd) { - msghdr msg; - iovec[1] iov; - - version(OSX) { - //msg.msg_accrights = cast(cattr_t) &sendfd; - //msg.msg_accrightslen = int.sizeof; - } else version(Android) { - } else { - union ControlUnion { - cmsghdr cm; - char[CMSG_SPACE(int.sizeof)] control; - } - - ControlUnion control_un; - cmsghdr* cmptr; - - msg.msg_control = control_un.control.ptr; - msg.msg_controllen = control_un.control.length; - - cmptr = CMSG_FIRSTHDR(&msg); - cmptr.cmsg_len = CMSG_LEN(int.sizeof); - cmptr.cmsg_level = SOL_SOCKET; - cmptr.cmsg_type = SCM_RIGHTS; - *(cast(int *) CMSG_DATA(cmptr)) = sendfd; - } - - msg.msg_name = null; - msg.msg_namelen = 0; - - iov[0].iov_base = ptr; - iov[0].iov_len = nbytes; - msg.msg_iov = iov.ptr; - msg.msg_iovlen = 1; - - return sendmsg(fd, &msg, 0); -} - -version(with_sendfd) -// copied from the web and ported from C -ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd) { - msghdr msg; - iovec[1] iov; - ssize_t n; - int newfd; - - version(OSX) { - //msg.msg_accrights = cast(cattr_t) recvfd; - //msg.msg_accrightslen = int.sizeof; - } else version(Android) { - } else { - union ControlUnion { - cmsghdr cm; - char[CMSG_SPACE(int.sizeof)] control; - } - ControlUnion control_un; - cmsghdr* cmptr; - - msg.msg_control = control_un.control.ptr; - msg.msg_controllen = control_un.control.length; - } - - msg.msg_name = null; - msg.msg_namelen = 0; - - iov[0].iov_base = ptr; - iov[0].iov_len = nbytes; - msg.msg_iov = iov.ptr; - msg.msg_iovlen = 1; - - if ( (n = recvmsg(fd, &msg, 0)) <= 0) - return n; - - version(OSX) { - //if(msg.msg_accrightslen != int.sizeof) - //*recvfd = -1; - } else version(Android) { - } else { - if ( (cmptr = CMSG_FIRSTHDR(&msg)) != null && - cmptr.cmsg_len == CMSG_LEN(int.sizeof)) { - if (cmptr.cmsg_level != SOL_SOCKET) - throw new Exception("control level != SOL_SOCKET"); - if (cmptr.cmsg_type != SCM_RIGHTS) - throw new Exception("control type != SCM_RIGHTS"); - *recvfd = *(cast(int *) CMSG_DATA(cmptr)); - } else - *recvfd = -1; /* descriptor was not passed */ - } - - return n; -} -/* end read_fd */ - - -/* - Event source stuff - - The api is: - - sendEvent(string url, string type, string data, int timeout = 60*10); - - attachEventListener(string url, int fd, lastId) - - - It just sends to all attached listeners, and stores it until the timeout - for replaying via lastEventId. -*/ - -/* - Session process stuff - - it stores it all. the cgi object has a session object that can grab it - - session may be done in the same process if possible, there is a version - switch to choose if you want to override. -*/ - -struct DispatcherDefinition(alias dispatchHandler, DispatcherDetails = typeof(null)) {// if(is(typeof(dispatchHandler("str", Cgi.init, void) == bool))) { // bool delegate(string urlPrefix, Cgi cgi) dispatchHandler; - alias handler = dispatchHandler; - string urlPrefix; - bool rejectFurther; - immutable(DispatcherDetails) details; -} - -private string urlify(string name) pure { - return beautify(name, '-', true); -} - -private string beautify(string name, char space = ' ', bool allLowerCase = false) pure { - if(name == "id") - return allLowerCase ? name : "ID"; - - char[160] buffer; - int bufferIndex = 0; - bool shouldCap = true; - bool shouldSpace; - bool lastWasCap; - foreach(idx, char ch; name) { - if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important - - if((ch >= 'A' && ch <= 'Z') || ch == '_') { - if(lastWasCap) { - // two caps in a row, don't change. Prolly acronym. - } else { - if(idx) - shouldSpace = true; // new word, add space - } - - lastWasCap = true; - } else { - lastWasCap = false; - } - - if(shouldSpace) { - buffer[bufferIndex++] = space; - if(bufferIndex == buffer.length) return name; // out of space, just give up, not that important - shouldSpace = false; - } - if(shouldCap) { - if(ch >= 'a' && ch <= 'z') - ch -= 32; - shouldCap = false; - } - if(allLowerCase && ch >= 'A' && ch <= 'Z') - ch += 32; - buffer[bufferIndex++] = ch; - } - return buffer[0 .. bufferIndex].idup; -} - -/* -string urlFor(alias func)() { - return __traits(identifier, func); -} -*/ - -/++ - UDA: The name displayed to the user in auto-generated HTML. - - Default is `beautify(identifier)`. -+/ -struct DisplayName { - string name; -} - -/++ - UDA: The name used in the URL or web parameter. - - Default is `urlify(identifier)` for functions and `identifier` for parameters and data members. -+/ -struct UrlName { - string name; -} - -/++ - UDA: default format to respond for this method -+/ -struct DefaultFormat { string value; } - -class MissingArgumentException : Exception { - string functionName; - string argumentName; - string argumentType; - - this(string functionName, string argumentName, string argumentType, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { - this.functionName = functionName; - this.argumentName = argumentName; - this.argumentType = argumentType; - - super("Missing Argument: " ~ this.argumentName, file, line, next); - } -} - -/++ - You can throw this from an api handler to indicate a 404 response. This is done by the presentExceptionAsHtml function in the presenter. - - History: - Added December 15, 2021 (dub v10.5) -+/ -class ResourceNotFoundException : Exception { - string resourceType; - string resourceId; - - this(string resourceType, string resourceId, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { - this.resourceType = resourceType; - this.resourceId = resourceId; - - super("Resource not found: " ~ resourceType ~ " " ~ resourceId, file, line, next); - } - -} - -/++ - This can be attached to any constructor or function called from the cgi system. - - If it is present, the function argument can NOT be set from web params, but instead - is set to the return value of the given `func`. - - If `func` can take a parameter of type [Cgi], it will be passed the one representing - the current request. Otherwise, it must take zero arguments. - - Any params in your function of type `Cgi` are automatically assumed to take the cgi object - for the connection. Any of type [Session] (with an argument) is also assumed to come from - the cgi object. - - const arguments are also supported. -+/ -struct ifCalledFromWeb(alias func) {} - -// it only looks at query params for GET requests, the rest must be in the body for a function argument. -auto callFromCgi(alias method, T)(T dg, Cgi cgi) { - - // FIXME: any array of structs should also be settable or gettable from csv as well. - - // FIXME: think more about checkboxes and bools. - - import std.traits; - - Parameters!method params; - alias idents = ParameterIdentifierTuple!method; - alias defaults = ParameterDefaults!method; - - const(string)[] names; - const(string)[] values; - - // first, check for missing arguments and initialize to defaults if necessary - - static if(is(typeof(method) P == __parameters)) - foreach(idx, param; P) {{ - // see: mustNotBeSetFromWebParams - static if(is(param : Cgi)) { - static assert(!is(param == immutable)); - cast() params[idx] = cgi; - } else static if(is(param == Session!D, D)) { - static assert(!is(param == immutable)); - cast() params[idx] = cgi.getSessionObject!D(); - } else { - bool populated; - foreach(uda; __traits(getAttributes, P[idx .. idx + 1])) { - static if(is(uda == ifCalledFromWeb!func, alias func)) { - static if(is(typeof(func(cgi)))) - params[idx] = func(cgi); - else - params[idx] = func(); - - populated = true; - } - } - - if(!populated) { - static if(__traits(compiles, { params[idx] = param.getAutomaticallyForCgi(cgi); } )) { - params[idx] = param.getAutomaticallyForCgi(cgi); - populated = true; - } - } - - if(!populated) { - auto ident = idents[idx]; - if(cgi.requestMethod == Cgi.RequestMethod.GET) { - if(ident !in cgi.get) { - static if(is(defaults[idx] == void)) { - static if(is(param == bool)) - params[idx] = false; - else - throw new MissingArgumentException(__traits(identifier, method), ident, param.stringof); - } else - params[idx] = defaults[idx]; - } - } else { - if(ident !in cgi.post) { - static if(is(defaults[idx] == void)) { - static if(is(param == bool)) - params[idx] = false; - else - throw new MissingArgumentException(__traits(identifier, method), ident, param.stringof); - } else - params[idx] = defaults[idx]; - } - } - } - } - }} - - // second, parse the arguments in order to build up arrays, etc. - - static bool setVariable(T)(string name, string paramName, T* what, string value) { - static if(is(T == struct)) { - if(name == paramName) { - *what = T.init; - return true; - } else { - // could be a child. gonna allow either obj.field OR obj[field] - - string afterName; - - if(name[paramName.length] == '[') { - int count = 1; - auto idx = paramName.length + 1; - while(idx < name.length && count > 0) { - if(name[idx] == '[') - count++; - else if(name[idx] == ']') { - count--; - if(count == 0) break; - } - idx++; - } - - if(idx == name.length) - return false; // malformed - - auto insideBrackets = name[paramName.length + 1 .. idx]; - afterName = name[idx + 1 .. $]; - - name = name[0 .. paramName.length]; - - paramName = insideBrackets; - - } else if(name[paramName.length] == '.') { - paramName = name[paramName.length + 1 .. $]; - name = paramName; - int p = 0; - foreach(ch; paramName) { - if(ch == '.' || ch == '[') - break; - p++; - } - - afterName = paramName[p .. $]; - paramName = paramName[0 .. p]; - } else { - return false; - } - - if(paramName.length) - // set the child member - switch(paramName) { - foreach(idx, memberName; __traits(allMembers, T)) - static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { - // data member! - case memberName: - return setVariable(name ~ afterName, paramName, &(__traits(getMember, *what, memberName)), value); - } - default: - // ok, not a member - } - } - - return false; - } else static if(is(T == enum)) { - *what = to!T(value); - return true; - } else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) { - *what = to!T(value); - return true; - } else static if(is(T == bool)) { - *what = value == "1" || value == "yes" || value == "t" || value == "true" || value == "on"; - return true; - } else static if(is(T == K[], K)) { - K tmp; - if(name == paramName) { - // direct - set and append - if(setVariable(name, paramName, &tmp, value)) { - (*what) ~= tmp; - return true; - } else { - return false; - } - } else { - // child, append to last element - // FIXME: what about range violations??? - auto ptr = &(*what)[(*what).length - 1]; - return setVariable(name, paramName, ptr, value); - - } - } else static if(is(T == V[K], K, V)) { - // assoc array, name[key] is valid - if(name == paramName) { - // no action necessary - return true; - } else if(name[paramName.length] == '[') { - int count = 1; - auto idx = paramName.length + 1; - while(idx < name.length && count > 0) { - if(name[idx] == '[') - count++; - else if(name[idx] == ']') { - count--; - if(count == 0) break; - } - idx++; - } - if(idx == name.length) - return false; // malformed - - auto insideBrackets = name[paramName.length + 1 .. idx]; - auto afterName = name[idx + 1 .. $]; - - auto k = to!K(insideBrackets); - V v; - if(auto ptr = k in *what) - v = *ptr; - - name = name[0 .. paramName.length]; - //writeln(name, afterName, " ", paramName); - - auto ret = setVariable(name ~ afterName, paramName, &v, value); - if(ret) { - (*what)[k] = v; - return true; - } - } - - return false; - } else { - static assert(0, "unsupported type for cgi call " ~ T.stringof); - } - - //return false; - } - - void setArgument(string name, string value) { - int p; - foreach(ch; name) { - if(ch == '.' || ch == '[') - break; - p++; - } - - auto paramName = name[0 .. p]; - - sw: switch(paramName) { - static if(is(typeof(method) P == __parameters)) - foreach(idx, param; P) { - static if(mustNotBeSetFromWebParams!(P[idx], __traits(getAttributes, P[idx .. idx + 1]))) { - // cannot be set from the outside - } else { - case idents[idx]: - static if(is(param == Cgi.UploadedFile)) { - params[idx] = cgi.files[name]; - } else { - setVariable(name, paramName, ¶ms[idx], value); - } - break sw; - } - } - default: - // ignore; not relevant argument - } - } - - if(cgi.requestMethod == Cgi.RequestMethod.GET) { - names = cgi.allGetNamesInOrder; - values = cgi.allGetValuesInOrder; - } else { - names = cgi.allPostNamesInOrder; - values = cgi.allPostValuesInOrder; - } - - foreach(idx, name; names) { - setArgument(name, values[idx]); - } - - static if(is(ReturnType!method == void)) { - typeof(null) ret; - dg(params); - } else { - auto ret = dg(params); - } - - // FIXME: format return values - // options are: json, html, csv. - // also may need to wrap in envelope format: none, html, or json. - return ret; -} - -private bool mustNotBeSetFromWebParams(T, attrs...)() { - static if(is(T : const(Cgi))) { - return true; - } else static if(is(T : const(Session!D), D)) { - return true; - } else static if(__traits(compiles, T.getAutomaticallyForCgi(Cgi.init))) { - return true; - } else { - foreach(uda; attrs) - static if(is(uda == ifCalledFromWeb!func, alias func)) - return true; - return false; - } -} - -private bool hasIfCalledFromWeb(attrs...)() { - foreach(uda; attrs) - static if(is(uda == ifCalledFromWeb!func, alias func)) - return true; - return false; -} - -/++ - Implies POST path for the thing itself, then GET will get the automatic form. - - The given customizer, if present, will be called as a filter on the Form object. - - History: - Added December 27, 2020 -+/ -template AutomaticForm(alias customizer) { } - -/++ - This is meant to be returned by a function that takes a form POST submission. You - want to set the url of the new resource it created, which is set as the http - Location header for a "201 Created" result, and you can also set a separate - destination for browser users, which it sets via a "Refresh" header. - - The `resourceRepresentation` should generally be the thing you just created, and - it will be the body of the http response when formatted through the presenter. - The exact thing is up to you - it could just return an id, or the whole object, or - perhaps a partial object. - - Examples: - --- - class Test : WebObject { - @(Cgi.RequestMethod.POST) - CreatedResource!int makeThing(string value) { - return CreatedResource!int(value.to!int, "/resources/id"); - } - } - --- - - History: - Added December 18, 2021 -+/ -struct CreatedResource(T) { - static if(!is(T == void)) - T resourceRepresentation; - string resourceUrl; - string refreshUrl; -} - -/+ -/++ - This can be attached as a UDA to a handler to add a http Refresh header on a - successful run. (It will not be attached if the function throws an exception.) - This will refresh the browser the given number of seconds after the page loads, - to the url returned by `urlFunc`, which can be either a static function or a - member method of the current handler object. - - You might use this for a POST handler that is normally used from ajax, but you - want it to degrade gracefully to a temporarily flashed message before reloading - the main page. - - History: - Added December 18, 2021 -+/ -struct Refresh(alias urlFunc) { - int waitInSeconds; - - string url() { - static if(__traits(isStaticFunction, urlFunc)) - return urlFunc(); - else static if(is(urlFunc : string)) - return urlFunc; - } -} -+/ - -/+ -/++ - Sets a filter to be run before - - A before function can do validations of params and log and stop the function from running. -+/ -template Before(alias b) {} -template After(alias b) {} -+/ - -/+ - Argument conversions: for the most part, it is to!Thing(string). - - But arrays and structs are a bit different. Arrays come from the cgi array. Thus - they are passed - - arr=foo&arr=bar <-- notice the same name. - - Structs are first declared with an empty thing, then have their members set individually, - with dot notation. The members are not required, just the initial declaration. - - struct Foo { - int a; - string b; - } - void test(Foo foo){} - - foo&foo.a=5&foo.b=str <-- the first foo declares the arg, the others set the members - - Arrays of structs use this declaration. - - void test(Foo[] foo) {} - - foo&foo.a=5&foo.b=bar&foo&foo.a=9 - - You can use a hidden input field in HTML forms to achieve this. The value of the naked name - declaration is ignored. - - Mind that order matters! The declaration MUST come first in the string. - - Arrays of struct members follow this rule recursively. - - struct Foo { - int[] a; - } - - foo&foo.a=1&foo.a=2&foo&foo.a=1 - - - Associative arrays are formatted with brackets, after a declaration, like structs: - - foo&foo[key]=value&foo[other_key]=value - - - Note: for maximum compatibility with outside code, keep your types simple. Some libraries - do not support the strict ordering requirements to work with these struct protocols. - - FIXME: also perhaps accept application/json to better work with outside trash. - - - Return values are also auto-formatted according to user-requested type: - for json, it loops over and converts. - for html, basic types are strings. Arrays are
    . Structs are
    . Arrays of structs are tables! -+/ - -/++ - A web presenter is responsible for rendering things to HTML to be usable - in a web browser. - - They are passed as template arguments to the base classes of [WebObject] - - Responsible for displaying stuff as HTML. You can put this into your own aggregate - and override it. Use forwarding and specialization to customize it. - - When you inherit from it, pass your own class as the CRTP argument. This lets the base - class templates and your overridden templates work with each other. - - --- - class MyPresenter : WebPresenter!(MyPresenter) { - @Override - void presentSuccessfulReturnAsHtml(T : CustomType)(Cgi cgi, T ret, typeof(null) meta) { - // present the CustomType - } - @Override - void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, typeof(null) meta) { - // handle everything else via the super class, which will call - // back to your class when appropriate - super.presentSuccessfulReturnAsHtml(cgi, ret); - } - } - --- - - The meta argument in there can be overridden by your own facility. - -+/ -class WebPresenter(CRTP) { - - /// A UDA version of the built-in `override`, to be used for static template polymorphism - /// If you override a plain method, use `override`. If a template, use `@Override`. - enum Override; - - string script() { - return ` - `; - } - - string style() { - return ` - :root { - --mild-border: #ccc; - --middle-border: #999; - --accent-color: #f2f2f2; - --sidebar-color: #fefefe; - } - ` ~ genericFormStyling() ~ genericSiteStyling(); - } - - string genericFormStyling() { - return -q"css - table.automatic-data-display { - border-collapse: collapse; - border: solid 1px var(--mild-border); - } - - table.automatic-data-display td { - vertical-align: top; - border: solid 1px var(--mild-border); - padding: 2px 4px; - } - - table.automatic-data-display th { - border: solid 1px var(--mild-border); - border-bottom: solid 1px var(--middle-border); - padding: 2px 4px; - } - - ol.automatic-data-display { - margin: 0px; - list-style-position: inside; - padding: 0px; - } - - dl.automatic-data-display { - - } - - .automatic-form { - max-width: 600px; - } - - .form-field { - margin: 0.5em; - padding-left: 0.5em; - } - - .label-text { - display: block; - font-weight: bold; - margin-left: -0.5em; - } - - .submit-button-holder { - padding-left: 2em; - } - - .add-array-button { - - } -css"; - } - - string genericSiteStyling() { - return -q"css - * { box-sizing: border-box; } - html, body { margin: 0px; } - body { - font-family: sans-serif; - } - header { - background: var(--accent-color); - height: 64px; - } - footer { - background: var(--accent-color); - height: 64px; - } - #site-container { - display: flex; - } - main { - flex: 1 1 auto; - order: 2; - min-height: calc(100vh - 64px - 64px); - padding: 4px; - padding-left: 1em; - } - #sidebar { - flex: 0 0 16em; - order: 1; - background: var(--sidebar-color); - } -css"; - } - - import arsd.dom; - Element htmlContainer() { - auto document = new Document(q"html - - - - - - D Application - - - -
    -
    -
    - -
    -
    - - - -html", true, true); - - return document.requireSelector("main"); - } - - /// Renders a response as an HTTP error - void renderBasicError(Cgi cgi, int httpErrorCode) { - cgi.setResponseStatus(getHttpCodeText(httpErrorCode)); - auto c = htmlContainer(); - c.innerText = getHttpCodeText(httpErrorCode); - cgi.setResponseContentType("text/html; charset=utf-8"); - cgi.write(c.parentDocument.toString(), true); - } - - template methodMeta(alias method) { - enum methodMeta = null; - } - - void presentSuccessfulReturn(T, Meta)(Cgi cgi, T ret, Meta meta, string format) { - switch(format) { - case "html": - (cast(CRTP) this).presentSuccessfulReturnAsHtml(cgi, ret, meta); - break; - case "json": - import arsd.jsvar; - static if(is(typeof(ret) == MultipleResponses!Types, Types...)) { - var json; - foreach(index, type; Types) { - if(ret.contains == index) - json = ret.payload[index]; - } - } else { - var json = ret; - } - var envelope = json; // var.emptyObject; - /* - envelope.success = true; - envelope.result = json; - envelope.error = null; - */ - cgi.setResponseContentType("application/json"); - cgi.write(envelope.toJson(), true); - break; - default: - cgi.setResponseStatus("406 Not Acceptable"); // not exactly but sort of. - } - } - - /// typeof(null) (which is also used to represent functions returning `void`) do nothing - /// in the default presenter - allowing the function to have full low-level control over the - /// response. - void presentSuccessfulReturn(T : typeof(null), Meta)(Cgi cgi, T ret, Meta meta, string format) { - // nothing intentionally! - } - - /// Redirections are forwarded to [Cgi.setResponseLocation] - void presentSuccessfulReturn(T : Redirection, Meta)(Cgi cgi, T ret, Meta meta, string format) { - cgi.setResponseLocation(ret.to, true, getHttpCodeText(ret.code)); - } - - /// [CreatedResource]s send code 201 and will set the given urls, then present the given representation. - void presentSuccessfulReturn(T : CreatedResource!R, Meta, R)(Cgi cgi, T ret, Meta meta, string format) { - cgi.setResponseStatus(getHttpCodeText(201)); - if(ret.resourceUrl.length) - cgi.header("Location: " ~ ret.resourceUrl); - if(ret.refreshUrl.length) - cgi.header("Refresh: 0;" ~ ret.refreshUrl); - static if(!is(R == void)) - presentSuccessfulReturn(cgi, ret.resourceRepresentation, meta, format); - } - - /// Multiple responses deconstruct the algebraic type and forward to the appropriate handler at runtime - void presentSuccessfulReturn(T : MultipleResponses!Types, Meta, Types...)(Cgi cgi, T ret, Meta meta, string format) { - bool outputted = false; - foreach(index, type; Types) { - if(ret.contains == index) { - assert(!outputted); - outputted = true; - (cast(CRTP) this).presentSuccessfulReturn(cgi, ret.payload[index], meta, format); - } - } - if(!outputted) - assert(0); - } - - /++ - An instance of the [arsd.dom.FileResource] interface has its own content type; assume it is a download of some sort if the filename member is non-null of the FileResource interface. - +/ - void presentSuccessfulReturn(T : FileResource, Meta)(Cgi cgi, T ret, Meta meta, string format) { - cgi.setCache(true); // not necessarily true but meh - if(auto fn = ret.filename()) { - cgi.header("Content-Disposition: attachment; filename="~fn~";"); - } - cgi.setResponseContentType(ret.contentType); - cgi.write(ret.getData(), true); - } - - /// And the default handler for HTML will call [formatReturnValueAsHtml] and place it inside the [htmlContainer]. - void presentSuccessfulReturnAsHtml(T)(Cgi cgi, T ret, typeof(null) meta) { - auto container = this.htmlContainer(); - container.appendChild(formatReturnValueAsHtml(ret)); - cgi.write(container.parentDocument.toString(), true); - } - - /++ - - History: - Added January 23, 2023 (dub v11.0) - +/ - void presentExceptionalReturn(Meta)(Cgi cgi, Throwable t, Meta meta, string format) { - switch(format) { - case "html": - presentExceptionAsHtml(cgi, t, meta); - break; - default: - } - } - - - /++ - If you override this, you will need to cast the exception type `t` dynamically, - but can then use the template arguments here to refer back to the function. - - `func` is an alias to the method itself, and `dg` is a callable delegate to the same - method on the live object. You could, in theory, change arguments and retry, but I - provide that information mostly with the expectation that you will use them to make - useful forms or richer error messages for the user. - - History: - BREAKING CHANGE on January 23, 2023 (v11.0 ): it previously took an `alias func` and `T dg` to call the function again. - I removed this in favor of a `Meta` param. - - Before: `void presentExceptionAsHtml(alias func, T)(Cgi cgi, Throwable t, T dg)` - - After: `void presentExceptionAsHtml(Meta)(Cgi cgi, Throwable t, Meta meta)` - - If you used the func for something, move that something into your `methodMeta` template. - - What is the benefit of this change? Somewhat smaller executables and faster builds thanks to more reused functions, together with - enabling an easier implementation of [presentExceptionalReturn]. - +/ - void presentExceptionAsHtml(Meta)(Cgi cgi, Throwable t, Meta meta) { - Form af; - /+ - foreach(attr; __traits(getAttributes, func)) { - static if(__traits(isSame, attr, AutomaticForm)) { - af = createAutomaticFormForFunction!(func)(dg); - } - } - +/ - presentExceptionAsHtmlImpl(cgi, t, af); - } - - void presentExceptionAsHtmlImpl(Cgi cgi, Throwable t, Form automaticForm) { - if(auto e = cast(ResourceNotFoundException) t) { - auto container = this.htmlContainer(); - - container.addChild("p", e.msg); - - if(!cgi.outputtedResponseData) - cgi.setResponseStatus("404 Not Found"); - cgi.write(container.parentDocument.toString(), true); - } else if(auto mae = cast(MissingArgumentException) t) { - if(automaticForm is null) - goto generic; - auto container = this.htmlContainer(); - if(cgi.requestMethod == Cgi.RequestMethod.POST) - container.appendChild(Element.make("p", "Argument `" ~ mae.argumentName ~ "` of type `" ~ mae.argumentType ~ "` is missing")); - container.appendChild(automaticForm); - - cgi.write(container.parentDocument.toString(), true); - } else { - generic: - auto container = this.htmlContainer(); - - // import std.stdio; writeln(t.toString()); - - container.appendChild(exceptionToElement(t)); - - container.addChild("h4", "GET"); - foreach(k, v; cgi.get) { - auto deets = container.addChild("details"); - deets.addChild("summary", k); - deets.addChild("div", v); - } - - container.addChild("h4", "POST"); - foreach(k, v; cgi.post) { - auto deets = container.addChild("details"); - deets.addChild("summary", k); - deets.addChild("div", v); - } - - - if(!cgi.outputtedResponseData) - cgi.setResponseStatus("500 Internal Server Error"); - cgi.write(container.parentDocument.toString(), true); - } - } - - Element exceptionToElement(Throwable t) { - auto div = Element.make("div"); - div.addClass("exception-display"); - - div.addChild("p", t.msg); - div.addChild("p", "Inner code origin: " ~ typeid(t).name ~ "@" ~ t.file ~ ":" ~ to!string(t.line)); - - auto pre = div.addChild("pre"); - string s; - s = t.toString(); - Element currentBox; - bool on = false; - foreach(line; s.splitLines) { - if(!on && line.startsWith("-----")) - on = true; - if(!on) continue; - if(line.indexOf("arsd/") != -1) { - if(currentBox is null) { - currentBox = pre.addChild("details"); - currentBox.addChild("summary", "Framework code"); - } - currentBox.addChild("span", line ~ "\n"); - } else { - pre.addChild("span", line ~ "\n"); - currentBox = null; - } - } - - return div; - } - - /++ - Returns an element for a particular type - +/ - Element elementFor(T)(string displayName, string name, Element function() udaSuggestion) { - import std.traits; - - auto div = Element.make("div"); - div.addClass("form-field"); - - static if(is(T == Cgi.UploadedFile)) { - Element lbl; - if(displayName !is null) { - lbl = div.addChild("label"); - lbl.addChild("span", displayName, "label-text"); - lbl.appendText(" "); - } else { - lbl = div; - } - auto i = lbl.addChild("input", name); - i.attrs.name = name; - i.attrs.type = "file"; - } else static if(is(T == enum)) { - Element lbl; - if(displayName !is null) { - lbl = div.addChild("label"); - lbl.addChild("span", displayName, "label-text"); - lbl.appendText(" "); - } else { - lbl = div; - } - auto i = lbl.addChild("select", name); - i.attrs.name = name; - - foreach(memberName; __traits(allMembers, T)) - i.addChild("option", memberName); - - } else static if(is(T == struct)) { - if(displayName !is null) - div.addChild("span", displayName, "label-text"); - auto fieldset = div.addChild("fieldset"); - fieldset.addChild("legend", beautify(T.stringof)); // FIXME - fieldset.addChild("input", name); - foreach(idx, memberName; __traits(allMembers, T)) - static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { - fieldset.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(beautify(memberName), name ~ "." ~ memberName, null /* FIXME: pull off the UDA */)); - } - } else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) { - Element lbl; - if(displayName !is null) { - lbl = div.addChild("label"); - lbl.addChild("span", displayName, "label-text"); - lbl.appendText(" "); - } else { - lbl = div; - } - Element i; - if(udaSuggestion) { - i = udaSuggestion(); - lbl.appendChild(i); - } else { - i = lbl.addChild("input", name); - } - i.attrs.name = name; - static if(isSomeString!T) - i.attrs.type = "text"; - else - i.attrs.type = "number"; - if(i.tagName == "textarea") - i.textContent = to!string(T.init); - else - i.attrs.value = to!string(T.init); - } else static if(is(T == bool)) { - Element lbl; - if(displayName !is null) { - lbl = div.addChild("label"); - lbl.addChild("span", displayName, "label-text"); - lbl.appendText(" "); - } else { - lbl = div; - } - auto i = lbl.addChild("input", name); - i.attrs.type = "checkbox"; - i.attrs.value = "true"; - i.attrs.name = name; - } else static if(is(T == K[], K)) { - auto templ = div.addChild("template"); - templ.appendChild(elementFor!(K)(null, name, null /* uda??*/)); - if(displayName !is null) - div.addChild("span", displayName, "label-text"); - auto btn = div.addChild("button"); - btn.addClass("add-array-button"); - btn.attrs.type = "button"; - btn.innerText = "Add"; - btn.attrs.onclick = q{ - var a = document.importNode(this.parentNode.firstChild.content, true); - this.parentNode.insertBefore(a, this); - }; - } else static if(is(T == V[K], K, V)) { - div.innerText = "assoc array not implemented for automatic form at this time"; - } else { - static assert(0, "unsupported type for cgi call " ~ T.stringof); - } - - - return div; - } - - /// creates a form for gathering the function's arguments - Form createAutomaticFormForFunction(alias method, T)(T dg) { - - auto form = cast(Form) Element.make("form"); - - form.method = "POST"; // FIXME - - form.addClass("automatic-form"); - - string formDisplayName = beautify(__traits(identifier, method)); - foreach(attr; __traits(getAttributes, method)) - static if(is(typeof(attr) == DisplayName)) - formDisplayName = attr.name; - form.addChild("h3", formDisplayName); - - import std.traits; - - //Parameters!method params; - //alias idents = ParameterIdentifierTuple!method; - //alias defaults = ParameterDefaults!method; - - static if(is(typeof(method) P == __parameters)) - foreach(idx, _; P) {{ - - alias param = P[idx .. idx + 1]; - - static if(!mustNotBeSetFromWebParams!(param[0], __traits(getAttributes, param))) { - string displayName = beautify(__traits(identifier, param)); - Element function() element; - foreach(attr; __traits(getAttributes, param)) { - static if(is(typeof(attr) == DisplayName)) - displayName = attr.name; - else static if(is(typeof(attr) : typeof(element))) { - element = attr; - } - } - auto i = form.appendChild(elementFor!(param)(displayName, __traits(identifier, param), element)); - if(i.querySelector("input[type=file]") !is null) - form.setAttribute("enctype", "multipart/form-data"); - } - }} - - form.addChild("div", Html(``), "submit-button-holder"); - - return form; - } - - /// creates a form for gathering object members (for the REST object thing right now) - Form createAutomaticFormForObject(T)(T obj) { - auto form = cast(Form) Element.make("form"); - - form.addClass("automatic-form"); - - form.addChild("h3", beautify(__traits(identifier, T))); - - import std.traits; - - //Parameters!method params; - //alias idents = ParameterIdentifierTuple!method; - //alias defaults = ParameterDefaults!method; - - foreach(idx, memberName; __traits(derivedMembers, T)) {{ - static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { - string displayName = beautify(memberName); - Element function() element; - foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) - static if(is(typeof(attr) == DisplayName)) - displayName = attr.name; - else static if(is(typeof(attr) : typeof(element))) - element = attr; - form.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(displayName, memberName, element)); - - form.setValue(memberName, to!string(__traits(getMember, obj, memberName))); - }}} - - form.addChild("div", Html(``), "submit-button-holder"); - - return form; - } - - /// - Element formatReturnValueAsHtml(T)(T t) { - import std.traits; - - static if(is(T == typeof(null))) { - return Element.make("span"); - } else static if(is(T : Element)) { - return t; - } else static if(is(T == MultipleResponses!Types, Types...)) { - foreach(index, type; Types) { - if(t.contains == index) - return formatReturnValueAsHtml(t.payload[index]); - } - assert(0); - } else static if(is(T == Paginated!E, E)) { - auto e = Element.make("div").addClass("paginated-result"); - e.appendChild(formatReturnValueAsHtml(t.items)); - if(t.nextPageUrl.length) - e.appendChild(Element.make("a", "Next Page", t.nextPageUrl)); - return e; - } else static if(isIntegral!T || isSomeString!T || isFloatingPoint!T) { - return Element.make("span", to!string(t), "automatic-data-display"); - } else static if(is(T == V[K], K, V)) { - auto dl = Element.make("dl"); - dl.addClass("automatic-data-display associative-array"); - foreach(k, v; t) { - dl.addChild("dt", to!string(k)); - dl.addChild("dd", formatReturnValueAsHtml(v)); - } - return dl; - } else static if(is(T == struct)) { - auto dl = Element.make("dl"); - dl.addClass("automatic-data-display struct"); - - foreach(idx, memberName; __traits(allMembers, T)) - static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { - dl.addChild("dt", beautify(memberName)); - dl.addChild("dd", formatReturnValueAsHtml(__traits(getMember, t, memberName))); - } - - return dl; - } else static if(is(T == bool)) { - return Element.make("span", t ? "true" : "false", "automatic-data-display"); - } else static if(is(T == E[], E)) { - static if(is(E : RestObject!Proxy, Proxy)) { - // treat RestObject similar to struct - auto table = cast(Table) Element.make("table"); - table.addClass("automatic-data-display"); - string[] names; - foreach(idx, memberName; __traits(derivedMembers, E)) - static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { - names ~= beautify(memberName); - } - table.appendHeaderRow(names); - - foreach(l; t) { - auto tr = table.appendRow(); - foreach(idx, memberName; __traits(derivedMembers, E)) - static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { - static if(memberName == "id") { - string val = to!string(__traits(getMember, l, memberName)); - tr.addChild("td", Element.make("a", val, E.stringof.toLower ~ "s/" ~ val)); // FIXME - } else { - tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName))); - } - } - } - - return table; - } else static if(is(E == struct)) { - // an array of structs is kinda special in that I like - // having those formatted as tables. - auto table = cast(Table) Element.make("table"); - table.addClass("automatic-data-display"); - string[] names; - foreach(idx, memberName; __traits(allMembers, E)) - static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { - names ~= beautify(memberName); - } - table.appendHeaderRow(names); - - foreach(l; t) { - auto tr = table.appendRow(); - foreach(idx, memberName; __traits(allMembers, E)) - static if(__traits(compiles, __traits(getMember, E, memberName).offsetof)) { - tr.addChild("td", formatReturnValueAsHtml(__traits(getMember, l, memberName))); - } - } - - return table; - } else { - // otherwise, I will just make a list. - auto ol = Element.make("ol"); - ol.addClass("automatic-data-display"); - foreach(e; t) - ol.addChild("li", formatReturnValueAsHtml(e)); - return ol; - } - } else static if(is(T : Object)) { - static if(is(typeof(t.toHtml()))) // FIXME: maybe i will make this an interface - return Element.make("div", t.toHtml()); - else - return Element.make("div", t.toString()); - } else static assert(0, "bad return value for cgi call " ~ T.stringof); - - assert(0); - } - -} - -/++ - The base class for the [dispatcher] function and object support. -+/ -class WebObject { - //protected Cgi cgi; - - protected void initialize(Cgi cgi) { - //this.cgi = cgi; - } -} - -/++ - Can return one of the given types, decided at runtime. The syntax - is to declare all the possible types in the return value, then you - can `return typeof(return)(...value...)` to construct it. - - It has an auto-generated constructor for each value it can hold. - - --- - MultipleResponses!(Redirection, string) getData(int how) { - if(how & 1) - return typeof(return)(Redirection("http://dpldocs.info/")); - else - return typeof(return)("hi there!"); - } - --- - - If you have lots of returns, you could, inside the function, `alias r = typeof(return);` to shorten it a little. -+/ -struct MultipleResponses(T...) { - private size_t contains; - private union { - private T payload; - } - - static foreach(index, type; T) - public this(type t) { - contains = index; - payload[index] = t; - } - - /++ - This is primarily for testing. It is your way of getting to the response. - - Let's say you wanted to test that one holding a Redirection and a string actually - holds a string, by name of "test": - - --- - auto valueToTest = your_test_function(); - - valueToTest.visit( - (Redirection r) { assert(0); }, // got a redirection instead of a string, fail the test - (string s) { assert(s == "test"); } // right value, go ahead and test it. - ); - --- - - History: - Was horribly broken until June 16, 2022. Ironically, I wrote it for tests but never actually tested it. - It tried to use alias lambdas before, but runtime delegates work much better so I changed it. - +/ - void visit(Handlers...)(Handlers handlers) { - template findHandler(type, int count, HandlersToCheck...) { - static if(HandlersToCheck.length == 0) - enum findHandler = -1; - else { - static if(is(typeof(HandlersToCheck[0].init(type.init)))) - enum findHandler = count; - else - enum findHandler = findHandler!(type, count + 1, HandlersToCheck[1 .. $]); - } - } - foreach(index, type; T) { - enum handlerIndex = findHandler!(type, 0, Handlers); - static if(handlerIndex == -1) - static assert(0, "Type " ~ type.stringof ~ " was not handled by visitor"); - else { - if(index == this.contains) - handlers[handlerIndex](this.payload[index]); - } - } - } - - /+ - auto toArsdJsvar()() { - import arsd.jsvar; - return var(null); - } - +/ -} - -// FIXME: implement this somewhere maybe -struct RawResponse { - int code; - string[] headers; - const(ubyte)[] responseBody; -} - -/++ - You can return this from [WebObject] subclasses for redirections. - - (though note the static types means that class must ALWAYS redirect if - you return this directly. You might want to return [MultipleResponses] if it - can be conditional) -+/ -struct Redirection { - string to; /// The URL to redirect to. - int code = 303; /// The HTTP code to return. -} - -/++ - Serves a class' methods, as a kind of low-state RPC over the web. To be used with [dispatcher]. - - Usage of this function will add a dependency on [arsd.dom] and [arsd.jsvar] unless you have overriden - the presenter in the dispatcher. - - FIXME: explain this better - - You can overload functions to a limited extent: you can provide a zero-arg and non-zero-arg function, - and non-zero-arg functions can filter via UDAs for various http methods. Do not attempt other overloads, - the runtime result of that is undefined. - - A method is assumed to allow any http method unless it lists some in UDAs, in which case it is limited to only those. - (this might change, like maybe i will use pure as an indicator GET is ok. idk.) - - $(WARNING - --- - // legal in D, undefined runtime behavior with cgi.d, it may call either method - // even if you put different URL udas on it, the current code ignores them. - void foo(int a) {} - void foo(string a) {} - --- - ) - - See_Also: [serveRestObject], [serveStaticFile] -+/ -auto serveApi(T)(string urlPrefix) { - assert(urlPrefix[$ - 1] == '/'); - return serveApiInternal!T(urlPrefix); -} - -private string nextPieceFromSlash(ref string remainingUrl) { - if(remainingUrl.length == 0) - return remainingUrl; - int slash = 0; - while(slash < remainingUrl.length && remainingUrl[slash] != '/') // && remainingUrl[slash] != '.') - slash++; - - // I am specifically passing `null` to differentiate it vs empty string - // so in your ctor, `items` means new T(null) and `items/` means new T("") - auto ident = remainingUrl.length == 0 ? null : remainingUrl[0 .. slash]; - // so if it is the last item, the dot can be used to load an alternative view - // otherwise tho the dot is considered part of the identifier - // FIXME - - // again notice "" vs null here! - if(slash == remainingUrl.length) - remainingUrl = null; - else - remainingUrl = remainingUrl[slash + 1 .. $]; - - return ident; -} - -/++ - UDA used to indicate to the [dispatcher] that a trailing slash should always be added to or removed from the url. It will do it as a redirect header as-needed. -+/ -enum AddTrailingSlash; -/// ditto -enum RemoveTrailingSlash; - -private auto serveApiInternal(T)(string urlPrefix) { - - import arsd.dom; - import arsd.jsvar; - - static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, immutable void* details) { - string remainingUrl = cgi.pathInfo[urlPrefix.length .. $]; - - try { - // see duplicated code below by searching subresource_ctor - // also see mustNotBeSetFromWebParams - - static if(is(typeof(T.__ctor) P == __parameters)) { - P params; - - foreach(pidx, param; P) { - static if(is(param : Cgi)) { - static assert(!is(param == immutable)); - cast() params[pidx] = cgi; - } else static if(is(param == Session!D, D)) { - static assert(!is(param == immutable)); - cast() params[pidx] = cgi.getSessionObject!D(); - - } else { - static if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) { - foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) { - static if(is(uda == ifCalledFromWeb!func, alias func)) { - static if(is(typeof(func(cgi)))) - params[pidx] = func(cgi); - else - params[pidx] = func(); - } - } - } else { - - static if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) { - params[pidx] = param.getAutomaticallyForCgi(cgi); - } else static if(is(param == string)) { - auto ident = nextPieceFromSlash(remainingUrl); - params[pidx] = ident; - } else static assert(0, "illegal type for subresource " ~ param.stringof); - } - } - } - - auto obj = new T(params); - } else { - auto obj = new T(); - } - - return internalHandlerWithObject(obj, remainingUrl, cgi, presenter); - } catch(Throwable t) { - switch(cgi.request("format", "html")) { - case "html": - static void dummy() {} - presenter.presentExceptionAsHtml(cgi, t, null); - return true; - case "json": - var envelope = var.emptyObject; - envelope.success = false; - envelope.result = null; - envelope.error = t.toString(); - cgi.setResponseContentType("application/json"); - cgi.write(envelope.toJson(), true); - return true; - default: - throw t; - // return true; - } - // return true; - } - - assert(0); - } - - static bool internalHandlerWithObject(T, Presenter)(T obj, string remainingUrl, Cgi cgi, Presenter presenter) { - - obj.initialize(cgi); - - /+ - Overload rules: - Any unique combination of HTTP verb and url path can be dispatched to function overloads - statically. - - Moreover, some args vs no args can be overloaded dynamically. - +/ - - auto methodNameFromUrl = nextPieceFromSlash(remainingUrl); - /+ - auto orig = remainingUrl; - assert(0, - (orig is null ? "__null" : orig) - ~ " .. " ~ - (methodNameFromUrl is null ? "__null" : methodNameFromUrl)); - +/ - - if(methodNameFromUrl is null) - methodNameFromUrl = "__null"; - - string hack = to!string(cgi.requestMethod) ~ " " ~ methodNameFromUrl; - - if(remainingUrl.length) - hack ~= "/"; - - switch(hack) { - foreach(methodName; __traits(derivedMembers, T)) - static if(methodName != "__ctor") - foreach(idx, overload; __traits(getOverloads, T, methodName)) { - static if(is(typeof(overload) P == __parameters)) - static if(is(typeof(overload) R == return)) - static if(__traits(getProtection, overload) == "public" || __traits(getProtection, overload) == "export") - { - static foreach(urlNameForMethod; urlNamesForMethod!(overload, urlify(methodName))) - case urlNameForMethod: - - static if(is(R : WebObject)) { - // if it returns a WebObject, it is considered a subresource. That means the url is dispatched like the ctor above. - - // the only argument it is allowed to take, outside of cgi, session, and set up thingies, is a single string - - // subresource_ctor - // also see mustNotBeSetFromWebParams - - P params; - - string ident; - - foreach(pidx, param; P) { - static if(is(param : Cgi)) { - static assert(!is(param == immutable)); - cast() params[pidx] = cgi; - } else static if(is(param == typeof(presenter))) { - cast() param[pidx] = presenter; - } else static if(is(param == Session!D, D)) { - static assert(!is(param == immutable)); - cast() params[pidx] = cgi.getSessionObject!D(); - } else { - static if(hasIfCalledFromWeb!(__traits(getAttributes, P[pidx .. pidx + 1]))) { - foreach(uda; __traits(getAttributes, P[pidx .. pidx + 1])) { - static if(is(uda == ifCalledFromWeb!func, alias func)) { - static if(is(typeof(func(cgi)))) - params[pidx] = func(cgi); - else - params[pidx] = func(); - } - } - } else { - - static if(__traits(compiles, { params[pidx] = param.getAutomaticallyForCgi(cgi); } )) { - params[pidx] = param.getAutomaticallyForCgi(cgi); - } else static if(is(param == string)) { - ident = nextPieceFromSlash(remainingUrl); - if(ident is null) { - // trailing slash mandated on subresources - cgi.setResponseLocation(cgi.pathInfo ~ "/"); - return true; - } else { - params[pidx] = ident; - } - } else static assert(0, "illegal type for subresource " ~ param.stringof); - } - } - } - - auto nobj = (__traits(getOverloads, obj, methodName)[idx])(ident); - return internalHandlerWithObject!(typeof(nobj), Presenter)(nobj, remainingUrl, cgi, presenter); - } else { - // 404 it if any url left - not a subresource means we don't get to play with that! - if(remainingUrl.length) - return false; - - bool automaticForm; - - foreach(attr; __traits(getAttributes, overload)) - static if(is(attr == AddTrailingSlash)) { - if(remainingUrl is null) { - cgi.setResponseLocation(cgi.pathInfo ~ "/"); - return true; - } - } else static if(is(attr == RemoveTrailingSlash)) { - if(remainingUrl !is null) { - cgi.setResponseLocation(cgi.pathInfo[0 .. lastIndexOf(cgi.pathInfo, "/")]); - return true; - } - - } else static if(__traits(isSame, AutomaticForm, attr)) { - automaticForm = true; - } - - /+ - int zeroArgOverload = -1; - int overloadCount = cast(int) __traits(getOverloads, T, methodName).length; - bool calledWithZeroArgs = true; - foreach(k, v; cgi.get) - if(k != "format") { - calledWithZeroArgs = false; - break; - } - foreach(k, v; cgi.post) - if(k != "format") { - calledWithZeroArgs = false; - break; - } - - // first, we need to go through and see if there is an empty one, since that - // changes inside. But otherwise, all the stuff I care about can be done via - // simple looping (other improper overloads might be flagged for runtime semantic check) - // - // an argument of type Cgi is ignored for these purposes - static foreach(idx, overload; __traits(getOverloads, T, methodName)) {{ - static if(is(typeof(overload) P == __parameters)) - static if(P.length == 0) - zeroArgOverload = cast(int) idx; - else static if(P.length == 1 && is(P[0] : Cgi)) - zeroArgOverload = cast(int) idx; - }} - // FIXME: static assert if there are multiple non-zero-arg overloads usable with a single http method. - bool overloadHasBeenCalled = false; - static foreach(idx, overload; __traits(getOverloads, T, methodName)) {{ - bool callFunction = true; - // there is a zero arg overload and this is NOT it, and we have zero args - don't call this - if(overloadCount > 1 && zeroArgOverload != -1 && idx != zeroArgOverload && calledWithZeroArgs) - callFunction = false; - // if this is the zero-arg overload, obviously it cannot be called if we got any args. - if(overloadCount > 1 && idx == zeroArgOverload && !calledWithZeroArgs) - callFunction = false; - - // FIXME: so if you just add ?foo it will give the error below even when. this might not be a great idea. - - bool hadAnyMethodRestrictions = false; - bool foundAcceptableMethod = false; - foreach(attr; __traits(getAttributes, overload)) { - static if(is(typeof(attr) == Cgi.RequestMethod)) { - hadAnyMethodRestrictions = true; - if(attr == cgi.requestMethod) - foundAcceptableMethod = true; - } - } - - if(hadAnyMethodRestrictions && !foundAcceptableMethod) - callFunction = false; - - /+ - The overloads we really want to allow are the sane ones - from the web perspective. Which is likely on HTTP verbs, - for the most part, but might also be potentially based on - some args vs zero args, or on argument names. Can't really - do argument types very reliable through the web though; those - should probably be different URLs. - - Even names I feel is better done inside the function, so I'm not - going to support that here. But the HTTP verbs and zero vs some - args makes sense - it lets you define custom forms pretty easily. - - Moreover, I'm of the opinion that empty overload really only makes - sense on GET for this case. On a POST, it is just a missing argument - exception and that should be handled by the presenter. But meh, I'll - let the user define that, D only allows one empty arg thing anyway - so the method UDAs are irrelevant. - +/ - if(callFunction) - +/ - - auto format = cgi.request("format", defaultFormat!overload()); - auto wantsFormFormat = format.startsWith("form-"); - - if(wantsFormFormat || (automaticForm && cgi.requestMethod == Cgi.RequestMethod.GET)) { - // Should I still show the form on a json thing? idk... - auto ret = presenter.createAutomaticFormForFunction!((__traits(getOverloads, obj, methodName)[idx]))(&(__traits(getOverloads, obj, methodName)[idx])); - presenter.presentSuccessfulReturn(cgi, ret, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), wantsFormFormat ? format["form_".length .. $] : "html"); - return true; - } - - try { - // a void return (or typeof(null) lol) means you, the user, is doing it yourself. Gives full control. - auto ret = callFromCgi!(__traits(getOverloads, obj, methodName)[idx])(&(__traits(getOverloads, obj, methodName)[idx]), cgi); - presenter.presentSuccessfulReturn(cgi, ret, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), format); - } catch(Throwable t) { - // presenter.presentExceptionAsHtml!(__traits(getOverloads, obj, methodName)[idx])(cgi, t, &(__traits(getOverloads, obj, methodName)[idx])); - presenter.presentExceptionalReturn(cgi, t, presenter.methodMeta!(__traits(getOverloads, obj, methodName)[idx]), format); - } - return true; - //}} - - //cgi.header("Accept: POST"); // FIXME list the real thing - //cgi.setResponseStatus("405 Method Not Allowed"); // again, not exactly, but sort of. no overload matched our args, almost certainly due to http verb filtering. - //return true; - } - } - } - case "GET script.js": - cgi.setResponseContentType("text/javascript"); - cgi.gzipResponse = true; - cgi.write(presenter.script(), true); - return true; - case "GET style.css": - cgi.setResponseContentType("text/css"); - cgi.gzipResponse = true; - cgi.write(presenter.style(), true); - return true; - default: - return false; - } - - assert(0); - } - return DispatcherDefinition!internalHandler(urlPrefix, false); -} - -string defaultFormat(alias method)() { - bool nonConstConditionForWorkingAroundASpuriousDmdWarning = true; - foreach(attr; __traits(getAttributes, method)) { - static if(is(typeof(attr) == DefaultFormat)) { - if(nonConstConditionForWorkingAroundASpuriousDmdWarning) - return attr.value; - } - } - return "html"; -} - -struct Paginated(T) { - T[] items; - string nextPageUrl; -} - -template urlNamesForMethod(alias method, string default_) { - string[] helper() { - auto verb = Cgi.RequestMethod.GET; - bool foundVerb = false; - bool foundNoun = false; - - string def = default_; - - bool hasAutomaticForm = false; - - foreach(attr; __traits(getAttributes, method)) { - static if(is(typeof(attr) == Cgi.RequestMethod)) { - verb = attr; - if(foundVerb) - assert(0, "Multiple http verbs on one function is not currently supported"); - foundVerb = true; - } - static if(is(typeof(attr) == UrlName)) { - if(foundNoun) - assert(0, "Multiple url names on one function is not currently supported"); - foundNoun = true; - def = attr.name; - } - static if(__traits(isSame, attr, AutomaticForm)) { - hasAutomaticForm = true; - } - } - - if(def is null) - def = "__null"; - - string[] ret; - - static if(is(typeof(method) R == return)) { - static if(is(R : WebObject)) { - def ~= "/"; - foreach(v; __traits(allMembers, Cgi.RequestMethod)) - ret ~= v ~ " " ~ def; - } else { - if(hasAutomaticForm) { - ret ~= "GET " ~ def; - ret ~= "POST " ~ def; - } else { - ret ~= to!string(verb) ~ " " ~ def; - } - } - } else static assert(0); - - return ret; - } - enum urlNamesForMethod = helper(); -} - - - enum AccessCheck { - allowed, - denied, - nonExistant, - } - - enum Operation { - show, - create, - replace, - remove, - update - } - - enum UpdateResult { - accessDenied, - noSuchResource, - success, - failure, - unnecessary - } - - enum ValidationResult { - valid, - invalid - } - - -/++ - The base of all REST objects, to be used with [serveRestObject] and [serveRestCollectionOf]. - - WARNING: this is not stable. -+/ -class RestObject(CRTP) : WebObject { - - import arsd.dom; - import arsd.jsvar; - - /// Prepare the object to be shown. - void show() {} - /// ditto - void show(string urlId) { - load(urlId); - show(); - } - - /// Override this to provide access control to this object. - AccessCheck accessCheck(string urlId, Operation operation) { - return AccessCheck.allowed; - } - - ValidationResult validate() { - // FIXME - return ValidationResult.valid; - } - - string getUrlSlug() { - import std.conv; - static if(is(typeof(CRTP.id))) - return to!string((cast(CRTP) this).id); - else - return null; - } - - // The functions with more arguments are the low-level ones, - // they forward to the ones with fewer arguments by default. - - // POST on a parent collection - this is called from a collection class after the members are updated - /++ - Given a populated object, this creates a new entry. Returns the url identifier - of the new object. - +/ - string create(scope void delegate() applyChanges) { - applyChanges(); - save(); - return getUrlSlug(); - } - - void replace() { - save(); - } - void replace(string urlId, scope void delegate() applyChanges) { - load(urlId); - applyChanges(); - replace(); - } - - void update(string[] fieldList) { - save(); - } - void update(string urlId, scope void delegate() applyChanges, string[] fieldList) { - load(urlId); - applyChanges(); - update(fieldList); - } - - void remove() {} - - void remove(string urlId) { - load(urlId); - remove(); - } - - abstract void load(string urlId); - abstract void save(); - - Element toHtml(Presenter)(Presenter presenter) { - import arsd.dom; - import std.conv; - auto obj = cast(CRTP) this; - auto div = Element.make("div"); - div.addClass("Dclass_" ~ CRTP.stringof); - div.dataset.url = getUrlSlug(); - bool first = true; - foreach(idx, memberName; __traits(derivedMembers, CRTP)) - static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { - if(!first) div.addChild("br"); else first = false; - div.appendChild(presenter.formatReturnValueAsHtml(__traits(getMember, obj, memberName))); - } - return div; - } - - var toJson() { - import arsd.jsvar; - var v = var.emptyObject(); - auto obj = cast(CRTP) this; - foreach(idx, memberName; __traits(derivedMembers, CRTP)) - static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { - v[memberName] = __traits(getMember, obj, memberName); - } - return v; - } - - /+ - auto structOf(this This) { - - } - +/ -} - -// FIXME XSRF token, prolly can just put in a cookie and then it needs to be copied to header or form hidden value -// https://use-the-index-luke.com/sql/partial-results/fetch-next-page - -/++ - Base class for REST collections. -+/ -class CollectionOf(Obj) : RestObject!(CollectionOf) { - /// You might subclass this and use the cgi object's query params - /// to implement a search filter, for example. - /// - /// FIXME: design a way to auto-generate that form - /// (other than using the WebObject thing above lol - // it'll prolly just be some searchParams UDA or maybe an enum. - // - // pagination too perhaps. - // - // and sorting too - IndexResult index() { return IndexResult.init; } - - string[] sortableFields() { return null; } - string[] searchableFields() { return null; } - - struct IndexResult { - Obj[] results; - - string[] sortableFields; - - string previousPageIdentifier; - string nextPageIdentifier; - string firstPageIdentifier; - string lastPageIdentifier; - - int numberOfPages; - } - - override string create(scope void delegate() applyChanges) { assert(0); } - override void load(string urlId) { assert(0); } - override void save() { assert(0); } - override void show() { - index(); - } - override void show(string urlId) { - show(); - } - - /// Proxy POST requests (create calls) to the child collection - alias PostProxy = Obj; -} - -/++ - Serves a REST object, similar to a Ruby on Rails resource. - - You put data members in your class. cgi.d will automatically make something out of those. - - It will call your constructor with the ID from the URL. This may be null. - It will then populate the data members from the request. - It will then call a method, if present, telling what happened. You don't need to write these! - It finally returns a reply. - - Your methods are passed a list of fields it actually set. - - The URL mapping - despite my general skepticism of the wisdom - matches up with what most REST - APIs I have used seem to follow. (I REALLY want to put trailing slashes on it though. Works better - with relative linking. But meh.) - - GET /items -> index. all values not set. - GET /items/id -> get. only ID will be set, other params ignored. - POST /items -> create. values set as given - PUT /items/id -> replace. values set as given - or POST /items/id with cgi.post["_method"] (thus urlencoded or multipart content-type) set to "PUT" to work around browser/html limitation - a GET with cgi.get["_method"] (in the url) set to "PUT" will render a form. - PATCH /items/id -> update. values set as given, list of changed fields passed - or POST /items/id with cgi.post["_method"] == "PATCH" - DELETE /items/id -> destroy. only ID guaranteed to be set - or POST /items/id with cgi.post["_method"] == "DELETE" - - Following the stupid convention, there will never be a trailing slash here, and if it is there, it will - redirect you away from it. - - API clients should set the `Accept` HTTP header to application/json or the cgi.get["_format"] = "json" var. - - I will also let you change the default, if you must. - - // One add-on is validation. You can issue a HTTP GET to a resource with _method = VALIDATE to check potential changes. - - You can define sub-resources on your object inside the object. These sub-resources are also REST objects - that follow the same thing. They may be individual resources or collections themselves. - - Your class is expected to have at least the following methods: - - FIXME: i kinda wanna add a routes object to the initialize call - - create - Create returns the new address on success, some code on failure. - show - index - update - remove - - You will want to be able to customize the HTTP, HTML, and JSON returns but generally shouldn't have to - the defaults - should usually work. The returned JSON will include a field "href" on all returned objects along with "id". Or omething like that. - - Usage of this function will add a dependency on [arsd.dom] and [arsd.jsvar]. - - NOT IMPLEMENTED - - - Really, a collection is a resource with a bunch of subresources. - - GET /items - index because it is GET on the top resource - - GET /items/foo - item but different than items? - - class Items { - - } - - ... but meh, a collection can be automated. not worth making it - a separate thing, let's look at a real example. Users has many - items and a virtual one, /users/current. - - the individual users have properties and two sub-resources: - session, which is just one, and comments, a collection. - - class User : RestObject!() { // no parent - int id; - string name; - - // the default implementations of the urlId ones is to call load(that_id) then call the arg-less one. - // but you can override them to do it differently. - - // any member which is of type RestObject can be linked automatically via href btw. - - void show() {} - void show(string urlId) {} // automated! GET of this specific thing - void create() {} // POST on a parent collection - this is called from a collection class after the members are updated - void replace(string urlId) {} // this is the PUT; really, it just updates all fields. - void update(string urlId, string[] fieldList) {} // PATCH, it updates some fields. - void remove(string urlId) {} // DELETE - - void load(string urlId) {} // the default implementation of show() populates the id, then - - this() {} - - mixin Subresource!Session; - mixin Subresource!Comment; - } - - class Session : RestObject!() { - // the parent object may not be fully constructed/loaded - this(User parent) {} - - } - - class Comment : CollectionOf!Comment { - this(User parent) {} - } - - class Users : CollectionOf!User { - // but you don't strictly need ANYTHING on a collection; it will just... collect. Implement the subobjects. - void index() {} // GET on this specific thing; just like show really, just different name for the different semantics. - User create() {} // You MAY implement this, but the default is to create a new object, populate it from args, and then call create() on the child - } - -+/ -auto serveRestObject(T)(string urlPrefix) { - assert(urlPrefix[0] == '/'); - assert(urlPrefix[$ - 1] != '/', "Do NOT use a trailing slash on REST objects."); - static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, immutable void* details) { - string url = cgi.pathInfo[urlPrefix.length .. $]; - - if(url.length && url[$ - 1] == '/') { - // remove the final slash... - cgi.setResponseLocation(cgi.scriptName ~ cgi.pathInfo[0 .. $ - 1]); - return true; - } - - return restObjectServeHandler!T(cgi, presenter, url); - } - return DispatcherDefinition!internalHandler(urlPrefix, false); -} - -/+ -/// Convenience method for serving a collection. It will be named the same -/// as type T, just with an s at the end. If you need any further, just -/// write the class yourself. -auto serveRestCollectionOf(T)(string urlPrefix) { - assert(urlPrefix[0] == '/'); - mixin(`static class `~T.stringof~`s : CollectionOf!(T) {}`); - return serveRestObject!(mixin(T.stringof ~ "s"))(urlPrefix); -} -+/ - -bool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string url) { - string urlId = null; - if(url.length && url[0] == '/') { - // asking for a subobject - urlId = url[1 .. $]; - foreach(idx, ch; urlId) { - if(ch == '/') { - urlId = urlId[0 .. idx]; - break; - } - } - } - - // FIXME handle other subresources - - static if(is(T : CollectionOf!(C), C)) { - if(urlId !is null) { - return restObjectServeHandler!(C, Presenter)(cgi, presenter, url); // FIXME? urlId); - } - } - - // FIXME: support precondition failed, if-modified-since, expectation failed, etc. - - auto obj = new T(); - obj.initialize(cgi); - // FIXME: populate reflection info delegates - - - // FIXME: I am not happy with this. - switch(urlId) { - case "script.js": - cgi.setResponseContentType("text/javascript"); - cgi.gzipResponse = true; - cgi.write(presenter.script(), true); - return true; - case "style.css": - cgi.setResponseContentType("text/css"); - cgi.gzipResponse = true; - cgi.write(presenter.style(), true); - return true; - default: - // intentionally blank - } - - - - - static void applyChangesTemplate(Obj)(Cgi cgi, Obj obj) { - foreach(idx, memberName; __traits(derivedMembers, Obj)) - static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { - __traits(getMember, obj, memberName) = cgi.request(memberName, __traits(getMember, obj, memberName)); - } - } - void applyChanges() { - applyChangesTemplate(cgi, obj); - } - - string[] modifiedList; - - void writeObject(bool addFormLinks) { - if(cgi.request("format") == "json") { - cgi.setResponseContentType("application/json"); - cgi.write(obj.toJson().toString, true); - } else { - auto container = presenter.htmlContainer(); - if(addFormLinks) { - static if(is(T : CollectionOf!(C), C)) - container.appendHtml(` - - - - `); - else - container.appendHtml(` -
    Back -
    - - -
    - `); - } - container.appendChild(obj.toHtml(presenter)); - cgi.write(container.parentDocument.toString, true); - } - } - - // FIXME: I think I need a set type in here.... - // it will be nice to pass sets of members. - - try - switch(cgi.requestMethod) { - case Cgi.RequestMethod.GET: - // I could prolly use template this parameters in the implementation above for some reflection stuff. - // sure, it doesn't automatically work in subclasses... but I instantiate here anyway... - - // automatic forms here for usable basic auto site from browser. - // even if the format is json, it could actually send out the links and formats, but really there i'ma be meh. - switch(cgi.request("_method", "GET")) { - case "GET": - static if(is(T : CollectionOf!(C), C)) { - auto results = obj.index(); - if(cgi.request("format", "html") == "html") { - auto container = presenter.htmlContainer(); - auto html = presenter.formatReturnValueAsHtml(results.results); - container.appendHtml(` -
    - -
    - `); - - container.appendChild(html); - cgi.write(container.parentDocument.toString, true); - } else { - cgi.setResponseContentType("application/json"); - import arsd.jsvar; - var json = var.emptyArray; - foreach(r; results.results) { - var o = var.emptyObject; - foreach(idx, memberName; __traits(derivedMembers, typeof(r))) - static if(__traits(compiles, __traits(getMember, r, memberName).offsetof)) { - o[memberName] = __traits(getMember, r, memberName); - } - - json ~= o; - } - cgi.write(json.toJson(), true); - } - } else { - obj.show(urlId); - writeObject(true); - } - break; - case "PATCH": - obj.load(urlId); - goto case; - case "PUT": - case "POST": - // an editing form for the object - auto container = presenter.htmlContainer(); - static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) { - auto form = (cgi.request("_method") == "POST") ? presenter.createAutomaticFormForObject(new obj.PostProxy()) : presenter.createAutomaticFormForObject(obj); - } else { - auto form = presenter.createAutomaticFormForObject(obj); - } - form.attrs.method = "POST"; - form.setValue("_method", cgi.request("_method", "GET")); - container.appendChild(form); - cgi.write(container.parentDocument.toString(), true); - break; - case "DELETE": - // FIXME: a delete form for the object (can be phrased "are you sure?") - auto container = presenter.htmlContainer(); - container.appendHtml(` -
    - Are you sure you want to delete this item? - - -
    - - `); - cgi.write(container.parentDocument.toString(), true); - break; - default: - cgi.write("bad method\n", true); - } - break; - case Cgi.RequestMethod.POST: - // this is to allow compatibility with HTML forms - switch(cgi.request("_method", "POST")) { - case "PUT": - goto PUT; - case "PATCH": - goto PATCH; - case "DELETE": - goto DELETE; - case "POST": - static if(__traits(compiles, () { auto o = new obj.PostProxy(); })) { - auto p = new obj.PostProxy(); - void specialApplyChanges() { - applyChangesTemplate(cgi, p); - } - string n = p.create(&specialApplyChanges); - } else { - string n = obj.create(&applyChanges); - } - - auto newUrl = cgi.scriptName ~ cgi.pathInfo ~ "/" ~ n; - cgi.setResponseLocation(newUrl); - cgi.setResponseStatus("201 Created"); - cgi.write(`The object has been created.`); - break; - default: - cgi.write("bad method\n", true); - } - // FIXME this should be valid on the collection, but not the child.... - // 303 See Other - break; - case Cgi.RequestMethod.PUT: - PUT: - obj.replace(urlId, &applyChanges); - writeObject(false); - break; - case Cgi.RequestMethod.PATCH: - PATCH: - obj.update(urlId, &applyChanges, modifiedList); - writeObject(false); - break; - case Cgi.RequestMethod.DELETE: - DELETE: - obj.remove(urlId); - cgi.setResponseStatus("204 No Content"); - break; - default: - // FIXME: OPTIONS, HEAD - } - catch(Throwable t) { - presenter.presentExceptionAsHtml(cgi, t); - } - - return true; -} - -/+ -struct SetOfFields(T) { - private void[0][string] storage; - void set(string what) { - //storage[what] = - } - void unset(string what) {} - void setAll() {} - void unsetAll() {} - bool isPresent(string what) { return false; } -} -+/ - -/+ -enum readonly; -enum hideonindex; -+/ - -/++ - Returns true if I recommend gzipping content of this type. You might - want to call it from your Presenter classes before calling cgi.write. - - --- - cgi.setResponseContentType(yourContentType); - cgi.gzipResponse = gzipRecommendedForContentType(yourContentType); - cgi.write(yourData, true); - --- - - This is used internally by [serveStaticFile], [serveStaticFileDirectory], [serveStaticData], and maybe others I forgot to update this doc about. - - - The implementation considers text content to be recommended to gzip. This may change, but it seems reasonable enough for now. - - History: - Added January 28, 2023 (dub v11.0) -+/ -bool gzipRecommendedForContentType(string contentType) { - if(contentType.startsWith("text/")) - return true; - if(contentType.startsWith("application/javascript")) - return true; - - return false; -} - -/++ - Serves a static file. To be used with [dispatcher]. - - See_Also: [serveApi], [serveRestObject], [dispatcher], [serveRedirect] -+/ -auto serveStaticFile(string urlPrefix, string filename = null, string contentType = null) { -// https://baus.net/on-tcp_cork/ -// man 2 sendfile - assert(urlPrefix[0] == '/'); - if(filename is null) - filename = decodeComponent(urlPrefix[1 .. $]); // FIXME is this actually correct? - if(contentType is null) { - contentType = contentTypeFromFileExtension(filename); - } - - static struct DispatcherDetails { - string filename; - string contentType; - } - - static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { - if(details.contentType.indexOf("image/") == 0 || details.contentType.indexOf("audio/") == 0) - cgi.setCache(true); - cgi.setResponseContentType(details.contentType); - cgi.gzipResponse = gzipRecommendedForContentType(details.contentType); - cgi.write(std.file.read(details.filename), true); - return true; - } - return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(filename, contentType)); -} - -/++ - Serves static data. To be used with [dispatcher]. - - History: - Added October 31, 2021 -+/ -auto serveStaticData(string urlPrefix, immutable(void)[] data, string contentType = null) { - assert(urlPrefix[0] == '/'); - if(contentType is null) { - contentType = contentTypeFromFileExtension(urlPrefix); - } - - static struct DispatcherDetails { - immutable(void)[] data; - string contentType; - } - - static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { - cgi.setCache(true); - cgi.setResponseContentType(details.contentType); - cgi.write(details.data, true); - return true; - } - return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(data, contentType)); -} - -string contentTypeFromFileExtension(string filename) { - if(filename.endsWith(".png")) - return "image/png"; - if(filename.endsWith(".apng")) - return "image/apng"; - if(filename.endsWith(".svg")) - return "image/svg+xml"; - if(filename.endsWith(".jpg")) - return "image/jpeg"; - if(filename.endsWith(".html")) - return "text/html"; - if(filename.endsWith(".css")) - return "text/css"; - if(filename.endsWith(".js")) - return "application/javascript"; - if(filename.endsWith(".wasm")) - return "application/wasm"; - if(filename.endsWith(".mp3")) - return "audio/mpeg"; - if(filename.endsWith(".pdf")) - return "application/pdf"; - return null; -} - -/// This serves a directory full of static files, figuring out the content-types from file extensions. -/// It does not let you to descend into subdirectories (or ascend out of it, of course) -auto serveStaticFileDirectory(string urlPrefix, string directory = null, bool recursive = false) { - assert(urlPrefix[0] == '/'); - assert(urlPrefix[$-1] == '/'); - - static struct DispatcherDetails { - string directory; - bool recursive; - } - - if(directory is null) - directory = urlPrefix[1 .. $]; - - if(directory.length == 0) - directory = "./"; - - assert(directory[$-1] == '/'); - - static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { - auto file = decodeComponent(cgi.pathInfo[urlPrefix.length .. $]); // FIXME: is this actually correct - - if(details.recursive) { - // never allow a backslash since it isn't in a typical url anyway and makes the following checks easier - if(file.indexOf("\\") != -1) - return false; - - import std.path; - - file = std.path.buildNormalizedPath(file); - enum upOneDir = ".." ~ std.path.dirSeparator; - - // also no point doing any kind of up directory things since that makes it more likely to break out of the parent - if(file == ".." || file.startsWith(upOneDir)) - return false; - if(std.path.isAbsolute(file)) - return false; - - // FIXME: if it has slashes and stuff, should we redirect to the canonical resource? or what? - - // once it passes these filters it is probably ok. - } else { - if(file.indexOf("/") != -1 || file.indexOf("\\") != -1) - return false; - } - - auto contentType = contentTypeFromFileExtension(file); - - auto fn = details.directory ~ file; - if(std.file.exists(fn)) { - //if(contentType.indexOf("image/") == 0) - //cgi.setCache(true); - //else if(contentType.indexOf("audio/") == 0) - cgi.setCache(true); - cgi.setResponseContentType(contentType); - cgi.gzipResponse = gzipRecommendedForContentType(contentType); - cgi.write(std.file.read(fn), true); - return true; - } else { - return false; - } - } - - return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, false, DispatcherDetails(directory, recursive)); -} - -/++ - Redirects one url to another - - See_Also: [dispatcher], [serveStaticFile] -+/ -auto serveRedirect(string urlPrefix, string redirectTo, int code = 303) { - assert(urlPrefix[0] == '/'); - static struct DispatcherDetails { - string redirectTo; - string code; - } - - static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { - cgi.setResponseLocation(details.redirectTo, true, details.code); - return true; - } - - - return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(redirectTo, getHttpCodeText(code))); -} - -/// Used exclusively with `dispatchTo` -struct DispatcherData(Presenter) { - Cgi cgi; /// You can use this cgi object. - Presenter presenter; /// This is the presenter from top level, and will be forwarded to the sub-dispatcher. - size_t pathInfoStart; /// This is forwarded to the sub-dispatcher. It may be marked private later, or at least read-only. -} - -/++ - Dispatches the URL to a specific function. -+/ -auto handleWith(alias handler)(string urlPrefix) { - // cuz I'm too lazy to do it better right now - static class Hack : WebObject { - static import std.traits; - @UrlName("") - auto handle(std.traits.Parameters!handler args) { - return handler(args); - } - } - - return urlPrefix.serveApiInternal!Hack; -} - -/++ - Dispatches the URL (and anything under it) to another dispatcher function. The function should look something like this: - - --- - bool other(DD)(DD dd) { - return dd.dispatcher!( - "/whatever".serveRedirect("/success"), - "/api/".serveApi!MyClass - ); - } - --- - - The `DD` in there will be an instance of [DispatcherData] which you can inspect, or forward to another dispatcher - here. It is a template to account for any Presenter type, so you can do compile-time analysis in your presenters. - Or, of course, you could just use the exact type in your own code. - - You return true if you handle the given url, or false if not. Just returning the result of [dispatcher] will do a - good job. - - -+/ -auto dispatchTo(alias handler)(string urlPrefix) { - assert(urlPrefix[0] == '/'); - assert(urlPrefix[$-1] != '/'); - static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, const void* details) { - return handler(DispatcherData!Presenter(cgi, presenter, urlPrefix.length)); - } - - return DispatcherDefinition!(internalHandler)(urlPrefix, false); -} - -/++ - See [serveStaticFile] if you want to serve a file off disk. - - History: - Added January 28, 2023 (dub v11.0) -+/ -auto serveStaticData(string urlPrefix, immutable(ubyte)[] data, string contentType, string filenameToSuggestAsDownload = null) { - assert(urlPrefix[0] == '/'); - - static struct DispatcherDetails { - immutable(ubyte)[] data; - string contentType; - string filenameToSuggestAsDownload; - } - - static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { - cgi.setCache(true); - cgi.setResponseContentType(details.contentType); - if(details.filenameToSuggestAsDownload.length) - cgi.header("Content-Disposition: attachment; filename=\""~details.filenameToSuggestAsDownload~"\""); - cgi.gzipResponse = gzipRecommendedForContentType(details.contentType); - cgi.write(details.data, true); - return true; - } - return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(data, contentType, filenameToSuggestAsDownload)); -} - -/++ - Placeholder for use with [dispatchSubsection]'s `NewPresenter` argument to indicate you want to keep the parent's presenter. - - History: - Added January 28, 2023 (dub v11.0) -+/ -alias KeepExistingPresenter = typeof(null); - -/++ - For use with [dispatchSubsection]. Calls your filter with the request and if your filter returns false, - this issues the given errorCode and stops processing. - - --- - bool hasAdminPermissions(Cgi cgi) { - return true; - } - - mixin DispatcherMain!( - "/admin".dispatchSubsection!( - passFilterOrIssueError!(hasAdminPermissions, 403), - KeepExistingPresenter, - "/".serveApi!AdminFunctions - ) - ); - --- - - History: - Added January 28, 2023 (dub v11.0) -+/ -template passFilterOrIssueError(alias filter, int errorCode) { - bool passFilterOrIssueError(DispatcherDetails)(DispatcherDetails dd) { - if(filter(dd.cgi)) - return true; - dd.presenter.renderBasicError(dd.cgi, errorCode); - return false; - } -} - -/++ - Allows for a subsection of your dispatched urls to be passed through other a pre-request filter, optionally pick up an new presenter class, - and then be dispatched to their own handlers. - - --- - /+ - // a long-form filter function - bool permissionCheck(DispatcherData)(DispatcherData dd) { - // you are permitted to call mutable methods on the Cgi object - // Note if you use a Cgi subclass, you can try dynamic casting it back to your custom type to attach per-request data - // though much of the request is immutable so there's only so much you're allowed to do to modify it. - - if(checkPermissionOnRequest(dd.cgi)) { - return true; // OK, allow processing to continue - } else { - dd.presenter.renderBasicError(dd.cgi, 403); // reply forbidden to the requester - return false; // and stop further processing into this subsection - } - } - +/ - - // but you can also do short-form filters: - - bool permissionCheck(Cgi cgi) { - return ("ok" in cgi.get) !is null; - } - - // handler for the subsection - class AdminClass : WebObject { - int foo() { return 5; } - } - - // handler for the main site - class TheMainSite : WebObject {} - - mixin DispatcherMain!( - "/admin".dispatchSubsection!( - // converts our short-form filter into a long-form filter - passFilterOrIssueError!(permissionCheck, 403), - // can use a new presenter if wanted for the subsection - KeepExistingPresenter, - // and then provide child route dispatchers - "/".serveApi!AdminClass - ), - // and back to the top level - "/".serveApi!TheMainSite - ); - --- - - Note you can encapsulate sections in files like this: - - --- - auto adminDispatcher(string urlPrefix) { - return urlPrefix.dispatchSubsection!( - .... - ); - } - - mixin DispatcherMain!( - "/admin".adminDispatcher, - // and so on - ) - --- - - If you want no filter, you can pass `(cgi) => true` as the filter to approve all requests. - - If you want to keep the same presenter as the parent, use [KeepExistingPresenter] as the presenter argument. - - - History: - Added January 28, 2023 (dub v11.0) -+/ -auto dispatchSubsection(alias PreRequestFilter, NewPresenter, definitions...)(string urlPrefix) { - assert(urlPrefix[0] == '/'); - assert(urlPrefix[$-1] != '/'); - static bool internalHandler(Presenter)(string urlPrefix, Cgi cgi, Presenter presenter, const void* details) { - static if(!is(PreRequestFilter == typeof(null))) { - if(!PreRequestFilter(DispatcherData!Presenter(cgi, presenter, urlPrefix.length))) - return true; // we handled it by rejecting it - } - - static if(is(NewPresenter == Presenter) || is(NewPresenter == typeof(null))) { - return dispatcher!definitions(DispatcherData!Presenter(cgi, presenter, urlPrefix.length)); - } else { - auto newPresenter = new NewPresenter(); - return dispatcher!(definitions(DispatcherData!NewPresenter(cgi, presenter, urlPrefix.length))); - } - } - - return DispatcherDefinition!(internalHandler)(urlPrefix, false); -} - -/++ - A URL dispatcher. - - --- - if(cgi.dispatcher!( - "/api/".serveApi!MyApiClass, - "/objects/lol".serveRestObject!MyRestObject, - "/file.js".serveStaticFile, - "/admin/".dispatchTo!adminHandler - )) return; - --- - - - You define a series of url prefixes followed by handlers. - - You may want to do different pre- and post- processing there, for example, - an authorization check and different page layout. You can use different - presenters and different function chains. See [dispatchSubsection] for details. - - [dispatchTo] will send the request to another function for handling. -+/ -template dispatcher(definitions...) { - bool dispatcher(Presenter)(Cgi cgi, Presenter presenterArg = null) { - static if(is(Presenter == typeof(null))) { - static class GenericWebPresenter : WebPresenter!(GenericWebPresenter) {} - auto presenter = new GenericWebPresenter(); - } else - alias presenter = presenterArg; - - return dispatcher(DispatcherData!(typeof(presenter))(cgi, presenter, 0)); - } - - bool dispatcher(DispatcherData)(DispatcherData dispatcherData) if(!is(DispatcherData : Cgi)) { - // I can prolly make this more efficient later but meh. - foreach(definition; definitions) { - if(definition.rejectFurther) { - if(dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $] == definition.urlPrefix) { - auto ret = definition.handler( - dispatcherData.cgi.pathInfo[0 .. dispatcherData.pathInfoStart + definition.urlPrefix.length], - dispatcherData.cgi, dispatcherData.presenter, definition.details); - if(ret) - return true; - } - } else if( - dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart .. $].startsWith(definition.urlPrefix) && - // cgi.d dispatcher urls must be complete or have a /; - // "foo" -> thing should NOT match "foobar", just "foo" or "foo/thing" - (definition.urlPrefix[$-1] == '/' || (dispatcherData.pathInfoStart + definition.urlPrefix.length) == dispatcherData.cgi.pathInfo.length - || dispatcherData.cgi.pathInfo[dispatcherData.pathInfoStart + definition.urlPrefix.length] == '/') - ) { - auto ret = definition.handler( - dispatcherData.cgi.pathInfo[0 .. dispatcherData.pathInfoStart + definition.urlPrefix.length], - dispatcherData.cgi, dispatcherData.presenter, definition.details); - if(ret) - return true; - } - } - return false; - } -} - -}); - -private struct StackBuffer { - char[1024] initial = void; - char[] buffer; - size_t position; - - this(int a) { - buffer = initial[]; - position = 0; - } - - void add(in char[] what) { - if(position + what.length > buffer.length) - buffer.length = position + what.length + 1024; // reallocate with GC to handle special cases - buffer[position .. position + what.length] = what[]; - position += what.length; - } - - void add(in char[] w1, in char[] w2, in char[] w3 = null) { - add(w1); - add(w2); - add(w3); - } - - void add(long v) { - char[16] buffer = void; - auto pos = buffer.length; - bool negative; - if(v < 0) { - negative = true; - v = -v; - } - do { - buffer[--pos] = cast(char) (v % 10 + '0'); - v /= 10; - } while(v); - - if(negative) - buffer[--pos] = '-'; - - auto res = buffer[pos .. $]; - - add(res[]); - } - - char[] get() @nogc { - return buffer[0 .. position]; - } -} - -// duplicated in http2.d -private static string getHttpCodeText(int code) pure nothrow @nogc { - switch(code) { - case 200: return "200 OK"; - case 201: return "201 Created"; - case 202: return "202 Accepted"; - case 203: return "203 Non-Authoritative Information"; - case 204: return "204 No Content"; - case 205: return "205 Reset Content"; - case 206: return "206 Partial Content"; - // - case 300: return "300 Multiple Choices"; - case 301: return "301 Moved Permanently"; - case 302: return "302 Found"; - case 303: return "303 See Other"; - case 304: return "304 Not Modified"; - case 305: return "305 Use Proxy"; - case 307: return "307 Temporary Redirect"; - case 308: return "308 Permanent Redirect"; - - // - case 400: return "400 Bad Request"; - case 401: return "401 Unauthorized"; - case 402: return "402 Payment Required"; - case 403: return "403 Forbidden"; - case 404: return "404 Not Found"; - case 405: return "405 Method Not Allowed"; - case 406: return "406 Not Acceptable"; - case 407: return "407 Proxy Authentication Required"; - case 408: return "408 Request Timeout"; - case 409: return "409 Conflict"; - case 410: return "410 Gone"; - case 411: return "411 Length Required"; - case 412: return "412 Precondition Failed"; - case 413: return "413 Payload Too Large"; - case 414: return "414 URI Too Long"; - case 415: return "415 Unsupported Media Type"; - case 416: return "416 Range Not Satisfiable"; - case 417: return "417 Expectation Failed"; - case 418: return "418 I'm a teapot"; - case 421: return "421 Misdirected Request"; - case 422: return "422 Unprocessable Entity (WebDAV)"; - case 423: return "423 Locked (WebDAV)"; - case 424: return "424 Failed Dependency (WebDAV)"; - case 425: return "425 Too Early"; - case 426: return "426 Upgrade Required"; - case 428: return "428 Precondition Required"; - case 431: return "431 Request Header Fields Too Large"; - case 451: return "451 Unavailable For Legal Reasons"; - - case 500: return "500 Internal Server Error"; - case 501: return "501 Not Implemented"; - case 502: return "502 Bad Gateway"; - case 503: return "503 Service Unavailable"; - case 504: return "504 Gateway Timeout"; - case 505: return "505 HTTP Version Not Supported"; - case 506: return "506 Variant Also Negotiates"; - case 507: return "507 Insufficient Storage (WebDAV)"; - case 508: return "508 Loop Detected (WebDAV)"; - case 510: return "510 Not Extended"; - case 511: return "511 Network Authentication Required"; - // - default: assert(0, "Unsupported http code"); - } -} - - -/+ -/++ - This is the beginnings of my web.d 2.0 - it dispatches web requests to a class object. - - It relies on jsvar.d and dom.d. - - - You can get javascript out of it to call. The generated functions need to look - like - - function name(a,b,c,d,e) { - return _call("name", {"realName":a,"sds":b}); - } - - And _call returns an object you can call or set up or whatever. -+/ -bool apiDispatcher()(Cgi cgi) { - import arsd.jsvar; - import arsd.dom; -} -+/ -version(linux) -private extern(C) int eventfd (uint initval, int flags) nothrow @trusted @nogc; -/* -Copyright: Adam D. Ruppe, 2008 - 2023 -License: [http://www.boost.org/LICENSE_1_0.txt|Boost License 1.0]. -Authors: Adam D. Ruppe - - Copyright Adam D. Ruppe 2008 - 2023. -Distributed under the Boost Software License, Version 1.0. - (See accompanying file LICENSE_1_0.txt or copy at - http://www.boost.org/LICENSE_1_0.txt) -*/ \ No newline at end of file diff --git a/src/config.d b/src/config.d deleted file mode 100644 index 8c9ba2ff..00000000 --- a/src/config.d +++ /dev/null @@ -1,901 +0,0 @@ -import core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit; -import std.file, std.string, std.regex, std.stdio, std.process, std.algorithm.searching, std.getopt, std.conv, std.path; -import std.algorithm.sorting: sort; -import selective; -static import log; - -final class Config -{ - // application defaults - public string defaultSyncDir = "~/OneDrive"; - public string defaultSkipFile = "~*|.~*|*.tmp"; - public string defaultSkipDir = ""; - public string defaultLogFileDir = "/var/log/onedrive/"; - // application set items - public string refreshTokenFilePath = ""; - public string deltaLinkFilePath = ""; - public string databaseFilePath = ""; - public string databaseFilePathDryRun = ""; - public string uploadStateFilePath = ""; - public string syncListFilePath = ""; - public string homePath = ""; - public string configDirName = ""; - public string systemConfigDirName = ""; - public string configFileSyncDir = ""; - public string configFileSkipFile = ""; - public string configFileSkipDir = ""; - public string businessSharedFolderFilePath = ""; - private string userConfigFilePath = ""; - private string systemConfigFilePath = ""; - // was the application just authorised - paste of response uri - public bool applicationAuthorizeResponseUri = false; - // hashmap for the values found in the user config file - // ARGGGG D is stupid and cannot make hashmap initializations!!! - // private string[string] foobar = [ "aa": "bb" ] does NOT work!!! - private string[string] stringValues; - private bool[string] boolValues; - 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; - - // Bring in v2.5.0 config items - - // HTTP Struct items, used for configuring HTTP() - // Curl Timeout Handling - // libcurl dns_cache_timeout timeout - immutable int defaultDnsTimeout = 60; - // Connect timeout for HTTP|HTTPS connections - immutable int defaultConnectTimeout = 10; - // With the following settings we force - // - if there is no data flow for 10min, abort - // - if the download time for one item exceeds 1h, abort - // - // Timeout for activity on connection - // this translates into Curl's CURLOPT_LOW_SPEED_TIME - // which says: - // It contains the time in number seconds that the - // transfer speed should be below the CURLOPT_LOW_SPEED_LIMIT - // for the library to consider it too slow and abort. - immutable int defaultDataTimeout = 600; - // Maximum time any operation is allowed to take - // This includes dns resolution, connecting, data transfer, etc. - immutable int defaultOperationTimeout = 3600; - // Specify how many redirects should be allowed - immutable int defaultMaxRedirects = 5; - // Specify what IP protocol version should be used when communicating with OneDrive - immutable int defaultIpProtocol = 0; // 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only - - - - 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 entry, read from config and set via update_from_args() - stringValues["sync_dir"] = defaultSyncDir; - stringValues["skip_file"] = defaultSkipFile; - stringValues["skip_dir"] = defaultSkipDir; - stringValues["log_dir"] = defaultLogFileDir; - stringValues["drive_id"] = ""; - stringValues["user_agent"] = ""; - boolValues["upload_only"] = false; - boolValues["check_nomount"] = false; - boolValues["check_nosync"] = false; - boolValues["download_only"] = false; - boolValues["disable_notifications"] = false; - boolValues["disable_download_validation"] = false; - boolValues["disable_upload_validation"] = false; - boolValues["enable_logging"] = false; - boolValues["force_http_11"] = false; - boolValues["local_first"] = false; - boolValues["no_remote_delete"] = false; - boolValues["skip_symlinks"] = false; - boolValues["debug_https"] = false; - boolValues["skip_dotfiles"] = false; - boolValues["dry_run"] = false; - boolValues["sync_root_files"] = false; - longValues["verbose"] = log.verbose; // might be initialized by the first getopt call! - // The amount of time (seconds) between monitor sync loops - longValues["monitor_interval"] = 300; - longValues["skip_size"] = 0; - longValues["min_notify_changes"] = 5; - longValues["monitor_log_frequency"] = 6; - // Number of N sync runs before performing a full local scan of sync_dir - // By default 12 which means every ~60 minutes a full disk scan of sync_dir will occur - // 'monitor_interval' * 'monitor_fullscan_frequency' = 3600 = 1 hour - longValues["monitor_fullscan_frequency"] = 12; - // Number of children in a path that is locally removed which will be classified as a 'big data delete' - longValues["classify_as_big_delete"] = 1000; - // Delete source after successful transfer - boolValues["remove_source_files"] = false; - // Strict matching for skip_dir - boolValues["skip_dir_strict_match"] = false; - // Allow for a custom Client ID / Application ID to be used to replace the inbuilt default - // This is a config file option ONLY - stringValues["application_id"] = ""; - // allow for resync to be set via config file - boolValues["resync"] = false; - // resync now needs to be acknowledged based on the 'risk' of using it - boolValues["resync_auth"] = false; - // Ignore data safety checks and overwrite local data rather than preserve & rename - // This is a config file option ONLY - boolValues["bypass_data_preservation"] = false; - // Support National Azure AD endpoints as per https://docs.microsoft.com/en-us/graph/deployments - // By default, if empty, use standard Azure AD URL's - // Will support the following options: - // - USL4 - // AD Endpoint: https://login.microsoftonline.us - // Graph Endpoint: https://graph.microsoft.us - // - USL5 - // AD Endpoint: https://login.microsoftonline.us - // Graph Endpoint: https://dod-graph.microsoft.us - // - DE - // AD Endpoint: https://portal.microsoftazure.de - // Graph Endpoint: https://graph.microsoft.de - // - CN - // AD Endpoint: https://login.chinacloudapi.cn - // Graph Endpoint: https://microsoftgraph.chinacloudapi.cn - stringValues["azure_ad_endpoint"] = ""; - // Support single-tenant applications that are not able to use the "common" multiplexer - 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; - // Configure download / upload rate limits - longValues["rate_limit"] = 0; - // To ensure we do not fill up the load disk, how much disk space should be reserved by default - longValues["space_reservation"] = 50 * 2^^20; // 50 MB as Bytes - // Webhook options - boolValues["webhook_enabled"] = false; - stringValues["webhook_public_url"] = ""; - stringValues["webhook_listening_host"] = ""; - longValues["webhook_listening_port"] = 8888; - longValues["webhook_expiration_interval"] = 3600 * 24; - longValues["webhook_renewal_interval"] = 3600 * 12; - // Log to application output running configuration values - boolValues["display_running_config"] = false; - // Configure read-only authentication scope - boolValues["read_only_auth_scope"] = false; - // Flag to cleanup local files when using --download-only - boolValues["cleanup_local_files"] = false; - - // DEVELOPER OPTIONS - // display_memory = true | false - // - It may be desirable to display the memory usage of the application to assist with diagnosing memory issues with the application - // - This is especially beneficial when debugging or performing memory tests with Valgrind - boolValues["display_memory"] = false; - // monitor_max_loop = long value - // - It may be desirable to, when running in monitor mode, force monitor mode to 'quit' after X number of loops - // - This is especially beneficial when debugging or performing memory tests with Valgrind - longValues["monitor_max_loop"] = 0; - // display_sync_options = true | false - // - It may be desirable to see what options are being passed in to performSync() without enabling the full verbose debug logging - boolValues["display_sync_options"] = false; - // force_children_scan = true | false - // - Force client to use /children rather than /delta to query changes on OneDrive - // - This option flags nationalCloudDeployment as true, forcing the client to act like it is using a National Cloud Deployment - boolValues["force_children_scan"] = false; - // display_processing_time = true | false - // - Enabling this option will add function processing times to the console output - // - This then enables tracking of where the application is spending most amount of time when processing data when users have questions re performance - boolValues["display_processing_time"] = false; - - // HTTPS & CURL Operation Settings - // - Maximum time an operation is allowed to take - // This includes dns resolution, connecting, data transfer, etc. - longValues["operation_timeout"] = defaultOperationTimeout; - // libcurl dns_cache_timeout timeout - longValues["dns_timeout"] = defaultDnsTimeout; - // Timeout for HTTPS connections - longValues["connect_timeout"] = defaultConnectTimeout; - // Timeout for activity on a HTTPS connection - longValues["data_timeout"] = defaultDataTimeout; - // What IP protocol version should be used when communicating with OneDrive - longValues["ip_protocol_version"] = defaultIpProtocol; // 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only - - // EXPAND USERS HOME DIRECTORY - // Determine the users home directory. - // Need to avoid using ~ here as expandTilde() below does not interpret correctly when running under init.d or systemd scripts - // Check for HOME environment variable - if (environment.get("HOME") != ""){ - // Use HOME environment variable - log.vdebug("homePath: HOME environment variable set"); - homePath = environment.get("HOME"); - } else { - if ((environment.get("SHELL") == "") && (environment.get("USER") == "")){ - // No shell is set or username - observed case when running as systemd service under CentOS 7.x - log.vdebug("homePath: WARNING - no HOME environment variable set"); - log.vdebug("homePath: WARNING - no SHELL environment variable set"); - log.vdebug("homePath: WARNING - no USER environment variable set"); - homePath = "/root"; - } else { - // A shell & valid user is set, but no HOME is set, use ~ which can be expanded - log.vdebug("homePath: WARNING - no HOME environment variable set"); - homePath = "~"; - } - } - - // Output homePath calculation - log.vdebug("homePath: ", homePath); - - // Determine the correct configuration directory to use - string configDirBase; - string systemConfigDirBase; - if (confdirOption != "") { - // A CLI 'confdir' was passed in - // Clean up any stray " .. these should not be there ... - confdirOption = strip(confdirOption,"\""); - log.vdebug("configDirName: CLI override to set configDirName to: ", confdirOption); - if (canFind(confdirOption,"~")) { - // A ~ was found - log.vdebug("configDirName: A '~' was found in configDirName, using the calculated 'homePath' to replace '~'"); - configDirName = homePath ~ strip(confdirOption,"~","~"); - } else { - configDirName = confdirOption; - } - } else { - // Determine the base directory relative to which user specific configuration files should be stored. - if (environment.get("XDG_CONFIG_HOME") != ""){ - log.vdebug("configDirBase: XDG_CONFIG_HOME environment variable set"); - configDirBase = environment.get("XDG_CONFIG_HOME"); - } else { - // XDG_CONFIG_HOME does not exist on systems where X11 is not present - ie - headless systems / servers - log.vdebug("configDirBase: WARNING - no XDG_CONFIG_HOME environment variable set"); - configDirBase = homePath ~ "/.config"; - // Also set up a path to pre-shipped shared configs (which can be overridden by supplying a config file in userspace) - systemConfigDirBase = "/etc"; - } - - // Output configDirBase calculation - log.vdebug("configDirBase: ", configDirBase); - // Set the default application configuration directory - log.vdebug("configDirName: Configuring application to use default config path"); - // configDirBase contains the correct path so we do not need to check for presence of '~' - configDirName = configDirBase ~ "/onedrive"; - // systemConfigDirBase contains the correct path so we do not need to check for presence of '~' - systemConfigDirName = systemConfigDirBase ~ "/onedrive"; - } - - // Config directory options all determined - if (!exists(configDirName)) { - // create the directory - mkdirRecurse(configDirName); - // Configure the applicable permissions for the folder - configDirName.setAttributes(returnRequiredDirectoryPermisions()); - } else { - // The config path exists - // The path that exists must be a directory, not a file - if (!isDir(configDirName)) { - if (!confdirOption.empty) { - // the configuration path was passed in by the user .. user error - writeln("ERROR: --confdir entered value is an existing file instead of an existing directory"); - } else { - // other error - writeln("ERROR: ~/.config/onedrive is a file rather than a directory"); - } - // Must exit - exit(EXIT_FAILURE); - } - } - - // configDirName has a trailing / - if (!configDirName.empty) log.vlog("Using 'user' Config Dir: ", configDirName); - if (!systemConfigDirName.empty) log.vlog("Using 'system' Config Dir: ", systemConfigDirName); - - // Update application set variables based on configDirName - refreshTokenFilePath = buildNormalizedPath(configDirName ~ "/refresh_token"); - deltaLinkFilePath = buildNormalizedPath(configDirName ~ "/delta_link"); - databaseFilePath = buildNormalizedPath(configDirName ~ "/items.sqlite3"); - databaseFilePathDryRun = buildNormalizedPath(configDirName ~ "/items-dryrun.sqlite3"); - uploadStateFilePath = buildNormalizedPath(configDirName ~ "/resume_upload"); - userConfigFilePath = buildNormalizedPath(configDirName ~ "/config"); - syncListFilePath = buildNormalizedPath(configDirName ~ "/sync_list"); - systemConfigFilePath = buildNormalizedPath(systemConfigDirName ~ "/config"); - businessSharedFolderFilePath = buildNormalizedPath(configDirName ~ "/business_shared_folders"); - - // Debug Output for application set variables based on configDirName - log.vdebug("refreshTokenFilePath = ", refreshTokenFilePath); - log.vdebug("deltaLinkFilePath = ", deltaLinkFilePath); - log.vdebug("databaseFilePath = ", databaseFilePath); - log.vdebug("databaseFilePathDryRun = ", databaseFilePathDryRun); - log.vdebug("uploadStateFilePath = ", uploadStateFilePath); - log.vdebug("userConfigFilePath = ", userConfigFilePath); - log.vdebug("syncListFilePath = ", syncListFilePath); - log.vdebug("systemConfigFilePath = ", systemConfigFilePath); - log.vdebug("businessSharedFolderFilePath = ", businessSharedFolderFilePath); - } - - bool initialize() - { - // Initialise the application - if (!exists(userConfigFilePath)) { - // 'user' configuration file does not exist - // Is there a system configuration file? - if (!exists(systemConfigFilePath)) { - // 'system' configuration file does not exist - log.vlog("No user or system config file found, using application defaults"); - return true; - } else { - // 'system' configuration file exists - // can we load the configuration file without error? - if (load(systemConfigFilePath)) { - // configuration file loaded without error - log.log("System configuration file successfully loaded"); - return true; - } else { - // there was a problem loading the configuration file - log.log("System configuration file has errors - please check your configuration"); - return false; - } - } - } else { - // 'user' configuration file exists - // can we load the configuration file without error? - if (load(userConfigFilePath)) { - // configuration file loaded without error - log.log("Configuration file successfully loaded"); - return true; - } else { - // there was a problem loading the configuration file - log.log("Configuration file has errors - please check your configuration"); - return false; - } - } - } - - void update_from_args(string[] args) - { - // Add additional options that are NOT configurable via config file - stringValues["create_directory"] = ""; - stringValues["create_share_link"] = ""; - stringValues["destination_directory"] = ""; - stringValues["get_file_link"] = ""; - stringValues["modified_by"] = ""; - stringValues["get_o365_drive_id"] = ""; - stringValues["remove_directory"] = ""; - stringValues["single_directory"] = ""; - stringValues["source_directory"] = ""; - stringValues["auth_files"] = ""; - stringValues["auth_response"] = ""; - boolValues["display_config"] = false; - boolValues["display_sync_status"] = false; - boolValues["print_token"] = false; - boolValues["logout"] = false; - boolValues["reauth"] = false; - boolValues["monitor"] = false; - boolValues["synchronize"] = false; - boolValues["force"] = false; - boolValues["list_business_shared_folders"] = false; - boolValues["force_sync"] = false; - boolValues["with_editing_perms"] = false; - - // Application Startup option validation - try { - string tmpStr; - bool tmpBol; - long tmpVerb; - // duplicated from main.d to get full help output! - auto opt = getopt( - - args, - std.getopt.config.bundling, - std.getopt.config.caseSensitive, - "auth-files", - "Perform authentication not via interactive dialog but via files read/writes to these files.", - &stringValues["auth_files"], - "auth-response", - "Perform authentication not via interactive dialog but via providing the response url directly.", - &stringValues["auth_response"], - "check-for-nomount", - "Check for the presence of .nosync in the syncdir root. If found, do not perform sync.", - &boolValues["check_nomount"], - "check-for-nosync", - "Check for the presence of .nosync in each directory. If found, skip directory from sync.", - &boolValues["check_nosync"], - "classify-as-big-delete", - "Number of children in a path that is locally removed which will be classified as a 'big data delete'", - &longValues["classify_as_big_delete"], - "cleanup-local-files", - "Cleanup additional local files when using --download-only. This will remove local data.", - &boolValues["cleanup_local_files"], - "create-directory", - "Create a directory on OneDrive - no sync will be performed.", - &stringValues["create_directory"], - "create-share-link", - "Create a shareable link for an existing file on OneDrive", - &stringValues["create_share_link"], - "debug-https", - "Debug OneDrive HTTPS communication.", - &boolValues["debug_https"], - "destination-directory", - "Destination directory for renamed or move on OneDrive - no sync will be performed.", - &stringValues["destination_directory"], - "disable-notifications", - "Do not use desktop notifications in monitor mode.", - &boolValues["disable_notifications"], - "disable-download-validation", - "Disable download validation when downloading from OneDrive", - &boolValues["disable_download_validation"], - "disable-upload-validation", - "Disable upload validation when uploading to OneDrive", - &boolValues["disable_upload_validation"], - "display-config", - "Display what options the client will use as currently configured - no sync will be performed.", - &boolValues["display_config"], - "display-running-config", - "Display what options the client has been configured to use on application startup.", - &boolValues["display_running_config"], - "display-sync-status", - "Display the sync status of the client - no sync will be performed.", - &boolValues["display_sync_status"], - "download-only", - "Replicate the OneDrive online state locally, by only downloading changes from OneDrive. Do not upload local changes to OneDrive.", - &boolValues["download_only"], - "dry-run", - "Perform a trial sync with no changes made", - &boolValues["dry_run"], - "enable-logging", - "Enable client activity to a separate log file", - &boolValues["enable_logging"], - "force-http-11", - "Force the use of HTTP 1.1 for all operations", - &boolValues["force_http_11"], - "force", - "Force the deletion of data when a 'big delete' is detected", - &boolValues["force"], - "force-sync", - "Force a synchronization of a specific folder, only when using --synchronize --single-directory and ignore all non-default skip_dir and skip_file rules", - &boolValues["force_sync"], - "get-file-link", - "Display the file link of a synced file", - &stringValues["get_file_link"], - "get-O365-drive-id", - "Query and return the Office 365 Drive ID for a given Office 365 SharePoint Shared Library", - &stringValues["get_o365_drive_id"], - "local-first", - "Synchronize from the local directory source first, before downloading changes from OneDrive.", - &boolValues["local_first"], - "log-dir", - "Directory where logging output is saved to, needs to end with a slash.", - &stringValues["log_dir"], - "logout", - "Logout the current user", - &boolValues["logout"], - "min-notify-changes", - "Minimum number of pending incoming changes necessary to trigger a desktop notification", - &longValues["min_notify_changes"], - "modified-by", - "Display the last modified by details of a given path", - &stringValues["modified_by"], - "monitor|m", - "Keep monitoring for local and remote changes", - &boolValues["monitor"], - "monitor-interval", - "Number of seconds by which each sync operation is undertaken when idle under monitor mode.", - &longValues["monitor_interval"], - "monitor-fullscan-frequency", - "Number of sync runs before performing a full local scan of the synced directory", - &longValues["monitor_fullscan_frequency"], - "monitor-log-frequency", - "Frequency of logging in monitor mode", - &longValues["monitor_log_frequency"], - "no-remote-delete", - "Do not delete local file 'deletes' from OneDrive when using --upload-only", - &boolValues["no_remote_delete"], - "print-token", - "Print the access token, useful for debugging", - &boolValues["print_token"], - "reauth", - "Reauthenticate the client with OneDrive", - &boolValues["reauth"], - "resync", - "Forget the last saved state, perform a full sync", - &boolValues["resync"], - "resync-auth", - "Approve the use of performing a --resync action", - &boolValues["resync_auth"], - "remove-directory", - "Remove a directory on OneDrive - no sync will be performed.", - &stringValues["remove_directory"], - "remove-source-files", - "Remove source file after successful transfer to OneDrive when using --upload-only", - &boolValues["remove_source_files"], - "single-directory", - "Specify a single local directory within the OneDrive root to sync.", - &stringValues["single_directory"], - "skip-dot-files", - "Skip dot files and folders from syncing", - &boolValues["skip_dotfiles"], - "skip-file", - "Skip any files that match this pattern from syncing", - &stringValues["skip_file"], - "skip-dir", - "Skip any directories that match this pattern from syncing", - &stringValues["skip_dir"], - "skip-size", - "Skip new files larger than this size (in MB)", - &longValues["skip_size"], - "skip-dir-strict-match", - "When matching skip_dir directories, only match explicit matches", - &boolValues["skip_dir_strict_match"], - "skip-symlinks", - "Skip syncing of symlinks", - &boolValues["skip_symlinks"], - "source-directory", - "Source directory to rename or move on OneDrive - no sync will be performed.", - &stringValues["source_directory"], - "space-reservation", - "The amount of disk space to reserve (in MB) to avoid 100% disk space utilisation", - &longValues["space_reservation"], - "syncdir", - "Specify the local directory used for synchronization to OneDrive", - &stringValues["sync_dir"], - "synchronize", - "Perform a synchronization", - &boolValues["synchronize"], - "sync-root-files", - "Sync all files in sync_dir root when using sync_list.", - &boolValues["sync_root_files"], - "upload-only", - "Replicate the locally configured sync_dir state to OneDrive, by only uploading local changes to OneDrive. Do not download changes from OneDrive.", - &boolValues["upload_only"], - "user-agent", - "Specify a User Agent string to the http client", - &stringValues["user_agent"], - "confdir", - "Set the directory used to store the configuration files", - &tmpStr, - "verbose|v+", - "Print more details, useful for debugging (repeat for extra debugging)", - &tmpVerb, - "version", - "Print the version and exit", - &tmpBol, - "list-shared-folders", - "List OneDrive Business Shared Folders", - &boolValues["list_business_shared_folders"], - "sync-shared-folders", - "Sync OneDrive Business Shared Folders", - &boolValues["sync_business_shared_folders"], - "with-editing-perms", - "Create a read-write shareable link for an existing file on OneDrive when used with --create-share-link ", - &boolValues["with_editing_perms"] - ); - if (opt.helpWanted) { - outputLongHelp(opt.options); - exit(EXIT_SUCCESS); - } - } catch (GetOptException e) { - log.error(e.msg); - log.error("Try 'onedrive -h' for more information"); - exit(EXIT_FAILURE); - } catch (Exception e) { - // error - log.error(e.msg); - log.error("Try 'onedrive -h' for more information"); - exit(EXIT_FAILURE); - } - } - - string getValueString(string key) - { - auto p = key in stringValues; - if (p) { - return *p; - } else { - throw new Exception("Missing config value: " ~ key); - } - } - - long getValueLong(string key) - { - auto p = key in longValues; - if (p) { - return *p; - } else { - throw new Exception("Missing config value: " ~ key); - } - } - - bool getValueBool(string key) - { - auto p = key in boolValues; - if (p) { - return *p; - } else { - throw new Exception("Missing config value: " ~ key); - } - } - - void setValueBool(string key, bool value) - { - boolValues[key] = value; - } - - void setValueString(string key, string value) - { - stringValues[key] = value; - } - - void setValueLong(string key, long value) - { - longValues[key] = value; - } - - // load a configuration file - private bool load(string filename) - { - // configure function variables - try { - readText(filename); - } catch (std.file.FileException e) { - // Unable to access required file - log.error("ERROR: Unable to access ", e.msg); - // Use exit scopes to shutdown API - return false; - } - - // We were able to readText the config file - so, we should be able to open and read it - auto file = File(filename, "r"); - string lineBuffer; - - // configure scopes - // - failure - scope(failure) { - // close file if open - if (file.isOpen()){ - // close open file - file.close(); - } - } - // - exit - scope(exit) { - // close file if open - if (file.isOpen()){ - // close open file - file.close(); - } - } - - // read file line by line - auto range = file.byLine(); - foreach (line; range) { - lineBuffer = stripLeft(line).to!string; - if (lineBuffer.length == 0 || lineBuffer[0] == ';' || lineBuffer[0] == '#') continue; - auto c = lineBuffer.matchFirst(configRegex); - if (!c.empty) { - c.popFront(); // skip the whole match - string key = c.front.dup; - auto p = key in boolValues; - if (p) { - c.popFront(); - // only accept "true" as true value. TODO Should we support other formats? - setValueBool(key, c.front.dup == "true" ? true : false); - } else { - auto pp = key in stringValues; - if (pp) { - c.popFront(); - setValueString(key, c.front.dup); - // detect need for --resync for these: - // --syncdir ARG - // --skip-file ARG - // --skip-dir ARG - if (key == "sync_dir") configFileSyncDir = c.front.dup; - if (key == "skip_file") { - // Handle multiple entries of skip_file - if (configFileSkipFile.empty) { - // currently no entry exists - configFileSkipFile = c.front.dup; - } else { - // add to existing entry - configFileSkipFile = configFileSkipFile ~ "|" ~ to!string(c.front.dup); - setValueString("skip_file", configFileSkipFile); - } - } - if (key == "skip_dir") { - // Handle multiple entries of skip_dir - if (configFileSkipDir.empty) { - // currently no entry exists - configFileSkipDir = c.front.dup; - } else { - // add to existing entry - configFileSkipDir = configFileSkipDir ~ "|" ~ to!string(c.front.dup); - setValueString("skip_dir", configFileSkipDir); - } - } - // --single-directory Strip quotation marks from path - // This is an issue when using ONEDRIVE_SINGLE_DIRECTORY with Docker - if (key == "single_directory") { - // Strip quotation marks from provided path - string configSingleDirectory = strip(to!string(c.front.dup), "\""); - setValueString("single_directory", configSingleDirectory); - } - // Azure AD Configuration - if (key == "azure_ad_endpoint") { - string azureConfigValue = c.front.dup; - switch(azureConfigValue) { - case "": - log.log("Using config option for Global Azure AD Endpoints"); - break; - case "USL4": - log.log("Using config option for Azure AD for US Government Endpoints"); - break; - case "USL5": - log.log("Using config option for Azure AD for US Government Endpoints (DOD)"); - break; - case "DE": - log.log("Using config option for Azure AD Germany"); - break; - case "CN": - log.log("Using config option for Azure AD China operated by 21Vianet"); - break; - // Default - all other entries - default: - log.log("Unknown Azure AD Endpoint - using Global Azure AD Endpoints"); - } - } - } else { - auto ppp = key in longValues; - if (ppp) { - c.popFront(); - setValueLong(key, to!long(c.front.dup)); - // if key is space_reservation we have to calculate MB -> bytes - if (key == "space_reservation") { - // temp value - ulong tempValue = to!long(c.front.dup); - // a value of 0 needs to be made at least 1MB .. - if (tempValue == 0) { - tempValue = 1; - } - setValueLong("space_reservation", to!long(tempValue * 2^^20)); - } - } else { - log.log("Unknown key in config file: ", key); - return false; - } - } - } - } else { - log.log("Malformed config line: ", lineBuffer); - return false; - } - } - 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 resetSkipToDefaults() { - // reset skip_file and skip_dir to application defaults - // skip_file - log.vdebug("original skip_file: ", getValueString("skip_file")); - log.vdebug("resetting skip_file"); - setValueString("skip_file", defaultSkipFile); - log.vdebug("reset skip_file: ", getValueString("skip_file")); - // skip_dir - log.vdebug("original skip_dir: ", getValueString("skip_dir")); - log.vdebug("resetting skip_dir"); - setValueString("skip_dir", defaultSkipDir); - log.vdebug("reset skip_dir: ", getValueString("skip_dir")); - } -} - -void outputLongHelp(Option[] opt) -{ - auto argsNeedingOptions = [ - "--auth-files", - "--auth-response", - "--confdir", - "--create-directory", - "--create-share-link", - "--destination-directory", - "--get-file-link", - "--get-O365-drive-id", - "--log-dir", - "--min-notify-changes", - "--modified-by", - "--monitor-interval", - "--monitor-log-frequency", - "--monitor-fullscan-frequency", - "--operation-timeout", - "--remove-directory", - "--single-directory", - "--skip-dir", - "--skip-file", - "--skip-size", - "--source-directory", - "--space-reservation", - "--syncdir", - "--user-agent" ]; - writeln(`OneDrive - a client for OneDrive Cloud Services - -Usage: - onedrive [options] --synchronize - Do a one time synchronization - onedrive [options] --monitor - Monitor filesystem and sync regularly - onedrive [options] --display-config - Display the currently used configuration - onedrive [options] --display-sync-status - Query OneDrive service and report on pending changes - onedrive -h | --help - Show this help screen - onedrive --version - Show version - -Options: -`); - foreach (it; opt.sort!("a.optLong < b.optLong")) { - writefln(" %s%s%s%s\n %s", - it.optLong, - it.optShort == "" ? "" : " " ~ it.optShort, - argsNeedingOptions.canFind(it.optLong) ? " ARG" : "", - it.required ? " (required)" : "", it.help); - } -} - -unittest -{ - auto cfg = new Config(""); - cfg.load("config"); - assert(cfg.getValueString("sync_dir") == "~/OneDrive"); -} diff --git a/src/itemdb.d b/src/itemdb.d deleted file mode 100644 index 28fc4712..00000000 --- a/src/itemdb.d +++ /dev/null @@ -1,525 +0,0 @@ -import std.datetime; -import std.exception; -import std.path; -import std.string; -import std.stdio; -import std.algorithm.searching; -import core.stdc.stdlib; -import sqlite; -static import log; - -enum ItemType { - file, - dir, - remote -} - -struct Item { - string driveId; - string id; - string name; - ItemType type; - string eTag; - string cTag; - SysTime mtime; - string parentId; - string quickXorHash; - string sha256Hash; - string remoteDriveId; - string remoteId; - string syncStatus; -} - -final class ItemDatabase -{ - // increment this for every change in the db schema - immutable int itemDatabaseVersion = 11; - - Database db; - string insertItemStmt; - string updateItemStmt; - string selectItemByIdStmt; - string selectItemByParentIdStmt; - string deleteItemByIdStmt; - bool databaseInitialised = false; - - this(const(char)[] filename) - { - db = Database(filename); - int dbVersion; - try { - dbVersion = db.getVersion(); - } catch (SqliteException e) { - // An error was generated - what was the error? - if (e.msg == "database is locked") { - writeln(); - log.error("ERROR: onedrive application is already running - check system process list for active application instances"); - log.vlog(" - Use 'sudo ps aufxw | grep onedrive' to potentially determine acive running process"); - writeln(); - } else { - writeln(); - log.error("ERROR: An internal database error occurred: " ~ e.msg); - writeln(); - } - return; - } - - if (dbVersion == 0) { - createTable(); - } else if (db.getVersion() != itemDatabaseVersion) { - log.log("The item database is incompatible, re-creating database table structures"); - db.exec("DROP TABLE item"); - createTable(); - } - // Set the enforcement of foreign key constraints. - // https://www.sqlite.org/pragma.html#pragma_foreign_keys - // PRAGMA foreign_keys = boolean; - db.exec("PRAGMA foreign_keys = TRUE"); - // Set the recursive trigger capability - // https://www.sqlite.org/pragma.html#pragma_recursive_triggers - // PRAGMA recursive_triggers = boolean; - db.exec("PRAGMA recursive_triggers = TRUE"); - // Set the journal mode for databases associated with the current connection - // https://www.sqlite.org/pragma.html#pragma_journal_mode - db.exec("PRAGMA journal_mode = WAL"); - // Automatic indexing is enabled by default as of version 3.7.17 - // https://www.sqlite.org/pragma.html#pragma_automatic_index - // PRAGMA automatic_index = boolean; - db.exec("PRAGMA automatic_index = FALSE"); - // Tell SQLite to store temporary tables in memory. This will speed up many read operations that rely on temporary tables, indices, and views. - // https://www.sqlite.org/pragma.html#pragma_temp_store - db.exec("PRAGMA temp_store = MEMORY"); - // Tell SQlite to cleanup database table size - // https://www.sqlite.org/pragma.html#pragma_auto_vacuum - // PRAGMA schema.auto_vacuum = 0 | NONE | 1 | FULL | 2 | INCREMENTAL; - db.exec("PRAGMA auto_vacuum = FULL"); - // This pragma sets or queries the database connection locking-mode. The locking-mode is either NORMAL or EXCLUSIVE. - // https://www.sqlite.org/pragma.html#pragma_locking_mode - // PRAGMA schema.locking_mode = NORMAL | EXCLUSIVE - db.exec("PRAGMA locking_mode = EXCLUSIVE"); - - insertItemStmt = " - INSERT OR REPLACE INTO item (driveId, id, name, type, eTag, cTag, mtime, parentId, quickXorHash, sha256Hash, remoteDriveId, remoteId, syncStatus) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13) - "; - updateItemStmt = " - UPDATE item - SET name = ?3, type = ?4, eTag = ?5, cTag = ?6, mtime = ?7, parentId = ?8, quickXorHash = ?9, sha256Hash = ?10, remoteDriveId = ?11, remoteId = ?12, syncStatus = ?13 - WHERE driveId = ?1 AND id = ?2 - "; - selectItemByIdStmt = " - SELECT * - FROM item - WHERE driveId = ?1 AND id = ?2 - "; - selectItemByParentIdStmt = "SELECT * FROM item WHERE driveId = ? AND parentId = ?"; - deleteItemByIdStmt = "DELETE FROM item WHERE driveId = ? AND id = ?"; - - // flag that the database is accessible and we have control - databaseInitialised = true; - } - - bool isDatabaseInitialised() - { - return databaseInitialised; - } - - void createTable() - { - db.exec("CREATE TABLE item ( - driveId TEXT NOT NULL, - id TEXT NOT NULL, - name TEXT NOT NULL, - type TEXT NOT NULL, - eTag TEXT, - cTag TEXT, - mtime TEXT NOT NULL, - parentId TEXT, - quickXorHash TEXT, - sha256Hash TEXT, - remoteDriveId TEXT, - remoteId TEXT, - deltaLink TEXT, - syncStatus TEXT, - PRIMARY KEY (driveId, id), - FOREIGN KEY (driveId, parentId) - REFERENCES item (driveId, id) - ON DELETE CASCADE - ON UPDATE RESTRICT - )"); - db.exec("CREATE INDEX name_idx ON item (name)"); - db.exec("CREATE INDEX remote_idx ON item (remoteDriveId, remoteId)"); - db.exec("CREATE INDEX item_children_idx ON item (driveId, parentId)"); - db.exec("CREATE INDEX selectByPath_idx ON item (name, driveId, parentId)"); - db.setVersion(itemDatabaseVersion); - } - - void insert(const ref Item item) - { - auto p = db.prepare(insertItemStmt); - bindItem(item, p); - p.exec(); - } - - void update(const ref Item item) - { - auto p = db.prepare(updateItemStmt); - bindItem(item, p); - p.exec(); - } - - void dump_open_statements() - { - db.dump_open_statements(); - } - - int db_checkpoint() - { - return db.db_checkpoint(); - } - - void upsert(const ref Item item) - { - auto s = db.prepare("SELECT COUNT(*) FROM item WHERE driveId = ? AND id = ?"); - s.bind(1, item.driveId); - s.bind(2, item.id); - auto r = s.exec(); - Statement stmt; - if (r.front[0] == "0") stmt = db.prepare(insertItemStmt); - else stmt = db.prepare(updateItemStmt); - bindItem(item, stmt); - stmt.exec(); - } - - Item[] selectChildren(const(char)[] driveId, const(char)[] id) - { - auto p = db.prepare(selectItemByParentIdStmt); - p.bind(1, driveId); - p.bind(2, id); - auto res = p.exec(); - Item[] items; - while (!res.empty) { - items ~= buildItem(res); - res.step(); - } - return items; - } - - bool selectById(const(char)[] driveId, const(char)[] id, out Item item) - { - auto p = db.prepare(selectItemByIdStmt); - p.bind(1, driveId); - p.bind(2, id); - auto r = p.exec(); - if (!r.empty) { - item = buildItem(r); - return true; - } - return false; - } - - // returns true if an item id is in the database - bool idInLocalDatabase(const(string) driveId, const(string)id) - { - auto p = db.prepare(selectItemByIdStmt); - p.bind(1, driveId); - p.bind(2, id); - auto r = p.exec(); - if (!r.empty) { - return true; - } - return false; - } - - // returns the item with the given path - // the path is relative to the sync directory ex: "./Music/Turbo Killer.mp3" - bool selectByPath(const(char)[] path, string rootDriveId, out Item item) - { - Item currItem = { driveId: rootDriveId }; - - // Issue https://github.com/abraunegg/onedrive/issues/578 - if (startsWith(path, "./") || path == ".") { - // Need to remove the . from the path prefix - path = "root/" ~ path.chompPrefix("."); - } else { - // Leave path as it is - path = "root/" ~ path; - } - - auto s = db.prepare("SELECT * FROM item WHERE name = ?1 AND driveId IS ?2 AND parentId IS ?3"); - foreach (name; pathSplitter(path)) { - s.bind(1, name); - s.bind(2, currItem.driveId); - s.bind(3, currItem.id); - auto r = s.exec(); - if (r.empty) return false; - currItem = buildItem(r); - // if the item is of type remote substitute it with the child - if (currItem.type == ItemType.remote) { - Item child; - if (selectById(currItem.remoteDriveId, currItem.remoteId, child)) { - assert(child.type != ItemType.remote, "The type of the child cannot be remote"); - currItem = child; - } - } - } - item = currItem; - return true; - } - - // same as selectByPath() but it does not traverse remote folders - bool selectByPathWithoutRemote(const(char)[] path, string rootDriveId, out Item item) - { - Item currItem = { driveId: rootDriveId }; - - // Issue https://github.com/abraunegg/onedrive/issues/578 - if (startsWith(path, "./") || path == ".") { - // Need to remove the . from the path prefix - path = "root/" ~ path.chompPrefix("."); - } else { - // Leave path as it is - path = "root/" ~ path; - } - - auto s = db.prepare("SELECT * FROM item WHERE name IS ?1 AND driveId IS ?2 AND parentId IS ?3"); - foreach (name; pathSplitter(path)) { - s.bind(1, name); - s.bind(2, currItem.driveId); - s.bind(3, currItem.id); - auto r = s.exec(); - if (r.empty) return false; - currItem = buildItem(r); - } - item = currItem; - return true; - } - - void deleteById(const(char)[] driveId, const(char)[] id) - { - auto p = db.prepare(deleteItemByIdStmt); - p.bind(1, driveId); - p.bind(2, id); - p.exec(); - } - - private void bindItem(const ref Item item, ref Statement stmt) - { - with (stmt) with (item) { - bind(1, driveId); - bind(2, id); - bind(3, name); - string typeStr = null; - final switch (type) with (ItemType) { - case file: typeStr = "file"; break; - case dir: typeStr = "dir"; break; - case remote: typeStr = "remote"; break; - } - bind(4, typeStr); - bind(5, eTag); - bind(6, cTag); - bind(7, mtime.toISOExtString()); - bind(8, parentId); - bind(9, quickXorHash); - bind(10, sha256Hash); - bind(11, remoteDriveId); - bind(12, remoteId); - bind(13, syncStatus); - } - } - - private Item buildItem(Statement.Result result) - { - assert(!result.empty, "The result must not be empty"); - assert(result.front.length == 14, "The result must have 14 columns"); - Item item = { - driveId: result.front[0].dup, - id: result.front[1].dup, - name: result.front[2].dup, - eTag: result.front[4].dup, - cTag: result.front[5].dup, - mtime: SysTime.fromISOExtString(result.front[6]), - parentId: result.front[7].dup, - quickXorHash: result.front[8].dup, - sha256Hash: result.front[9].dup, - remoteDriveId: result.front[10].dup, - remoteId: result.front[11].dup, - syncStatus: result.front[12].dup - }; - switch (result.front[3]) { - case "file": item.type = ItemType.file; break; - case "dir": item.type = ItemType.dir; break; - case "remote": item.type = ItemType.remote; break; - default: assert(0, "Invalid item type"); - } - return item; - } - - // computes the path of the given item id - // the path is relative to the sync directory ex: "Music/Turbo Killer.mp3" - // the trailing slash is not added even if the item is a directory - string computePath(const(char)[] driveId, const(char)[] id) - { - assert(driveId && id); - string path; - Item item; - auto s = db.prepare("SELECT * FROM item WHERE driveId = ?1 AND id = ?2"); - auto s2 = db.prepare("SELECT driveId, id FROM item WHERE remoteDriveId = ?1 AND remoteId = ?2"); - while (true) { - s.bind(1, driveId); - s.bind(2, id); - auto r = s.exec(); - if (!r.empty) { - item = buildItem(r); - if (item.type == ItemType.remote) { - // substitute the last name with the current - ptrdiff_t idx = indexOf(path, '/'); - path = idx >= 0 ? item.name ~ path[idx .. $] : item.name; - } else { - if (path) path = item.name ~ "/" ~ path; - else path = item.name; - } - id = item.parentId; - } else { - if (id == null) { - // check for remoteItem - s2.bind(1, item.driveId); - s2.bind(2, item.id); - auto r2 = s2.exec(); - if (r2.empty) { - // root reached - assert(path.length >= 4); - // remove "root/" from path string if it exists - if (path.length >= 5) { - if (canFind(path, "root/")){ - path = path[5 .. $]; - } - } else { - path = path[4 .. $]; - } - // special case of computing the path of the root itself - if (path.length == 0) path = "."; - break; - } else { - // remote folder - driveId = r2.front[0].dup; - id = r2.front[1].dup; - } - } else { - // broken tree - log.vdebug("The following generated a broken tree query:"); - log.vdebug("Drive ID: ", driveId); - log.vdebug("Item ID: ", id); - assert(0); - } - } - } - return path; - } - - Item[] selectRemoteItems() - { - Item[] items; - auto stmt = db.prepare("SELECT * FROM item WHERE remoteDriveId IS NOT NULL"); - auto res = stmt.exec(); - while (!res.empty) { - items ~= buildItem(res); - res.step(); - } - return items; - } - - string getDeltaLink(const(char)[] driveId, const(char)[] id) - { - assert(driveId && id); - auto stmt = db.prepare("SELECT deltaLink FROM item WHERE driveId = ?1 AND id = ?2"); - stmt.bind(1, driveId); - stmt.bind(2, id); - auto res = stmt.exec(); - if (res.empty) return null; - return res.front[0].dup; - } - - void setDeltaLink(const(char)[] driveId, const(char)[] id, const(char)[] deltaLink) - { - assert(driveId && id); - assert(deltaLink); - auto stmt = db.prepare("UPDATE item SET deltaLink = ?3 WHERE driveId = ?1 AND id = ?2"); - stmt.bind(1, driveId); - stmt.bind(2, id); - stmt.bind(3, deltaLink); - stmt.exec(); - } - - // National Cloud Deployments (US and DE) do not support /delta as a query - // We need to track in the database that this item is in sync - // As we query /children to get all children from OneDrive, update anything in the database - // to be flagged as not-in-sync, thus, we can use that flag to determing what was previously - // in-sync, but now deleted on OneDrive - void downgradeSyncStatusFlag(const(char)[] driveId, const(char)[] id) - { - assert(driveId); - auto stmt = db.prepare("UPDATE item SET syncStatus = 'N' WHERE driveId = ?1 AND id = ?2"); - stmt.bind(1, driveId); - stmt.bind(2, id); - stmt.exec(); - } - - // National Cloud Deployments (US and DE) do not support /delta as a query - // Select items that have a out-of-sync flag set - Item[] selectOutOfSyncItems(const(char)[] driveId) - { - assert(driveId); - Item[] items; - auto stmt = db.prepare("SELECT * FROM item WHERE syncStatus = 'N' AND driveId = ?1"); - stmt.bind(1, driveId); - auto res = stmt.exec(); - while (!res.empty) { - items ~= buildItem(res); - res.step(); - } - return items; - } - - // OneDrive Business Folders are stored in the database potentially without a root | parentRoot link - // Select items associated with the provided driveId - Item[] selectByDriveId(const(char)[] driveId) - { - assert(driveId); - Item[] items; - auto stmt = db.prepare("SELECT * FROM item WHERE driveId = ?1 AND parentId IS NULL"); - stmt.bind(1, driveId); - auto res = stmt.exec(); - while (!res.empty) { - items ~= buildItem(res); - res.step(); - } - return items; - } - - // Perform a vacuum on the database, commit WAL / SHM to file - void performVacuum() - { - try { - auto stmt = db.prepare("VACUUM;"); - stmt.exec(); - } catch (SqliteException e) { - writeln(); - log.error("ERROR: Unable to perform a database vacuum: " ~ e.msg); - writeln(); - } - } - - // Select distinct driveId items from database - string[] selectDistinctDriveIds() - { - string[] driveIdArray; - auto stmt = db.prepare("SELECT DISTINCT driveId FROM item;"); - auto res = stmt.exec(); - if (res.empty) return driveIdArray; - while (!res.empty) { - driveIdArray ~= res.front[0].dup; - res.step(); - } - return driveIdArray; - } -} diff --git a/src/log.d b/src/log.d deleted file mode 100644 index b7aa0da6..00000000 --- a/src/log.d +++ /dev/null @@ -1,239 +0,0 @@ -import std.stdio; -import std.file; -import std.datetime; -import std.process; -import std.conv; -import core.memory; -import core.sys.posix.pwd, core.sys.posix.unistd, core.stdc.string : strlen; -import std.algorithm : splitter; -version(Notifications) { - import dnotify; -} - -// enable verbose logging -long verbose; -bool writeLogFile = false; -bool logFileWriteFailFlag = false; - -private bool doNotifications; - -// shared string variable for username -string username; -string logFilePath; - -void init(string logDir) -{ - writeLogFile = true; - username = getUserName(); - logFilePath = logDir; - - if (!exists(logFilePath)){ - // logfile path does not exist - try { - mkdirRecurse(logFilePath); - } - catch (std.file.FileException e) { - // we got an error .. - writeln("\nUnable to access ", logFilePath); - writeln("Please manually create '",logFilePath, "' and set appropriate permissions to allow write access"); - writeln("The requested client activity log will instead be located in your users home directory"); - } - } -} - -void setNotifications(bool value) -{ - version(Notifications) { - // if we try to enable notifications, check for server availability - // and disable in case dbus server is not reachable - if (value) { - auto serverAvailable = dnotify.check_availability(); - if (!serverAvailable) { - log("Notification (dbus) server not available, disabling"); - value = false; - } - } - } - doNotifications = value; -} - -void log(T...)(T args) -{ - writeln(args); - if(writeLogFile){ - // Write to log file - logfileWriteLine(args); - } -} - -void logAndNotify(T...)(T args) -{ - notify(args); - log(args); -} - -void fileOnly(T...)(T args) -{ - if(writeLogFile){ - // Write to log file - logfileWriteLine(args); - } -} - -void vlog(T...)(T args) -{ - if (verbose >= 1) { - writeln(args); - if(writeLogFile){ - // Write to log file - logfileWriteLine(args); - } - } -} - -void vdebug(T...)(T args) -{ - if (verbose >= 2) { - writeln("[DEBUG] ", args); - if(writeLogFile){ - // Write to log file - logfileWriteLine("[DEBUG] ", args); - } - } -} - -void vdebugNewLine(T...)(T args) -{ - if (verbose >= 2) { - writeln("\n[DEBUG] ", args); - if(writeLogFile){ - // Write to log file - logfileWriteLine("\n[DEBUG] ", args); - } - } -} - -void error(T...)(T args) -{ - stderr.writeln(args); - if(writeLogFile){ - // Write to log file - logfileWriteLine(args); - } -} - -void errorAndNotify(T...)(T args) -{ - notify(args); - error(args); -} - -void notify(T...)(T args) -{ - version(Notifications) { - if (doNotifications) { - string result; - foreach (index, arg; args) { - result ~= to!string(arg); - if (index != args.length - 1) - result ~= " "; - } - auto n = new Notification("OneDrive", result, "IGNORED"); - try { - n.show(); - // Sent message to notification daemon - if (verbose >= 2) { - writeln("[DEBUG] Sent notification to notification service. If notification is not displayed, check dbus or notification-daemon for errors"); - } - - } catch (Throwable e) { - vlog("Got exception from showing notification: ", e); - } - } - } -} - -private void logfileWriteLine(T...)(T args) -{ - static import std.exception; - // Write to log file - string logFileName = .logFilePath ~ .username ~ ".onedrive.log"; - auto currentTime = Clock.currTime(); - auto timeString = currentTime.toString(); - File logFile; - - // Resolve: std.exception.ErrnoException@std/stdio.d(423): Cannot open file `/var/log/onedrive/xxxxx.onedrive.log' in mode `a' (Permission denied) - try { - logFile = File(logFileName, "a"); - } - catch (std.exception.ErrnoException e) { - // We cannot open the log file in logFilePath location for writing - // The user is not part of the standard 'users' group (GID 100) - // Change logfile to ~/onedrive.log putting the log file in the users home directory - - if (!logFileWriteFailFlag) { - // write out error message that we cant log to the requested file - writeln("\nUnable to write activity log to ", logFileName); - writeln("Please set appropriate permissions to allow write access to the logging directory for your user account"); - writeln("The requested client activity log will instead be located in your users home directory\n"); - - // set the flag so we dont keep printing this error message - logFileWriteFailFlag = true; - } - - string homePath = environment.get("HOME"); - string logFileNameAlternate = homePath ~ "/onedrive.log"; - logFile = File(logFileNameAlternate, "a"); - } - // Write to the log file - logFile.writeln(timeString, "\t", args); - logFile.close(); -} - -private string getUserName() -{ - auto pw = getpwuid(getuid); - - // get required details - auto runtime_pw_name = pw.pw_name[0 .. strlen(pw.pw_name)].splitter(','); - auto runtime_pw_uid = pw.pw_uid; - auto runtime_pw_gid = pw.pw_gid; - - // user identifiers from process - vdebug("Process ID: ", pw); - vdebug("User UID: ", runtime_pw_uid); - vdebug("User GID: ", runtime_pw_gid); - - // What should be returned as username? - if (!runtime_pw_name.empty && runtime_pw_name.front.length){ - // user resolved - vdebug("User Name: ", runtime_pw_name.front.idup); - return runtime_pw_name.front.idup; - } else { - // Unknown user? - vdebug("User Name: unknown"); - return "unknown"; - } -} - -void displayMemoryUsagePreGC() -{ -// Display memory usage -writeln("\nMemory Usage pre GC (bytes)"); -writeln("--------------------"); -writeln("memory usedSize = ", GC.stats.usedSize); -writeln("memory freeSize = ", GC.stats.freeSize); -// uncomment this if required, if not using LDC 1.16 as this does not exist in that version -//writeln("memory allocatedInCurrentThread = ", GC.stats.allocatedInCurrentThread, "\n"); -} - -void displayMemoryUsagePostGC() -{ -// Display memory usage -writeln("\nMemory Usage post GC (bytes)"); -writeln("--------------------"); -writeln("memory usedSize = ", GC.stats.usedSize); -writeln("memory freeSize = ", GC.stats.freeSize); -// uncomment this if required, if not using LDC 1.16 as this does not exist in that version -//writeln("memory allocatedInCurrentThread = ", GC.stats.allocatedInCurrentThread, "\n"); -} diff --git a/src/main.d b/src/main.d deleted file mode 100644 index 688cd1d5..00000000 --- a/src/main.d +++ /dev/null @@ -1,2094 +0,0 @@ -import core.stdc.stdlib: EXIT_SUCCESS, EXIT_FAILURE, exit; -import core.memory, core.time, core.thread; -import std.getopt, std.file, std.path, std.process, std.stdio, std.conv, std.algorithm.searching, std.string, std.regex; -import config, itemdb, monitor, onedrive, selective, sync, util; -import std.net.curl: CurlException; -import core.stdc.signal; -import std.traits, std.format; -import std.concurrency: receiveTimeout; -import std.datetime; -static import log; - -OneDriveApi oneDrive; -ItemDatabase itemDb; - -bool onedriveInitialised = false; -const int EXIT_UNAUTHORIZED = 3; -const int EXIT_RESYNC_REQUIRED = 126; - -enum MONITOR_LOG_SILENT = 2; -enum MONITOR_LOG_QUIET = 1; -enum LOG_NORMAL = 0; - -int main(string[] args) -{ - // Disable buffering on stdout - stdout.setvbuf(0, _IONBF); - - // main function variables - string confdirOption; - string configFilePath; - string syncListFilePath; - string databaseFilePath; - string businessSharedFolderFilePath; - string currentConfigHash; - string currentSyncListHash; - string previousConfigHash; - string previousSyncListHash; - string configHashFile; - string syncListHashFile; - string configBackupFile; - string syncDir; - string logOutputMessage; - string currentBusinessSharedFoldersHash; - string previousBusinessSharedFoldersHash; - string businessSharedFoldersHashFile; - string databaseFilePathDryRunGlobal; - bool configOptionsDifferent = false; - bool businessSharedFoldersDifferent = false; - bool syncListConfigured = false; - bool syncListDifferent = false; - bool syncDirDifferent = false; - bool skipFileDifferent = false; - bool skipDirDifferent = false; - bool online = false; - bool performSyncOK = false; - bool displayMemoryUsage = false; - bool displaySyncOptions = false; - bool cleanupLocalFilesGlobal = false; - bool synchronizeConfigured = false; - bool invalidSyncExit = false; - - // start and finish messages - string startMessage = "Starting a sync with OneDrive"; - string finishMessage = "Sync with OneDrive is complete"; - string helpMessage = "Please use 'onedrive --help' for further assistance in regards to running this application."; - - // hash file permission values - string hashPermissionValue = "600"; - auto convertedPermissionValue = parse!long(hashPermissionValue, 8); - - // Define scopes - scope(exit) { - // detail what scope was called - log.vdebug("Exit scope called"); - if (synchronizeConfigured) { - log.log(finishMessage); - } - // Display memory details - if (displayMemoryUsage) { - log.displayMemoryUsagePreGC(); - } - // if initialised, shut down the HTTP instance - if (onedriveInitialised) { - oneDrive.shutdown(); - } - // was itemDb initialised? - if (itemDb !is null) { - // Make sure the .wal file is incorporated into the main db before we exit - if(!invalidSyncExit) { - itemDb.performVacuum(); - } - destroy(itemDb); - } - // cleanup any dry-run data - cleanupDryRunDatabase(databaseFilePathDryRunGlobal); - // free API instance - if (oneDrive !is null) { - destroy(oneDrive); - } - // Perform Garbage Cleanup - GC.collect(); - // Display memory details - if (displayMemoryUsage) { - log.displayMemoryUsagePostGC(); - } - } - - scope(failure) { - // detail what scope was called - log.vdebug("Failure scope called"); - // Display memory details - if (displayMemoryUsage) { - log.displayMemoryUsagePreGC(); - } - // if initialised, shut down the HTTP instance - if (onedriveInitialised) { - oneDrive.shutdown(); - } - // was itemDb initialised? - if (itemDb !is null) { - // Make sure the .wal file is incorporated into the main db before we exit - if(!invalidSyncExit) { - itemDb.performVacuum(); - } - destroy(itemDb); - } - // cleanup any dry-run data - cleanupDryRunDatabase(databaseFilePathDryRunGlobal); - // free API instance - if (oneDrive !is null) { - destroy(oneDrive); - } - // Perform Garbage Cleanup - GC.collect(); - // Display memory details - if (displayMemoryUsage) { - log.displayMemoryUsagePostGC(); - } - } - - // read in application options as passed in - try { - bool printVersion = false; - auto opt = getopt( - args, - std.getopt.config.passThrough, - std.getopt.config.bundling, - std.getopt.config.caseSensitive, - "confdir", "Set the directory used to store the configuration files", &confdirOption, - "verbose|v+", "Print more details, useful for debugging (repeat for extra debugging)", &log.verbose, - "version", "Print the version and exit", &printVersion - ); - - // print help and exit - if (opt.helpWanted) { - args ~= "--help"; - } - // print the version and exit - if (printVersion) { - writeln("onedrive ", strip(import("version"))); - return EXIT_SUCCESS; - } - } catch (GetOptException e) { - // option errors - log.error(e.msg); - log.error("Try 'onedrive --help' for more information"); - return EXIT_FAILURE; - } catch (Exception e) { - // generic error - log.error(e.msg); - log.error("Try 'onedrive --help' for more information"); - return EXIT_FAILURE; - } - - // confdirOption must be a directory, not a file - // - By default ~/.config/onedrive will be used - // - If the user is using --confdir , the confdirOption needs to be evaluated when trying to load any file - // load configuration file if available - auto cfg = new config.Config(confdirOption); - if (!cfg.initialize()) { - // There was an error loading the configuration - // Error message already printed - return EXIT_FAILURE; - } - - // How was this application started - what options were passed in - log.vdebug("passed in options: ", args); - log.vdebug("note --confdir and --verbose not listed in args"); - - // set memory display - displayMemoryUsage = cfg.getValueBool("display_memory"); - - // set display sync options - displaySyncOptions = cfg.getValueBool("display_sync_options"); - - // update configuration from command line args - cfg.update_from_args(args); - - // --resync should be a 'last resort item' .. the user needs to 'accept' to proceed - if ((cfg.getValueBool("resync")) && (!cfg.getValueBool("display_config"))) { - // what is the risk acceptance? - bool resyncRiskAcceptance = false; - - if (!cfg.getValueBool("resync_auth")) { - // need to prompt user - char response; - // warning message - writeln("\nThe use of --resync will remove your local 'onedrive' client state, thus no record will exist regarding your current 'sync status'"); - writeln("This has the potential to overwrite local versions of files with potentially older versions downloaded from OneDrive which can lead to data loss"); - writeln("If in-doubt, backup your local data first before proceeding with --resync"); - write("\nAre you sure you wish to proceed with --resync? [Y/N] "); - - try { - // Attempt to read user response - readf(" %c\n", &response); - } catch (std.format.FormatException e) { - // Caught an error - return EXIT_FAILURE; - } - - // Evaluate user repsonse - if ((to!string(response) == "y") || (to!string(response) == "Y")) { - // User has accepted --resync risk to proceed - resyncRiskAcceptance = true; - // Are you sure you wish .. does not use writeln(); - write("\n"); - } - } else { - // resync_auth is true - resyncRiskAcceptance = true; - } - - // Action based on response - if (!resyncRiskAcceptance){ - // --resync risk not accepted - return EXIT_FAILURE; - } - } - - // Initialise normalised file paths - configFilePath = buildNormalizedPath(cfg.configDirName ~ "/config"); - syncListFilePath = buildNormalizedPath(cfg.configDirName ~ "/sync_list"); - databaseFilePath = buildNormalizedPath(cfg.configDirName ~ "/items.db"); - businessSharedFolderFilePath = buildNormalizedPath(cfg.configDirName ~ "/business_shared_folders"); - - // Has any of our configuration that would require a --resync been changed? - // 1. sync_list file modification - // 2. config file modification - but only if sync_dir, skip_dir, skip_file or drive_id was modified - // 3. CLI input overriding configured config file option - configHashFile = buildNormalizedPath(cfg.configDirName ~ "/.config.hash"); - syncListHashFile = buildNormalizedPath(cfg.configDirName ~ "/.sync_list.hash"); - configBackupFile = buildNormalizedPath(cfg.configDirName ~ "/.config.backup"); - businessSharedFoldersHashFile = buildNormalizedPath(cfg.configDirName ~ "/.business_shared_folders.hash"); - - // Does a 'config' file exist with a valid hash file - if (exists(configFilePath)) { - if (!exists(configHashFile)) { - // hash of config file needs to be created, but only if we are not in a --resync scenario - if (!cfg.getValueBool("resync")) { - std.file.write(configHashFile, "initial-hash"); - // Hash file should only be readable by the user who created it - 0600 permissions needed - configHashFile.setAttributes(to!int(convertedPermissionValue)); - } - } - } else { - // no 'config' file exists, application defaults being used, no hash file required - if (exists(configHashFile)) { - // remove the hash, but only if --resync was issued as now the application will use 'defaults' which 'may' be different - if (cfg.getValueBool("resync")) { - // resync issued, remove hash files - safeRemove(configHashFile); - safeRemove(configBackupFile); - } - } - } - - // Does a 'sync_list' file exist with a valid hash file - if (exists(syncListFilePath)) { - if (!exists(syncListHashFile)) { - // hash of config file needs to be created, but only if we are not in a --resync scenario - if (!cfg.getValueBool("resync")) { - std.file.write(syncListHashFile, "initial-hash"); - // Hash file should only be readable by the user who created it - 0600 permissions needed - syncListHashFile.setAttributes(to!int(convertedPermissionValue)); - } - } - } else { - // no 'sync_list' file exists, no hash file required - if (exists(syncListHashFile)) { - // remove the hash, but only if --resync was issued as now the application will use 'defaults' which 'may' be different - if (cfg.getValueBool("resync")) { - // resync issued, remove hash files - safeRemove(syncListHashFile); - } - } - } - - // Does a 'business_shared_folders' file exist with a valid hash file - if (exists(businessSharedFolderFilePath)) { - if (!exists(businessSharedFoldersHashFile)) { - // hash of config file needs to be created, but only if we are not in a --resync scenario - if (!cfg.getValueBool("resync")) { - std.file.write(businessSharedFoldersHashFile, "initial-hash"); - // Hash file should only be readable by the user who created it - 0600 permissions needed - businessSharedFoldersHashFile.setAttributes(to!int(convertedPermissionValue)); - } - } - } else { - // no 'business_shared_folders' file exists, no hash file required - if (exists(businessSharedFoldersHashFile)) { - // remove the hash, but only if --resync was issued as now the application will use 'defaults' which 'may' be different - if (cfg.getValueBool("resync")) { - // resync issued, remove hash files - safeRemove(businessSharedFoldersHashFile); - } - } - } - - // Generate current hashes for the relevant configuration files if they exist - if (exists(configFilePath)) currentConfigHash = computeQuickXorHash(configFilePath); - if (exists(syncListFilePath)) currentSyncListHash = computeQuickXorHash(syncListFilePath); - if (exists(businessSharedFolderFilePath)) currentBusinessSharedFoldersHash = computeQuickXorHash(businessSharedFolderFilePath); - - // read the existing hashes for each of the relevant configuration files if they exist - if (exists(configHashFile)) { - try { - previousConfigHash = readText(configHashFile); - } catch (std.file.FileException e) { - // Unable to access required file - log.error("ERROR: Unable to access ", e.msg); - // Use exit scopes to shutdown API - return EXIT_FAILURE; - } - } - if (exists(syncListHashFile)) { - try { - previousSyncListHash = readText(syncListHashFile); - } catch (std.file.FileException e) { - // Unable to access required file - log.error("ERROR: Unable to access ", e.msg); - // Use exit scopes to shutdown API - return EXIT_FAILURE; - } - } - if (exists(businessSharedFoldersHashFile)) { - try { - previousBusinessSharedFoldersHash = readText(businessSharedFoldersHashFile); - } catch (std.file.FileException e) { - // Unable to access required file - log.error("ERROR: Unable to access ", e.msg); - // Use exit scopes to shutdown API - return EXIT_FAILURE; - } - } - - // Was sync_list file updated? - if (currentSyncListHash != previousSyncListHash) { - // Debugging output to assist what changed - log.vdebug("sync_list file has been updated, --resync needed"); - syncListDifferent = true; - } - - // Was business_shared_folders updated? - if (currentBusinessSharedFoldersHash != previousBusinessSharedFoldersHash) { - // Debugging output to assist what changed - log.vdebug("business_shared_folders file has been updated, --resync needed"); - businessSharedFoldersDifferent = true; - } - - // Was config file updated between last execution ang this execution? - if (currentConfigHash != previousConfigHash) { - // config file was updated, however we only want to trigger a --resync requirement if sync_dir, skip_dir, skip_file or drive_id was modified - if (!cfg.getValueBool("display_config")){ - // only print this message if we are not using --display-config - log.log("config file has been updated, checking if --resync needed"); - } - if (exists(configBackupFile)) { - // check backup config what has changed for these configuration options if anything - // # sync_dir = "~/OneDrive" - // # skip_file = "~*|.~*|*.tmp" - // # skip_dir = "" - // # drive_id = "" - string[string] stringValues; - stringValues["sync_dir"] = ""; - stringValues["skip_file"] = ""; - stringValues["skip_dir"] = ""; - stringValues["drive_id"] = ""; - auto configBackupFileHandle = File(configBackupFile, "r"); - string lineBuffer; - auto range = configBackupFileHandle.byLine(); - // read configBackupFile line by line - foreach (line; range) { - lineBuffer = stripLeft(line).to!string; - if (lineBuffer.length == 0 || lineBuffer[0] == ';' || lineBuffer[0] == '#') continue; - auto c = lineBuffer.matchFirst(cfg.configRegex); - if (!c.empty) { - c.popFront(); // skip the whole match - string key = c.front.dup; - auto p = key in stringValues; - if (p) { - c.popFront(); - // compare this key - if ((key == "sync_dir") && (c.front.dup != cfg.getValueString("sync_dir"))) { - log.vdebug(key, " was modified since the last time the application was successfully run, --resync needed"); - configOptionsDifferent = true; - } - - if ((key == "skip_file") && (c.front.dup != cfg.getValueString("skip_file"))){ - log.vdebug(key, " was modified since the last time the application was successfully run, --resync needed"); - configOptionsDifferent = true; - } - if ((key == "skip_dir") && (c.front.dup != cfg.getValueString("skip_dir"))){ - log.vdebug(key, " was modified since the last time the application was successfully run, --resync needed"); - configOptionsDifferent = true; - } - if ((key == "drive_id") && (c.front.dup != cfg.getValueString("drive_id"))){ - log.vdebug(key, " was modified since the last time the application was successfully run, --resync needed"); - configOptionsDifferent = true; - } - } - } - } - // close file if open - if (configBackupFileHandle.isOpen()){ - // close open file - configBackupFileHandle.close(); - } - } else { - // no backup to check - log.vdebug("WARNING: no backup config file was found, unable to validate if any changes made"); - } - - // If there was a backup, any modified values we need to worry about would been detected - if (!cfg.getValueBool("display_config")) { - // we are not testing the configuration - if (!configOptionsDifferent) { - // no options are different - if (!cfg.getValueBool("dry_run")) { - // we are not in a dry-run scenario - // update config hash - log.vdebug("updating config hash as it is out of date"); - std.file.write(configHashFile, computeQuickXorHash(configFilePath)); - // Hash file should only be readable by the user who created it - 0600 permissions needed - configHashFile.setAttributes(to!int(convertedPermissionValue)); - // create backup copy of current config file - log.vdebug("making backup of config file as it is out of date"); - std.file.copy(configFilePath, configBackupFile); - // File Copy should only be readable by the user who created it - 0600 permissions needed - configBackupFile.setAttributes(to!int(convertedPermissionValue)); - } - } - } - } - - // Is there a backup of the config file if the config file exists? - if ((exists(configFilePath)) && (!exists(configBackupFile))) { - // create backup copy of current config file - std.file.copy(configFilePath, configBackupFile); - // File Copy should only be readable by the user who created it - 0600 permissions needed - configBackupFile.setAttributes(to!int(convertedPermissionValue)); - } - - // config file set options can be changed via CLI input, specifically these will impact sync and --resync will be needed: - // --syncdir ARG - // --skip-file ARG - // --skip-dir ARG - if (exists(configFilePath)) { - // config file exists - // was the sync_dir updated by CLI? - if (cfg.configFileSyncDir != "") { - // sync_dir was set in config file - if (cfg.configFileSyncDir != cfg.getValueString("sync_dir")) { - // config file was set and CLI input changed this - log.vdebug("sync_dir: CLI override of config file option, --resync needed"); - syncDirDifferent = true; - } - } - - // was the skip_file updated by CLI? - if (cfg.configFileSkipFile != "") { - // skip_file was set in config file - if (cfg.configFileSkipFile != cfg.getValueString("skip_file")) { - // config file was set and CLI input changed this - log.vdebug("skip_file: CLI override of config file option, --resync needed"); - skipFileDifferent = true; - } - } - - // was the skip_dir updated by CLI? - if (cfg.configFileSkipDir != "") { - // skip_dir was set in config file - if (cfg.configFileSkipDir != cfg.getValueString("skip_dir")) { - // config file was set and CLI input changed this - log.vdebug("skip_dir: CLI override of config file option, --resync needed"); - skipDirDifferent = true; - } - } - } - - // Has anything triggered a --resync requirement? - if (configOptionsDifferent || syncListDifferent || syncDirDifferent || skipFileDifferent || skipDirDifferent || businessSharedFoldersDifferent) { - // --resync needed, is the user performing any operation where a --resync is not required? - // flag to ignore --resync requirement - bool ignoreResyncRequirement = false; - // These flags do not need --resync as no sync operation is needed: --display-config, --list-shared-folders, --get-O365-drive-id, --get-file-link - if (cfg.getValueBool("display_config")) ignoreResyncRequirement = true; - if (cfg.getValueBool("list_business_shared_folders")) ignoreResyncRequirement = true; - if ((!cfg.getValueString("get_o365_drive_id").empty)) ignoreResyncRequirement = true; - if ((!cfg.getValueString("get_file_link").empty)) ignoreResyncRequirement = true; - - // Do we need to ignore a --resync requirement? - if (!ignoreResyncRequirement) { - // We are not ignoring --requirement - if (!cfg.getValueBool("resync")) { - // --resync not issued, fail fast - log.error("An application configuration change has been detected where a --resync is required"); - return EXIT_RESYNC_REQUIRED; - } else { - // --resync issued, update hashes of config files if they exist - if (!cfg.getValueBool("dry_run")) { - // not doing a dry run, update hash files if config & sync_list exist - if (exists(configFilePath)) { - // update hash - log.vdebug("updating config hash as --resync issued"); - std.file.write(configHashFile, computeQuickXorHash(configFilePath)); - // Hash file should only be readable by the user who created it - 0600 permissions needed - configHashFile.setAttributes(to!int(convertedPermissionValue)); - // create backup copy of current config file - log.vdebug("making backup of config file as --resync issued"); - std.file.copy(configFilePath, configBackupFile); - // File copy should only be readable by the user who created it - 0600 permissions needed - configBackupFile.setAttributes(to!int(convertedPermissionValue)); - } - if (exists(syncListFilePath)) { - // update sync_list hash - log.vdebug("updating sync_list hash as --resync issued"); - std.file.write(syncListHashFile, computeQuickXorHash(syncListFilePath)); - // Hash file should only be readable by the user who created it - 0600 permissions needed - syncListHashFile.setAttributes(to!int(convertedPermissionValue)); - } - if (exists(businessSharedFolderFilePath)) { - // update business_shared_folders hash - log.vdebug("updating business_shared_folders hash as --resync issued"); - std.file.write(businessSharedFoldersHashFile, computeQuickXorHash(businessSharedFolderFilePath)); - // Hash file should only be readable by the user who created it - 0600 permissions needed - businessSharedFoldersHashFile.setAttributes(to!int(convertedPermissionValue)); - } - } - } - } - } - - // --dry-run operation notification and database setup - // Are we performing any of the following operations? - // --dry-run, --list-shared-folders, --get-O365-drive-id, --get-file-link - if ((cfg.getValueBool("dry_run")) || (cfg.getValueBool("list_business_shared_folders")) || (!cfg.getValueString("get_o365_drive_id").empty) || (!cfg.getValueString("get_file_link").empty)) { - // is this a --list-shared-folders, --get-O365-drive-id, --get-file-link operation - if (cfg.getValueBool("dry_run")) { - // this is a --dry-run operation - log.log("DRY-RUN Configured. Output below shows what 'would' have occurred."); - } else { - // is this a --list-shared-folders, --get-O365-drive-id, --get-file-link operation - log.log("Using dry-run database copy for OneDrive API query"); - } - // configure databaseFilePathDryRunGlobal - databaseFilePathDryRunGlobal = cfg.databaseFilePathDryRun; - - string dryRunShmFile = databaseFilePathDryRunGlobal ~ "-shm"; - string dryRunWalFile = databaseFilePathDryRunGlobal ~ "-wal"; - // If the dry run database exists, clean this up - if (exists(databaseFilePathDryRunGlobal)) { - // remove the existing file - log.vdebug("Removing items-dryrun.sqlite3 as it still exists for some reason"); - safeRemove(databaseFilePathDryRunGlobal); - } - // 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)) { - // in a --dry-run --resync scenario, we should not copy the existing database file - if (!cfg.getValueBool("resync")) { - // copy the existing DB file to the dry-run copy - log.vdebug("Copying items.sqlite3 to items-dryrun.sqlite3 to use for dry run operations"); - copy(cfg.databaseFilePath,databaseFilePathDryRunGlobal); - } else { - // no database copy due to --resync - log.vdebug("No database copy created for --dry-run due to --resync also being used"); - } - } - } - - // sync_dir environment handling to handle ~ expansion properly - bool shellEnvSet = false; - if ((environment.get("SHELL") == "") && (environment.get("USER") == "")){ - log.vdebug("sync_dir: No SHELL or USER environment variable configuration detected"); - // No shell or user set, so expandTilde() will fail - usually headless system running under init.d / systemd or potentially Docker - // Does the 'currently configured' sync_dir include a ~ - if (canFind(cfg.getValueString("sync_dir"), "~")) { - // A ~ was found in sync_dir - log.vdebug("sync_dir: A '~' was found in sync_dir, using the calculated 'homePath' to replace '~' as no SHELL or USER environment variable set"); - syncDir = cfg.homePath ~ strip(cfg.getValueString("sync_dir"), "~"); - } else { - // No ~ found in sync_dir, use as is - log.vdebug("sync_dir: Getting syncDir from config value sync_dir"); - syncDir = cfg.getValueString("sync_dir"); - } - } else { - // A shell and user is set, expand any ~ as this will be expanded correctly if present - shellEnvSet = true; - log.vdebug("sync_dir: Getting syncDir from config value sync_dir"); - if (canFind(cfg.getValueString("sync_dir"), "~")) { - log.vdebug("sync_dir: A '~' was found in configured sync_dir, automatically expanding as SHELL and USER environment variable is set"); - syncDir = expandTilde(cfg.getValueString("sync_dir")); - } else { - syncDir = cfg.getValueString("sync_dir"); - } - } - - // vdebug syncDir as set and calculated - log.vdebug("syncDir: ", syncDir); - - // Configure the logging directory if different from application default - // log_dir environment handling to handle ~ expansion properly - string logDir = cfg.getValueString("log_dir"); - if (logDir != cfg.defaultLogFileDir) { - // user modified log_dir entry - // if 'log_dir' contains a '~' this needs to be expanded correctly - if (canFind(cfg.getValueString("log_dir"), "~")) { - // ~ needs to be expanded correctly - if (!shellEnvSet) { - // No shell or user set, so expandTilde() will fail - usually headless system running under init.d / systemd or potentially Docker - log.vdebug("log_dir: A '~' was found in log_dir, using the calculated 'homePath' to replace '~' as no SHELL or USER environment variable set"); - logDir = cfg.homePath ~ strip(cfg.getValueString("log_dir"), "~"); - } else { - // A shell and user is set, expand any ~ as this will be expanded correctly if present - log.vdebug("log_dir: A '~' was found in log_dir, using SHELL or USER environment variable to expand '~'"); - logDir = expandTilde(cfg.getValueString("log_dir")); - } - } else { - // '~' not found in log_dir entry, use as is - logDir = cfg.getValueString("log_dir"); - } - // update log_dir with normalised path, with '~' expanded correctly - cfg.setValueString("log_dir", logDir); - } - - // Configure logging only if enabled - if (cfg.getValueBool("enable_logging")){ - // Initialise using the configured logging directory - log.vlog("Using logfile dir: ", logDir); - log.init(logDir); - } - - // Configure whether notifications are used - log.setNotifications(cfg.getValueBool("monitor") && !cfg.getValueBool("disable_notifications")); - - // Application upgrades - skilion version etc - if (exists(databaseFilePath)) { - if (!cfg.getValueBool("dry_run")) { - safeRemove(databaseFilePath); - } - log.logAndNotify("Database schema changed, resync needed"); - cfg.setValueBool("resync", true); - } - - // Handle --logout as separate item, do not 'resync' on a --logout - if (cfg.getValueBool("logout")) { - log.vdebug("--logout requested"); - log.log("Deleting the saved authentication status ..."); - if (!cfg.getValueBool("dry_run")) { - safeRemove(cfg.refreshTokenFilePath); - } - // Exit - return EXIT_SUCCESS; - } - - // Handle --reauth to re-authenticate the client - if (cfg.getValueBool("reauth")) { - log.vdebug("--reauth requested"); - log.log("Deleting the saved authentication status ... re-authentication requested"); - if (!cfg.getValueBool("dry_run")) { - safeRemove(cfg.refreshTokenFilePath); - } - } - - // Display current application configuration - if ((cfg.getValueBool("display_config")) || (cfg.getValueBool("display_running_config"))) { - if (cfg.getValueBool("display_running_config")) { - writeln("--------------- Application Runtime Configuration ---------------"); - } - - // Display application version - writeln("onedrive version = ", strip(import("version"))); - // Display all of the pertinent configuration options - writeln("Config path = ", cfg.configDirName); - // Does a config file exist or are we using application defaults - writeln("Config file found in config path = ", exists(configFilePath)); - - // Is config option drive_id configured? - if (cfg.getValueString("drive_id") != ""){ - writeln("Config option 'drive_id' = ", cfg.getValueString("drive_id")); - } - - // Config Options as per 'config' file - writeln("Config option 'sync_dir' = ", syncDir); - - // logging and notifications - writeln("Config option 'enable_logging' = ", cfg.getValueBool("enable_logging")); - writeln("Config option 'log_dir' = ", cfg.getValueString("log_dir")); - writeln("Config option 'disable_notifications' = ", cfg.getValueBool("disable_notifications")); - writeln("Config option 'min_notify_changes' = ", cfg.getValueLong("min_notify_changes")); - - // skip files and directory and 'matching' policy - writeln("Config option 'skip_dir' = ", cfg.getValueString("skip_dir")); - writeln("Config option 'skip_dir_strict_match' = ", cfg.getValueBool("skip_dir_strict_match")); - writeln("Config option 'skip_file' = ", cfg.getValueString("skip_file")); - writeln("Config option 'skip_dotfiles' = ", cfg.getValueBool("skip_dotfiles")); - writeln("Config option 'skip_symlinks' = ", cfg.getValueBool("skip_symlinks")); - - // --monitor sync process options - writeln("Config option 'monitor_interval' = ", cfg.getValueLong("monitor_interval")); - writeln("Config option 'monitor_log_frequency' = ", cfg.getValueLong("monitor_log_frequency")); - writeln("Config option 'monitor_fullscan_frequency' = ", cfg.getValueLong("monitor_fullscan_frequency")); - - // sync process and method - writeln("Config option 'read_only_auth_scope' = ", cfg.getValueBool("read_only_auth_scope")); - writeln("Config option 'dry_run' = ", cfg.getValueBool("dry_run")); - writeln("Config option 'upload_only' = ", cfg.getValueBool("upload_only")); - writeln("Config option 'download_only' = ", cfg.getValueBool("download_only")); - writeln("Config option 'local_first' = ", cfg.getValueBool("local_first")); - writeln("Config option 'check_nosync' = ", cfg.getValueBool("check_nosync")); - writeln("Config option 'check_nomount' = ", cfg.getValueBool("check_nomount")); - writeln("Config option 'resync' = ", cfg.getValueBool("resync")); - writeln("Config option 'resync_auth' = ", cfg.getValueBool("resync_auth")); - writeln("Config option 'cleanup_local_files' = ", cfg.getValueBool("cleanup_local_files")); - - // data integrity - writeln("Config option 'classify_as_big_delete' = ", cfg.getValueLong("classify_as_big_delete")); - writeln("Config option 'disable_upload_validation' = ", cfg.getValueBool("disable_upload_validation")); - writeln("Config option 'bypass_data_preservation' = ", cfg.getValueBool("bypass_data_preservation")); - writeln("Config option 'no_remote_delete' = ", cfg.getValueBool("no_remote_delete")); - writeln("Config option 'remove_source_files' = ", cfg.getValueBool("remove_source_files")); - writeln("Config option 'sync_dir_permissions' = ", cfg.getValueLong("sync_dir_permissions")); - writeln("Config option 'sync_file_permissions' = ", cfg.getValueLong("sync_file_permissions")); - writeln("Config option 'space_reservation' = ", cfg.getValueLong("space_reservation")); - - // curl operations - writeln("Config option 'application_id' = ", cfg.getValueString("application_id")); - writeln("Config option 'azure_ad_endpoint' = ", cfg.getValueString("azure_ad_endpoint")); - writeln("Config option 'azure_tenant_id' = ", cfg.getValueString("azure_tenant_id")); - writeln("Config option 'user_agent' = ", cfg.getValueString("user_agent")); - writeln("Config option 'force_http_11' = ", cfg.getValueBool("force_http_11")); - writeln("Config option 'debug_https' = ", cfg.getValueBool("debug_https")); - writeln("Config option 'rate_limit' = ", cfg.getValueLong("rate_limit")); - writeln("Config option 'operation_timeout' = ", cfg.getValueLong("operation_timeout")); - writeln("Config option 'dns_timeout' = ", cfg.getValueLong("dns_timeout")); - writeln("Config option 'connect_timeout' = ", cfg.getValueLong("connect_timeout")); - writeln("Config option 'data_timeout' = ", cfg.getValueLong("data_timeout")); - writeln("Config option 'ip_protocol_version' = ", cfg.getValueLong("ip_protocol_version")); - - // Is sync_list configured ? - writeln("Config option 'sync_root_files' = ", cfg.getValueBool("sync_root_files")); - if (exists(syncListFilePath)){ - - writeln("Selective sync 'sync_list' configured = true"); - writeln("sync_list contents:"); - // Output the sync_list contents - auto syncListFile = File(syncListFilePath, "r"); - auto range = syncListFile.byLine(); - foreach (line; range) - { - writeln(line); - } - } else { - writeln("Selective sync 'sync_list' configured = false"); - - } - - // Is business_shared_folders enabled and configured ? - writeln("Config option 'sync_business_shared_folders' = ", cfg.getValueBool("sync_business_shared_folders")); - if (exists(businessSharedFolderFilePath)){ - writeln("Business Shared Folders configured = true"); - writeln("business_shared_folders contents:"); - // Output the business_shared_folders contents - auto businessSharedFolderFileList = File(businessSharedFolderFilePath, "r"); - auto range = businessSharedFolderFileList.byLine(); - foreach (line; range) - { - writeln(line); - } - } else { - writeln("Business Shared Folders configured = false"); - } - - // Are webhooks enabled? - writeln("Config option 'webhook_enabled' = ", cfg.getValueBool("webhook_enabled")); - if (cfg.getValueBool("webhook_enabled")) { - writeln("Config option 'webhook_public_url' = ", cfg.getValueString("webhook_public_url")); - writeln("Config option 'webhook_listening_host' = ", cfg.getValueString("webhook_listening_host")); - writeln("Config option 'webhook_listening_port' = ", cfg.getValueLong("webhook_listening_port")); - writeln("Config option 'webhook_expiration_interval' = ", cfg.getValueLong("webhook_expiration_interval")); - writeln("Config option 'webhook_renewal_interval' = ", cfg.getValueLong("webhook_renewal_interval")); - } - - if (cfg.getValueBool("display_running_config")) { - writeln("-----------------------------------------------------------------"); - } - - // Do we exit? We only exit if --display-config has been used - if (cfg.getValueBool("display_config")) { - return EXIT_SUCCESS; - } - } - - // --upload-only and --download-only are mutually exclusive and cannot be used together - if ((cfg.getValueBool("upload_only")) && (cfg.getValueBool("download_only"))) { - // both cannot be true at the same time - writeln("ERROR: --upload-only and --download-only are mutually exclusive and cannot be used together.\n"); - return EXIT_FAILURE; - } - - // Handle the actual --resync to remove local files - if (cfg.getValueBool("resync")) { - log.vdebug("--resync requested"); - log.vdebug("Testing if we have exclusive access to local database file"); - // Are we the only running instance? Test that we can open the database file path - itemDb = new ItemDatabase(cfg.databaseFilePath); - - // did we successfully initialise the database class? - if (!itemDb.isDatabaseInitialised()) { - // no .. destroy class - itemDb = null; - // exit application - return EXIT_FAILURE; - } - - // If we have exclusive access we will not have exited - // destroy access test - destroy(itemDb); - // delete application sync state - log.log("Deleting the saved application sync status ..."); - if (!cfg.getValueBool("dry_run")) { - safeRemove(cfg.databaseFilePath); - safeRemove(cfg.deltaLinkFilePath); - safeRemove(cfg.uploadStateFilePath); - } - } - - // Test if OneDrive service can be reached, exit if it cant be reached - log.vdebug("Testing network to ensure network connectivity to Microsoft OneDrive Service"); - online = testNetwork(cfg); - if (!online) { - // Cant initialise the API as we are not online - if (!cfg.getValueBool("monitor")) { - // Running as --synchronize - log.error("Unable to reach Microsoft OneDrive API service, unable to initialize application\n"); - return EXIT_FAILURE; - } else { - // Running as --monitor - log.error("Unable to reach Microsoft OneDrive API service at this point in time, re-trying network tests\n"); - // re-try network connection to OneDrive - // https://github.com/abraunegg/onedrive/issues/1184 - // Back off & retry with incremental delay - int retryCount = 10000; - int retryAttempts = 1; - int backoffInterval = 1; - int maxBackoffInterval = 3600; - - bool retrySuccess = false; - while (!retrySuccess){ - // retry to access OneDrive API - backoffInterval++; - int thisBackOffInterval = retryAttempts*backoffInterval; - log.vdebug(" Retry Attempt: ", retryAttempts); - if (thisBackOffInterval <= maxBackoffInterval) { - log.vdebug(" Retry In (seconds): ", thisBackOffInterval); - Thread.sleep(dur!"seconds"(thisBackOffInterval)); - } else { - log.vdebug(" Retry In (seconds): ", maxBackoffInterval); - Thread.sleep(dur!"seconds"(maxBackoffInterval)); - } - // perform the re-rty - online = testNetwork(cfg); - if (online) { - // We are now online - log.log("Internet connectivity to Microsoft OneDrive service has been restored"); - retrySuccess = true; - } else { - // We are still offline - if (retryAttempts == retryCount) { - // we have attempted to re-connect X number of times - // false set this to true to break out of while loop - retrySuccess = true; - } - } - // Increment & loop around - retryAttempts++; - } - if (!online) { - // Not online after 1.2 years of trying - log.error("ERROR: Was unable to reconnect to the Microsoft OneDrive service after 10000 attempts lasting over 1.2 years!"); - return EXIT_FAILURE; - } - } - } - - // Check application version and Initialize OneDrive API, check for authorization - if (online) { - // Check Application Version - log.vlog("Checking Application Version ..."); - checkApplicationVersion(); - - // we can only initialise if we are online - log.vlog("Initializing the OneDrive API ..."); - oneDrive = new OneDriveApi(cfg); - onedriveInitialised = oneDrive.init(); - oneDrive.printAccessToken = cfg.getValueBool("print_token"); - } - - if (!onedriveInitialised) { - log.error("Could not initialize the OneDrive API"); - // Use exit scopes to shutdown API - return EXIT_UNAUTHORIZED; - } - - // if --synchronize or --monitor not passed in, configure the flag to display help & exit - if (cfg.getValueBool("synchronize") || cfg.getValueBool("monitor")) { - performSyncOK = true; - } - - // --source-directory must only be used with --destination-directory - // neither can (or should) be added individually as they have a no operational impact if they are - if (((cfg.getValueString("source_directory") == "") && (cfg.getValueString("destination_directory") != "")) || ((cfg.getValueString("source_directory") != "") && (cfg.getValueString("destination_directory") == ""))) { - // so either --source-directory or --destination-directory was passed in, without the other required item being passed in - // --source-directory or --destination-directory cannot be used with --synchronize or --monitor - writeln(); - if (performSyncOK) { - // log an error - log.error("ERROR: --source-directory or --destination-directory cannot be used with --synchronize or --monitor"); - } else { - // display issue with using these options - string emptyParameter; - string dataParameter; - if (cfg.getValueString("source_directory").empty) { - emptyParameter = "--source-directory"; - dataParameter = "--destination-directory"; - } else { - emptyParameter = "--destination-directory"; - dataParameter = "--source-directory"; - } - log.error("ERROR: " ~ dataParameter ~ " was passed in without also using " ~ emptyParameter); - } - // Use exit scopes to shutdown API - writeln(); - log.error(helpMessage); - writeln(); - return EXIT_FAILURE; - } - - // --create-directory, --remove-directory, --source-directory, --destination-directory - // these are activities that dont perform a sync, so to not generate an error message for these items either - if (((cfg.getValueString("create_directory") != "") || (cfg.getValueString("remove_directory") != "")) || ((cfg.getValueString("source_directory") != "") && (cfg.getValueString("destination_directory") != "")) || (cfg.getValueString("get_file_link") != "") || (cfg.getValueString("modified_by") != "") || (cfg.getValueString("create_share_link") != "") || (cfg.getValueString("get_o365_drive_id") != "") || cfg.getValueBool("display_sync_status") || cfg.getValueBool("list_business_shared_folders")) { - performSyncOK = true; - } - - // Were acceptable sync operations provided? Was --synchronize or --monitor passed in - if (!performSyncOK) { - // was the application just authorised? - if (cfg.applicationAuthorizeResponseUri) { - // Application was just authorised - 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(helpMessage); - writeln(); - // 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 'onedrive --help' for further assistance.\n"); - log.log("No OneDrive sync will be performed without one of these two arguments being present.\n"); - // Use exit scopes to shutdown API - invalidSyncExit = true; - return EXIT_FAILURE; - } - } - - // if --synchronize && --monitor passed in, exit & display help as these conflict with each other - if (cfg.getValueBool("synchronize") && cfg.getValueBool("monitor")) { - writeln(); - log.error("ERROR: --synchronize and --monitor cannot be used together"); - writeln(); - log.error(helpMessage); - writeln(); - // Use exit scopes to shutdown API - return EXIT_FAILURE; - } - - // Initialize the item database - log.vlog("Opening the item database ..."); - // Are we performing any of the following operations? - // --dry-run, --list-shared-folders, --get-O365-drive-id, --get-file-link - if ((cfg.getValueBool("dry_run")) || (cfg.getValueBool("list_business_shared_folders")) || (!cfg.getValueString("get_o365_drive_id").empty) || (!cfg.getValueString("get_file_link").empty)) { - // Load the items-dryrun.sqlite3 file as the database - log.vdebug("Using database file: ", asNormalizedPath(databaseFilePathDryRunGlobal)); - itemDb = new ItemDatabase(databaseFilePathDryRunGlobal); - } else { - // Not a dry-run scenario or trying to query O365 Library - should be the default scenario - // Load the items.sqlite3 file as the database - log.vdebug("Using database file: ", asNormalizedPath(cfg.databaseFilePath)); - itemDb = new ItemDatabase(cfg.databaseFilePath); - } - - // did we successfully initialise the database class? - if (!itemDb.isDatabaseInitialised()) { - // no .. destroy class - itemDb = null; - // exit application - return EXIT_FAILURE; - } - - // 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); - try { - if (!exists(syncDir)) { - log.vdebug("syncDir: Configured syncDir is missing. Creating: ", syncDir); - try { - // Attempt to create the sync dir we have been configured with - mkdirRecurse(syncDir); - // 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); - // Use exit scopes to shutdown API - return EXIT_FAILURE; - } - } - } catch (std.file.FileException e) { - // Creating the sync directory failed - log.error("ERROR: Unable to test the configured OneDrive syncDir - ", e.msg); - // Use exit scopes to shutdown API - 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 - auto selectiveSync = new SelectiveSync(); - - // load sync_list if it exists - if (exists(syncListFilePath)){ - log.vdebug("Loading user configured sync_list file ..."); - syncListConfigured = true; - // list what will be synced - auto syncListFile = File(syncListFilePath, "r"); - auto range = syncListFile.byLine(); - foreach (line; range) - { - log.vdebug("sync_list: ", line); - } - // close syncListFile if open - if (syncListFile.isOpen()){ - // close open file - syncListFile.close(); - } - } - selectiveSync.load(syncListFilePath); - - // load business_shared_folders if it exists - if (exists(businessSharedFolderFilePath)){ - log.vdebug("Loading user configured business_shared_folders file ..."); - // list what will be synced - auto businessSharedFolderFileList = File(businessSharedFolderFilePath, "r"); - auto range = businessSharedFolderFileList.byLine(); - foreach (line; range) - { - log.vdebug("business_shared_folders: ", line); - } - } - selectiveSync.loadSharedFolders(businessSharedFolderFilePath); - - // Configure skip_dir, skip_file, skip-dir-strict-match & skip_dotfiles from config entries - // Handle skip_dir configuration in config file - log.vdebug("Configuring skip_dir ..."); - log.vdebug("skip_dir: ", cfg.getValueString("skip_dir")); - selectiveSync.setDirMask(cfg.getValueString("skip_dir")); - - // Was --skip-dir-strict-match configured? - log.vdebug("Configuring skip_dir_strict_match ..."); - log.vdebug("skip_dir_strict_match: ", cfg.getValueBool("skip_dir_strict_match")); - if (cfg.getValueBool("skip_dir_strict_match")) { - selectiveSync.setSkipDirStrictMatch(); - } - - // Was --skip-dot-files configured? - log.vdebug("Configuring skip_dotfiles ..."); - log.vdebug("skip_dotfiles: ", cfg.getValueBool("skip_dotfiles")); - if (cfg.getValueBool("skip_dotfiles")) { - selectiveSync.setSkipDotfiles(); - } - - // Handle skip_file configuration in config file - log.vdebug("Configuring skip_file ..."); - // Validate skip_file to ensure that this does not contain an invalid configuration - // Do not use a skip_file entry of .* as this will prevent correct searching of local changes to process. - foreach(entry; cfg.getValueString("skip_file").split("|")){ - if (entry == ".*") { - // invalid entry element detected - log.logAndNotify("ERROR: Invalid skip_file entry '.*' detected"); - return EXIT_FAILURE; - } - } - // All skip_file entries are valid - log.vdebug("skip_file: ", cfg.getValueString("skip_file")); - selectiveSync.setFileMask(cfg.getValueString("skip_file")); - - // Implement https://github.com/abraunegg/onedrive/issues/1129 - // Force a synchronization of a specific folder, only when using --synchronize --single-directory and ignoring all non-default skip_dir and skip_file rules - if ((cfg.getValueBool("synchronize")) && (cfg.getValueString("single_directory") != "") && (cfg.getValueBool("force_sync"))) { - log.log("\nWARNING: Overriding application configuration to use application defaults for skip_dir and skip_file due to --synchronize --single-directory --force-sync being used"); - // performing this action could have undesirable effects .. the user must accept this risk - // what is the risk acceptance? - bool resyncRiskAcceptance = false; - - // need to prompt user - char response; - // warning message - writeln("\nThe use of --force-sync will reconfigure the application to use defaults. This may have untold and unknown future impacts."); - writeln("By proceeding in using this option you accept any impacts including any data loss that may occur as a result of using --force-sync."); - write("\nAre you sure you wish to proceed with --force-sync [Y/N] "); - - try { - // Attempt to read user response - readf(" %c\n", &response); - } catch (std.format.FormatException e) { - // Caught an error - return EXIT_FAILURE; - } - - // Evaluate user repsonse - if ((to!string(response) == "y") || (to!string(response) == "Y")) { - // User has accepted --force-sync risk to proceed - resyncRiskAcceptance = true; - // Are you sure you wish .. does not use writeln(); - write("\n"); - } - - // Action based on response - if (!resyncRiskAcceptance){ - // --force-sync not accepted - return EXIT_FAILURE; - } else { - // --force-sync risk accepted - // reset set config using function to use application defaults - cfg.resetSkipToDefaults(); - // update sync engine regex with reset defaults - selectiveSync.setDirMask(cfg.getValueString("skip_dir")); - selectiveSync.setFileMask(cfg.getValueString("skip_file")); - } - } - - // Initialize the sync engine - auto sync = new SyncEngine(cfg, oneDrive, itemDb, selectiveSync); - try { - if (!initSyncEngine(sync)) { - // Use exit scopes to shutdown API - return EXIT_FAILURE; - } else { - if ((cfg.getValueString("get_file_link") == "") && (cfg.getValueString("create_share_link") == "")) { - // Print out that we are initializing the engine only if we are not grabbing the file link or creating a shareable link - log.logAndNotify("Initializing the Synchronization Engine ..."); - } - } - } catch (CurlException e) { - if (!cfg.getValueBool("monitor")) { - log.log("\nNo Internet connection."); - // Use exit scopes to shutdown API - return EXIT_FAILURE; - } - } - - // if sync list is configured, set to true now that the sync engine is initialised - if (syncListConfigured) { - sync.setSyncListConfigured(); - } - - // 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(); - } - } - - // Do we configure to disable the upload validation routine - if (cfg.getValueBool("disable_upload_validation")) sync.setDisableUploadValidation(); - - // Do we configure to disable the download validation routine - if (cfg.getValueBool("disable_download_validation")) sync.setDisableDownloadValidation(); - - // Has the user enabled to bypass data preservation of renaming local files when there is a conflict? - if (cfg.getValueBool("bypass_data_preservation")) { - log.log("WARNING: Application has been configured to bypass local data preservation in the event of file conflict."); - log.log("WARNING: Local data loss MAY occur in this scenario."); - sync.setBypassDataPreservation(); - } - - // Do we configure to clean up local files if using --download-only ? - if ((cfg.getValueBool("download_only")) && (cfg.getValueBool("cleanup_local_files"))) { - // --download-only and --cleanup-local-files were passed in - log.log("WARNING: Application has been configured to cleanup local files that are not present online."); - log.log("WARNING: Local data loss MAY occur in this scenario if you are expecting data to remain archived locally."); - sync.setCleanupLocalFiles(); - // Set the global flag as we will use this as thhe item to be passed into the sync function below - cleanupLocalFilesGlobal = true; - } - - // Are we configured to use a National Cloud Deployment - if (cfg.getValueString("azure_ad_endpoint") != "") { - // value is configured, is it a valid value? - if ((cfg.getValueString("azure_ad_endpoint") == "USL4") || (cfg.getValueString("azure_ad_endpoint") == "USL5") || (cfg.getValueString("azure_ad_endpoint") == "DE") || (cfg.getValueString("azure_ad_endpoint") == "CN")) { - // valid entries to flag we are using a National Cloud Deployment - // National Cloud Deployments do not support /delta as a query - // https://docs.microsoft.com/en-us/graph/deployments#supported-features - // Flag that we have a valid National Cloud Deployment that cannot use /delta queries - sync.setNationalCloudDeployment(); - } - } - - // Are we forcing to use /children scan instead of /delta to simulate National Cloud Deployment use of /children? - if (cfg.getValueBool("force_children_scan")) { - log.log("Forcing client to use /children scan rather than /delta to simulate National Cloud Deployment use of /children"); - sync.setNationalCloudDeployment(); - } - - // Do we need to display the function processing timing - if (cfg.getValueBool("display_processing_time")) { - log.log("Forcing client to display function processing times"); - sync.setPerformanceProcessingOutput(); - } - - // Do we need to validate the syncDir to check for the presence of a '.nosync' file - if (cfg.getValueBool("check_nomount")) { - // we were asked to check the mounts - if (exists(syncDir ~ "/.nosync")) { - log.logAndNotify("ERROR: .nosync file found. Aborting synchronization process to safeguard data."); - // Use exit scopes to shutdown API - return EXIT_FAILURE; - } - } - - // Do we need to create or remove a directory? - if ((cfg.getValueString("create_directory") != "") || (cfg.getValueString("remove_directory") != "")) { - // create directory - if (cfg.getValueString("create_directory") != "") { - // create a directory on OneDrive - sync.createDirectoryNoSync(cfg.getValueString("create_directory")); - } - //remove directory - if (cfg.getValueString("remove_directory") != "") { - // remove a directory on OneDrive - sync.deleteDirectoryNoSync(cfg.getValueString("remove_directory")); - } - } - - // Are we renaming or moving a directory? - if ((cfg.getValueString("source_directory") != "") && (cfg.getValueString("destination_directory") != "")) { - // We are renaming or moving a directory - sync.renameDirectoryNoSync(cfg.getValueString("source_directory"), cfg.getValueString("destination_directory")); - } - - // Are we obtaining the Office 365 Drive ID for a given Office 365 SharePoint Shared Library? - if (cfg.getValueString("get_o365_drive_id") != "") { - sync.querySiteCollectionForDriveID(cfg.getValueString("get_o365_drive_id")); - // Exit application - // Use exit scopes to shutdown API and cleanup data - return EXIT_SUCCESS; - } - - // --create-share-link - Are we createing a shareable link for an existing file on OneDrive? - if (cfg.getValueString("create_share_link") != "") { - // Query OneDrive for the file, and if valid, create a shareable link for the file - - // By default, the shareable link will be read-only. - // If the user adds: - // --with-editing-perms - // this will create a writeable link - bool writeablePermissions = cfg.getValueBool("with_editing_perms"); - sync.createShareableLinkForFile(cfg.getValueString("create_share_link"), writeablePermissions); - - // Exit application - // Use exit scopes to shutdown API - return EXIT_SUCCESS; - } - - // --get-file-link - Are we obtaining the URL path for a synced file? - if (cfg.getValueString("get_file_link") != "") { - // Query OneDrive for the file link - sync.queryOneDriveForFileDetails(cfg.getValueString("get_file_link"), syncDir, "URL"); - // Exit application - // Use exit scopes to shutdown API - return EXIT_SUCCESS; - } - - // --modified-by - Are we listing the modified-by details of a provided path? - if (cfg.getValueString("modified_by") != "") { - // Query OneDrive for the file link - sync.queryOneDriveForFileDetails(cfg.getValueString("modified_by"), syncDir, "ModifiedBy"); - // Exit application - // Use exit scopes to shutdown API - return EXIT_SUCCESS; - } - - // Are we listing OneDrive Business Shared Folders - if (cfg.getValueBool("list_business_shared_folders")) { - // Is this a business account type? - if (sync.getAccountType() == "business"){ - // List OneDrive Business Shared Folders - sync.listOneDriveBusinessSharedFolders(); - } else { - log.error("ERROR: Unsupported account type for listing OneDrive Business Shared Folders"); - } - // Exit application - // Use exit scopes to shutdown API - return EXIT_SUCCESS; - } - - // Are we going to sync OneDrive Business Shared Folders - if (cfg.getValueBool("sync_business_shared_folders")) { - // Is this a business account type? - if (sync.getAccountType() == "business"){ - // Configure flag to sync business folders - sync.setSyncBusinessFolders(); - } else { - log.error("ERROR: Unsupported account type for syncing OneDrive Business Shared Folders"); - } - } - - // Ensure that the value stored for cfg.getValueString("single_directory") does not contain any extra quotation marks - if (cfg.getValueString("single_directory") != ""){ - string originalSingleDirectoryValue = cfg.getValueString("single_directory"); - // Strip quotation marks from provided path to ensure no issues within a Docker environment when using passed in values - string updatedSingleDirectoryValue = strip(originalSingleDirectoryValue, "\""); - cfg.setValueString("single_directory", updatedSingleDirectoryValue); - } - - // Are we displaying the sync status of the client? - if (cfg.getValueBool("display_sync_status")) { - string remotePath = "/"; - // Are we doing a single directory check? - if (cfg.getValueString("single_directory") != ""){ - // Need two different path strings here - remotePath = cfg.getValueString("single_directory"); - } - sync.queryDriveForChanges(remotePath); - } - - // Are we performing a sync, or monitor operation? - if ((cfg.getValueBool("synchronize")) || (cfg.getValueBool("monitor"))) { - // Initialise the monitor class, so that we can do more granular inotify handling when performing the actual sync - // needed for --synchronize and --monitor handling - Monitor m = new Monitor(selectiveSync); - - if (cfg.getValueBool("synchronize")) { - if (online) { - // set flag for exit scope - synchronizeConfigured = true; - - // Check user entry for local path - the above chdir means we are already in ~/OneDrive/ thus singleDirectory is local to this path - if (cfg.getValueString("single_directory") != "") { - // Does the directory we want to sync actually exist? - if (!exists(cfg.getValueString("single_directory"))) { - // The requested 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 --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, cleanupLocalFilesGlobal); - - // 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(); - } - } - - if (cfg.getValueBool("monitor")) { - log.logAndNotify("Initializing monitor ..."); - log.log("OneDrive monitor interval (seconds): ", cfg.getValueLong("monitor_interval")); - - m.onDirCreated = delegate(string path) { - // Handle .folder creation if skip_dotfiles is enabled - if ((cfg.getValueBool("skip_dotfiles")) && (selectiveSync.isDotFile(path))) { - log.vlog("[M] Skipping watching local path - .folder found & --skip-dot-files enabled: ", path); - } else { - log.vlog("[M] Local directory created: ", path); - try { - sync.scanForDifferences(path); - } catch (CurlException e) { - log.vlog("Offline, cannot create remote dir!"); - } catch(Exception e) { - log.logAndNotify("Cannot create remote directory: ", e.msg); - } - } - }; - m.onFileChanged = delegate(string path) { - log.vlog("[M] Local file changed: ", path); - try { - sync.scanForDifferences(path); - } catch (CurlException e) { - log.vlog("Offline, cannot upload changed item!"); - } catch(Exception e) { - log.logAndNotify("Cannot upload file changes/creation: ", e.msg); - } - }; - m.onDelete = delegate(string path) { - log.log("Received inotify delete event from operating system .. attempting item deletion as requested"); - log.vlog("[M] Local item deleted: ", path); - try { - sync.deleteByPath(path); - } catch (CurlException e) { - log.vlog("Offline, cannot delete item!"); - } catch(SyncException e) { - if (e.msg == "The item to delete is not in the local database") { - log.vlog("Item cannot be deleted from OneDrive because it was not found in the local database"); - } else { - log.logAndNotify("Cannot delete remote item: ", e.msg); - } - } catch(Exception e) { - log.logAndNotify("Cannot delete remote item: ", e.msg); - } - }; - m.onMove = delegate(string from, string to) { - log.vlog("[M] Local item moved: ", from, " -> ", to); - try { - // Handle .folder -> folder if skip_dotfiles is enabled - if ((cfg.getValueBool("skip_dotfiles")) && (selectiveSync.isDotFile(from))) { - // .folder -> folder handling - has to be handled as a new folder - sync.scanForDifferences(to); - } else { - sync.uploadMoveItem(from, to); - } - } catch (CurlException e) { - log.vlog("Offline, cannot move item!"); - } catch(Exception e) { - log.logAndNotify("Cannot move item: ", e.msg); - } - }; - signal(SIGINT, &exitHandler); - signal(SIGTERM, &exitHandler); - - // attempt to initialise monitor class - if (!cfg.getValueBool("download_only")) { - try { - m.init(cfg, cfg.getValueLong("verbose") > 0, cfg.getValueBool("skip_symlinks"), cfg.getValueBool("check_nosync")); - } catch (MonitorException e) { - // monitor initialisation failed - log.error("ERROR: ", e.msg); - oneDrive.shutdown(); - return EXIT_FAILURE; - } - } - - // monitor loop - bool performMonitor = true; - ulong monitorLoopFullCount = 0; - immutable auto checkInterval = dur!"seconds"(cfg.getValueLong("monitor_interval")); - immutable auto githubCheckInterval = dur!"seconds"(86400); - immutable long logInterval = cfg.getValueLong("monitor_log_frequency"); - immutable long fullScanFrequency = cfg.getValueLong("monitor_fullscan_frequency"); - MonoTime lastCheckTime = MonoTime.currTime(); - MonoTime lastGitHubCheckTime = MonoTime.currTime(); - - long logMonitorCounter = 0; - long fullScanCounter = 0; - // set fullScanRequired to true so that at application startup we perform a full walk - bool fullScanRequired = true; - bool syncListConfiguredFullScanOverride = false; - // if sync list is configured, set to true - if (syncListConfigured) { - // sync list is configured - syncListConfiguredFullScanOverride = true; - } - immutable bool webhookEnabled = cfg.getValueBool("webhook_enabled"); - - while (performMonitor) { - if (!cfg.getValueBool("download_only")) { - try { - m.update(online); - } catch (MonitorException e) { - // Catch any exceptions thrown by inotify / monitor engine - log.error("ERROR: The following inotify error was generated: ", e.msg); - } - } - - // Check for notifications pushed from Microsoft to the webhook - bool notificationReceived = false; - if (webhookEnabled) { - // Create a subscription on the first run, or renew the subscription - // on subsequent runs when it is about to expire. - oneDrive.createOrRenewSubscription(); - - // Process incoming notifications if any. - - // Empirical evidence shows that Microsoft often sends multiple - // notifications for one single change, so we need a loop to exhaust - // all signals that were queued up by the webhook. The notifications - // do not contain any actual changes, and we will always rely do the - // delta endpoint to sync to latest. Therefore, only one sync run is - // good enough to catch up for multiple notifications. - for (int signalCount = 0;; signalCount++) { - const auto signalExists = receiveTimeout(dur!"seconds"(-1), (ulong _) {}); - if (signalExists) { - notificationReceived = true; - } else { - if (notificationReceived) { - log.log("Received ", signalCount," refresh signals from the webhook"); - } - break; - } - } - } - - auto currTime = MonoTime.currTime(); - // has monitor_interval elapsed or are we at application startup / monitor startup? - // in a --resync scenario, if we have not 're-populated' the database, valid changes will get skipped: - // Monitor directory: ./target - // Monitor directory: target/2eVPInOMTFNXzRXeNMEoJch5OR9XpGby - // [M] Item moved: random_files/2eVPInOMTFNXzRXeNMEoJch5OR9XpGby -> target/2eVPInOMTFNXzRXeNMEoJch5OR9XpGby - // Moving random_files/2eVPInOMTFNXzRXeNMEoJch5OR9XpGby to target/2eVPInOMTFNXzRXeNMEoJch5OR9XpGby - // Skipping uploading this new file as parent path is not in the database: target/2eVPInOMTFNXzRXeNMEoJch5OR9XpGby - // 'target' should be in the DB, it should also exist online, but because of --resync, it does not exist in the database thus parent check fails - if (notificationReceived || (currTime - lastCheckTime > checkInterval) || (monitorLoopFullCount == 0)) { - // Check Application Version against GitHub once per day - if (currTime - lastGitHubCheckTime > githubCheckInterval) { - // --monitor GitHub Application Version Check time expired - checkApplicationVersion(); - // update when we have performed this check - lastGitHubCheckTime = MonoTime.currTime(); - } - - // monitor sync loop - logOutputMessage = "################################################## NEW LOOP ##################################################"; - if (displaySyncOptions) { - log.log(logOutputMessage); - } else { - log.vdebug(logOutputMessage); - } - // Increment monitorLoopFullCount - monitorLoopFullCount++; - // Display memory details at start of loop - if (displayMemoryUsage) { - log.displayMemoryUsagePreGC(); - } - - // log monitor output suppression - logMonitorCounter += 1; - if (logMonitorCounter > logInterval) { - logMonitorCounter = 1; - } - - // do we perform a full scan of sync_dir and database integrity check? - fullScanCounter += 1; - // fullScanFrequency = 'monitor_fullscan_frequency' from config - if (fullScanCounter > fullScanFrequency){ - // 'monitor_fullscan_frequency' counter has exceeded - fullScanCounter = 1; - // set fullScanRequired = true due to 'monitor_fullscan_frequency' counter has been exceeded - fullScanRequired = true; - // are we using sync_list? - if (syncListConfigured) { - // sync list is configured - syncListConfiguredFullScanOverride = true; - } - } - - if (displaySyncOptions) { - // sync option handling per sync loop - log.log("fullScanCounter = ", fullScanCounter); - log.log("syncListConfigured = ", syncListConfigured); - log.log("fullScanRequired = ", fullScanRequired); - log.log("syncListConfiguredFullScanOverride = ", syncListConfiguredFullScanOverride); - } else { - // sync option handling per sync loop via debug - log.vdebug("fullScanCounter = ", fullScanCounter); - log.vdebug("syncListConfigured = ", syncListConfigured); - log.vdebug("fullScanRequired = ", fullScanRequired); - log.vdebug("syncListConfiguredFullScanOverride = ", syncListConfiguredFullScanOverride); - } - - try { - if (!initSyncEngine(sync)) { - // Use exit scopes to shutdown API - return EXIT_FAILURE; - } - try { - // performance timing - SysTime startSyncProcessingTime = Clock.currTime(); - - // perform a --monitor sync - if ((cfg.getValueLong("verbose") > 0) || (logMonitorCounter == logInterval) || (fullScanRequired) ) { - // log to console and log file if enabled - if (cfg.getValueBool("display_processing_time")) { - log.log(startMessage, " ", startSyncProcessingTime); - } else { - log.log(startMessage); - } - } else { - // log file only if enabled so we know when a sync started when not using --verbose - log.fileOnly(startMessage); - } - performSync(sync, cfg.getValueString("single_directory"), cfg.getValueBool("download_only"), cfg.getValueBool("local_first"), cfg.getValueBool("upload_only"), (logMonitorCounter == logInterval ? MONITOR_LOG_QUIET : MONITOR_LOG_SILENT), fullScanRequired, syncListConfiguredFullScanOverride, displaySyncOptions, cfg.getValueBool("monitor"), m, cleanupLocalFilesGlobal); - if (!cfg.getValueBool("download_only")) { - // discard all events that may have been generated by the sync that have not already been handled - try { - m.update(false); - } catch (MonitorException e) { - // Catch any exceptions thrown by inotify / monitor engine - log.error("ERROR: The following inotify error was generated: ", e.msg); - } - } - SysTime endSyncProcessingTime = Clock.currTime(); - if ((cfg.getValueLong("verbose") > 0) || (logMonitorCounter == logInterval) || (fullScanRequired) ) { - // log to console and log file if enabled - if (cfg.getValueBool("display_processing_time")) { - log.log(finishMessage, " ", endSyncProcessingTime); - log.log("Elapsed Sync Time with OneDrive Service: ", (endSyncProcessingTime - startSyncProcessingTime)); - } else { - log.log(finishMessage); - } - } else { - // log file only if enabled so we know when a sync completed when not using --verbose - log.fileOnly(finishMessage); - } - } catch (CurlException e) { - // we already tried three times in the performSync routine - // if we still have problems, then the sync handle might have - // gone stale and we need to re-initialize the sync engine - log.log("Persistent connection errors, reinitializing connection"); - sync.reset(); - } - } catch (CurlException e) { - log.log("Cannot initialize connection to OneDrive"); - } - // performSync complete, set lastCheckTime to current time - lastCheckTime = MonoTime.currTime(); - - // Display memory details before cleanup - if (displayMemoryUsage) log.displayMemoryUsagePreGC(); - // Perform Garbage Cleanup - GC.collect(); - // Display memory details after cleanup - if (displayMemoryUsage) log.displayMemoryUsagePostGC(); - - // If we did a full scan, make sure we merge the conents of the WAL and SHM to disk - if (fullScanRequired) { - // 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(); - } - - // reset fullScanRequired and syncListConfiguredFullScanOverride - fullScanRequired = false; - if (syncListConfigured) syncListConfiguredFullScanOverride = false; - - // monitor loop complete - logOutputMessage = "################################################ LOOP COMPLETE ###############################################"; - - // Handle display options - if (displaySyncOptions) { - log.log(logOutputMessage); - } else { - log.vdebug(logOutputMessage); - } - // Developer break via config option - if (cfg.getValueLong("monitor_max_loop") > 0) { - // developer set option to limit --monitor loops - if (monitorLoopFullCount == (cfg.getValueLong("monitor_max_loop"))) { - performMonitor = false; - log.log("Exiting after ", monitorLoopFullCount, " loops due to developer set option"); - } - } - } - // Sleep the monitor thread for 1 second, loop around and pick up any inotify changes - Thread.sleep(dur!"seconds"(1)); - } - } - } - - // Exit application - // Use exit scopes to shutdown API - return EXIT_SUCCESS; -} - -void cleanupDryRunDatabase(string databaseFilePathDryRun) -{ - // cleanup dry-run data - log.vdebug("Running cleanupDryRunDatabase"); - string dryRunShmFile = databaseFilePathDryRun ~ "-shm"; - string dryRunWalFile = databaseFilePathDryRun ~ "-wal"; - if (exists(databaseFilePathDryRun)) { - // remove the file - log.vdebug("Removing items-dryrun.sqlite3 as dry run operations complete"); - // remove items-dryrun.sqlite3 - safeRemove(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); - } -} - -bool initSyncEngine(SyncEngine sync) -{ - try { - sync.init(); - } catch (OneDriveException e) { - if (e.httpStatusCode == 400 || e.httpStatusCode == 401) { - // Authorization is invalid - log.log("\nAuthorization token invalid, use --reauth to authorize the client again\n"); - return false; - } - if (e.httpStatusCode >= 500) { - // There was a HTTP 5xx Server Side Error, message already printed - return false; - } - } - return true; -} - -// try to synchronize the folder three times -void performSync(SyncEngine sync, string singleDirectory, bool downloadOnly, bool localFirst, bool uploadOnly, long logLevel, bool fullScanRequired, bool syncListConfiguredFullScanOverride, bool displaySyncOptions, bool monitorEnabled, Monitor m, bool cleanupLocalFiles) -{ - int count; - string remotePath = "/"; - string localPath = "."; - string logOutputMessage; - - // performSync API scan triggers - log.vdebug("performSync API scan triggers"); - log.vdebug("-----------------------------"); - log.vdebug("fullScanRequired = ", fullScanRequired); - log.vdebug("syncListConfiguredFullScanOverride = ", syncListConfiguredFullScanOverride); - log.vdebug("-----------------------------"); - - // Are we doing a single directory sync? - if (singleDirectory != ""){ - // Need two different path strings here - remotePath = singleDirectory; - localPath = singleDirectory; - // Set flag for singleDirectoryScope for change handling - sync.setSingleDirectoryScope(); - } - - // Due to Microsoft Sharepoint 'enrichment' of files, we try to download the Microsoft modified file automatically - // Set flag if we are in upload only state to handle this differently - // See: https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details - if (uploadOnly) sync.setUploadOnly(); - - do { - try { - // starting a sync - logOutputMessage = "################################################## NEW SYNC ##################################################"; - if (displaySyncOptions) { - log.log(logOutputMessage); - } else { - log.vdebug(logOutputMessage); - } - if (singleDirectory != ""){ - // we were requested to sync a single directory - log.vlog("Syncing changes from this selected path: ", singleDirectory); - if (uploadOnly){ - // Upload Only of selected single directory - if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from selected local path only - NOT syncing data changes from OneDrive ..."); - sync.scanForDifferences(localPath); - } else { - // No upload only - if (localFirst) { - // Local First - if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from selected local path first before downloading changes from OneDrive ..."); - sync.scanForDifferences(localPath); - sync.applyDifferencesSingleDirectory(remotePath); - } else { - // OneDrive First - if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from selected OneDrive path ..."); - sync.applyDifferencesSingleDirectory(remotePath); - - // Is this a --download-only --cleanup-local-files request? - // If yes, scan for local changes - but --cleanup-local-files is being used, a further flag will trigger local file deletes rather than attempt to upload files to OneDrive - if (cleanupLocalFiles) { - // --download-only and --cleanup-local-files were passed in - log.log("Searching local filesystem for extra files and folders which need to be removed"); - sync.scanForDifferencesFilesystemScan(localPath); - } else { - // is this a --download-only request? - if (!downloadOnly) { - // process local changes - sync.scanForDifferences(localPath); - // ensure that the current remote state is updated locally - sync.applyDifferencesSingleDirectory(remotePath); - } - } - } - } - } else { - // no single directory sync - if (uploadOnly){ - // Upload Only of entire sync_dir - if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from local path only - NOT syncing data changes from OneDrive ..."); - sync.scanForDifferences(localPath); - } else { - // No upload only - string syncCallLogOutput; - if (localFirst) { - // sync local files first before downloading from OneDrive - if (logLevel < MONITOR_LOG_QUIET) log.log("Syncing changes from local path first before downloading changes from OneDrive ..."); - sync.scanForDifferences(localPath); - // if syncListConfiguredFullScanOverride = true - if (syncListConfiguredFullScanOverride) { - // perform a full walk of OneDrive objects - sync.applyDifferences(syncListConfiguredFullScanOverride); - } else { - // perform a walk based on if a full scan is required - sync.applyDifferences(fullScanRequired); - } - } else { - // sync from OneDrive first before uploading files to OneDrive - if ((logLevel < MONITOR_LOG_SILENT) || (fullScanRequired)) log.log("Syncing changes and items from OneDrive ..."); - - // For the initial sync, always use the delta link so that we capture all the right delta changes including adds, moves & deletes - logOutputMessage = "Initial Scan: Call OneDrive Delta API for delta changes as compared to last successful sync."; - syncCallLogOutput = "Calling sync.applyDifferences(false);"; - if (displaySyncOptions) { - log.log(logOutputMessage); - log.log(syncCallLogOutput); - } else { - log.vdebug(logOutputMessage); - log.vdebug(syncCallLogOutput); - } - sync.applyDifferences(false); - - // Is this a --download-only --cleanup-local-files request? - // If yes, scan for local changes - but --cleanup-local-files is being used, a further flag will trigger local file deletes rather than attempt to upload files to OneDrive - if (cleanupLocalFiles) { - // --download-only and --cleanup-local-files were passed in - log.log("Searching local filesystem for extra files and folders which need to be removed"); - sync.scanForDifferencesFilesystemScan(localPath); - } else { - // is this a --download-only request? - if (!downloadOnly) { - // process local changes walking the entire path checking for changes - // in monitor mode all local changes are captured via inotify - // thus scanning every 'monitor_interval' (default 300 seconds) for local changes is excessive and not required - logOutputMessage = "Process local filesystem (sync_dir) for file changes as compared to database entries"; - syncCallLogOutput = "Calling sync.scanForDifferences(localPath);"; - if (displaySyncOptions) { - log.log(logOutputMessage); - log.log(syncCallLogOutput); - } else { - log.vdebug(logOutputMessage); - log.vdebug(syncCallLogOutput); - } - - SysTime startIntegrityCheckProcessingTime = Clock.currTime(); - if (sync.getPerformanceProcessingOutput()) { - // performance timing for DB and file system integrity check - start - writeln("============================================================"); - writeln("Start Integrity Check Processing Time: ", startIntegrityCheckProcessingTime); - } - - // What sort of local scan do we want to do? - // In --monitor mode, when performing the DB scan, a race condition occurs where by if a file or folder is moved during this process - // the inotify event is discarded once performSync() is finished (see m.update(false) above), so these events need to be handled - // This can be remediated by breaking the DB and file system scan into separate processes, and handing any applicable inotify events in between - if (!monitorEnabled) { - // --synchronize in use - log.log("Performing a database consistency and integrity check on locally stored data ... "); - // standard process flow - sync.scanForDifferences(localPath); - } else { - // --monitor in use - // Use individual calls with inotify checks between to avoid a race condition between these 2 functions - // Database scan integrity check to compare DB data vs actual content on disk to ensure what we think is local, is local - // and that the data 'hash' as recorded in the DB equals the hash of the actual content - // This process can be extremely expensive time and CPU processing wise - // - // fullScanRequired is set to TRUE when the application starts up, or the config option 'monitor_fullscan_frequency' count is reached - // By default, 'monitor_fullscan_frequency' = 12, and 'monitor_interval' = 300, meaning that by default, a full database consistency check - // is done once an hour. - // - // To change this behaviour adjust 'monitor_interval' and 'monitor_fullscan_frequency' to desired values in the application config file - if (fullScanRequired) { - log.log("Performing a database consistency and integrity check on locally stored data due to fullscan requirement ... "); - sync.scanForDifferencesDatabaseScan(localPath); - // handle any inotify events that occured 'whilst' we were scanning the database - m.update(true); - } else { - log.vdebug("NOT performing Database Integrity Check .. fullScanRequired = FALSE"); - m.update(true); - } - - // Filesystem walk to find new files not uploaded - log.vdebug("Searching local filesystem for new data"); - sync.scanForDifferencesFilesystemScan(localPath); - // handle any inotify events that occured 'whilst' we were scanning the local filesystem - m.update(true); - } - - SysTime endIntegrityCheckProcessingTime = Clock.currTime(); - if (sync.getPerformanceProcessingOutput()) { - // performance timing for DB and file system integrity check - finish - writeln("End Integrity Check Processing Time: ", endIntegrityCheckProcessingTime); - writeln("Elapsed Function Processing Time: ", (endIntegrityCheckProcessingTime - startIntegrityCheckProcessingTime)); - writeln("============================================================"); - } - - // At this point, all OneDrive changes / local changes should be uploaded and in sync - // This MAY not be the case when using sync_list, thus a full walk of OneDrive ojects is required - - // --synchronize & no sync_list : fullScanRequired = false, syncListConfiguredFullScanOverride = false - // --synchronize & sync_list in use : fullScanRequired = false, syncListConfiguredFullScanOverride = true - - // --monitor loops around 12 iterations. On the 1st loop, sets fullScanRequired = true, syncListConfiguredFullScanOverride = true if requried - - // --monitor & no sync_list (loop #1) : fullScanRequired = true, syncListConfiguredFullScanOverride = false - // --monitor & no sync_list (loop #2 - #12) : fullScanRequired = false, syncListConfiguredFullScanOverride = false - // --monitor & sync_list in use (loop #1) : fullScanRequired = true, syncListConfiguredFullScanOverride = true - // --monitor & sync_list in use (loop #2 - #12) : fullScanRequired = false, syncListConfiguredFullScanOverride = false - - // Do not perform a full walk of the OneDrive objects - if ((!fullScanRequired) && (!syncListConfiguredFullScanOverride)){ - logOutputMessage = "Final True-Up: Do not perform a full walk of the OneDrive objects - not required"; - syncCallLogOutput = "Calling sync.applyDifferences(false);"; - if (displaySyncOptions) { - log.log(logOutputMessage); - log.log(syncCallLogOutput); - } else { - log.vdebug(logOutputMessage); - log.vdebug(syncCallLogOutput); - } - sync.applyDifferences(false); - } - - // Perform a full walk of OneDrive objects because sync_list is in use / or trigger was set in --monitor loop - if ((!fullScanRequired) && (syncListConfiguredFullScanOverride)){ - logOutputMessage = "Final True-Up: Perform a full walk of OneDrive objects because sync_list is in use / or trigger was set in --monitor loop"; - syncCallLogOutput = "Calling sync.applyDifferences(true);"; - if (displaySyncOptions) { - log.log(logOutputMessage); - log.log(syncCallLogOutput); - } else { - log.vdebug(logOutputMessage); - log.vdebug(syncCallLogOutput); - } - sync.applyDifferences(true); - } - - // Perform a full walk of OneDrive objects because a full scan was required - if ((fullScanRequired) && (!syncListConfiguredFullScanOverride)){ - logOutputMessage = "Final True-Up: Perform a full walk of OneDrive objects because a full scan was required"; - syncCallLogOutput = "Calling sync.applyDifferences(true);"; - if (displaySyncOptions) { - log.log(logOutputMessage); - log.log(syncCallLogOutput); - } else { - log.vdebug(logOutputMessage); - log.vdebug(syncCallLogOutput); - } - sync.applyDifferences(true); - } - - // Perform a full walk of OneDrive objects because a full scan was required and sync_list is in use and trigger was set in --monitor loop - if ((fullScanRequired) && (syncListConfiguredFullScanOverride)){ - logOutputMessage = "Final True-Up: Perform a full walk of OneDrive objects because a full scan was required and sync_list is in use and trigger was set in --monitor loop"; - syncCallLogOutput = "Calling sync.applyDifferences(true);"; - if (displaySyncOptions) { - log.log(logOutputMessage); - log.log(syncCallLogOutput); - } else { - log.vdebug(logOutputMessage); - log.vdebug(syncCallLogOutput); - } - sync.applyDifferences(true); - } - } - } - } - } - } - - // sync is complete - logOutputMessage = "################################################ SYNC COMPLETE ###############################################"; - if (displaySyncOptions) { - log.log(logOutputMessage); - } else { - log.vdebug(logOutputMessage); - } - - count = -1; - } catch (Exception e) { - if (++count == 3) { - log.log("Giving up on sync after three attempts: ", e.msg); - throw e; - } else - log.log("Retry sync count: ", count, ": ", e.msg); - } - } while (count != -1); -} - -// getting around the @nogc problem -// https://p0nce.github.io/d-idioms/#Bypassing-@nogc -auto assumeNoGC(T) (T t) if (isFunctionPointer!T || isDelegate!T) -{ - enum attrs = functionAttributes!T | FunctionAttribute.nogc; - return cast(SetFunctionAttributes!(T, functionLinkage!T, attrs)) t; -} - -extern(C) nothrow @nogc @system void exitHandler(int value) { - try { - assumeNoGC ( () { - log.log("Got termination signal, performing clean up"); - // if initialised, shut down the HTTP instance - if (onedriveInitialised) { - log.log("Shutting down the HTTP instance"); - oneDrive.shutdown(); - } - // was itemDb initialised? - if (itemDb.isDatabaseInitialised()) { - // Make sure the .wal file is incorporated into the main db before we exit - log.log("Shutting down db connection and merging temporary data"); - itemDb.performVacuum(); - destroy(itemDb); - } - })(); - } catch(Exception e) {} - exit(0); -} - diff --git a/src/monitor.d b/src/monitor.d deleted file mode 100644 index 06aac0d7..00000000 --- a/src/monitor.d +++ /dev/null @@ -1,391 +0,0 @@ -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; -import core.stdc.stdlib; -import config; -import selective; -import util; -static import log; - -// relevant inotify events -private immutable uint32_t mask = IN_CLOSE_WRITE | IN_CREATE | IN_DELETE | IN_MOVE | IN_IGNORED | IN_Q_OVERFLOW; - -class MonitorException: ErrnoException -{ - @safe this(string msg, string file = __FILE__, size_t line = __LINE__) - { - super(msg, file, line); - } -} - -final class Monitor -{ - bool verbose; - // inotify file descriptor - private int fd; - // map every inotify watch descriptor to its directory - private string[int] wdToDirName; - // map the inotify cookies of move_from events to their path - private string[int] cookieToPath; - // buffer to receive the inotify events - private void[] buffer; - // skip symbolic links - bool skip_symlinks; - // check for .nosync if enabled - bool check_nosync; - - private SelectiveSync selectiveSync; - - void delegate(string path) onDirCreated; - void delegate(string path) onFileChanged; - void delegate(string path) onDelete; - void delegate(string from, string to) onMove; - - this(SelectiveSync selectiveSync) - { - assert(selectiveSync); - this.selectiveSync = selectiveSync; - } - - void init(Config cfg, bool verbose, bool skip_symlinks, bool check_nosync) - { - this.verbose = verbose; - this.skip_symlinks = skip_symlinks; - this.check_nosync = check_nosync; - - assert(onDirCreated && onFileChanged && onDelete && onMove); - fd = inotify_init(); - if (fd < 0) throw new MonitorException("inotify_init failed"); - if (!buffer) buffer = new void[4096]; - - // from which point do we start watching for changes? - string monitorPath; - if (cfg.getValueString("single_directory") != ""){ - // single directory in use, monitor only this - monitorPath = "./" ~ cfg.getValueString("single_directory"); - } else { - // default - monitorPath = "."; - } - addRecursive(monitorPath); - } - - void shutdown() - { - if (fd > 0) close(fd); - wdToDirName = null; - } - - private void addRecursive(string dirname) - { - // skip non existing/disappeared items - if (!exists(dirname)) { - log.vlog("Not adding non-existing/disappeared directory: ", dirname); - return; - } - - // Skip the monitoring of any user filtered items - if (dirname != ".") { - // Is the directory name a match to a skip_dir entry? - // The path that needs to be checked needs to include the '/' - // This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched - if (isDir(dirname)) { - if (selectiveSync.isDirNameExcluded(dirname.strip('.'))) { - // dont add a watch for this item - log.vdebug("Skipping monitoring due to skip_dir match: ", dirname); - return; - } - } - if (isFile(dirname)) { - // Is the filename a match to a skip_file entry? - // 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 (selectiveSync.isFileNameExcluded(dirname.strip('.'))) { - // dont add a watch for this item - log.vdebug("Skipping monitoring due to skip_file match: ", dirname); - return; - } - } - // is the path exluded by sync_list? - if (selectiveSync.isPathExcludedViaSyncList(buildNormalizedPath(dirname))) { - // dont add a watch for this item - log.vdebug("Skipping monitoring due to sync_list match: ", dirname); - return; - } - } - - // skip symlinks if configured - if (isSymlink(dirname)) { - // if config says so we skip all symlinked items - if (skip_symlinks) { - // dont add a watch for this directory - return; - } - } - - // Do we need to check for .nosync? Only if check_nosync is true - if (check_nosync) { - if (exists(buildNormalizedPath(dirname) ~ "/.nosync")) { - log.vlog("Skipping watching path - .nosync found & --check-for-nosync enabled: ", buildNormalizedPath(dirname)); - return; - } - } - - // passed all potential exclusions - // add inotify watch for this path / directory / file - log.vdebug("Calling add() for this dirname: ", dirname); - add(dirname); - - // 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, getFunctionName!({})); - 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, getFunctionName!({})); - return; - } - } - } - } - - private void add(string pathname) - { - int wd = inotify_add_watch(fd, toStringz(pathname), mask); - if (wd < 0) { - if (errno() == ENOSPC) { - log.log("The user limit on the total number of inotify watches has been reached."); - log.log("To see the current max number of watches run:"); - log.log("sysctl fs.inotify.max_user_watches"); - log.log("To change the current max number of watches to 524288 run:"); - log.log("sudo sysctl fs.inotify.max_user_watches=524288"); - } - if (errno() == 13) { - if ((selectiveSync.getSkipDotfiles()) && (selectiveSync.isDotFile(pathname))) { - // no misleading output that we could not add a watch due to permission denied - return; - } else { - log.vlog("WARNING: inotify_add_watch failed - permission denied: ", pathname); - return; - } - } - // Flag any other errors - log.error("ERROR: inotify_add_watch failed: ", pathname); - return; - } - - // 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)) { - // This is a directory - // is the path exluded if skip_dotfiles configured and path is a .folder? - if ((selectiveSync.getSkipDotfiles()) && (selectiveSync.isDotFile(pathname))) { - // no misleading output that we are monitoring this directory - return; - } - // Log that this is directory is being monitored - log.vlog("Monitor directory: ", pathname); - } - } - - // remove a watch descriptor - private void remove(int wd) - { - assert(wd in wdToDirName); - int ret = inotify_rm_watch(fd, wd); - if (ret < 0) throw new MonitorException("inotify_rm_watch failed"); - log.vlog("Monitored directory removed: ", wdToDirName[wd]); - wdToDirName.remove(wd); - } - - // remove the watch descriptors associated to the given path - private void remove(const(char)[] path) - { - path ~= "/"; - foreach (wd, dirname; wdToDirName) { - if (dirname.startsWith(path)) { - int ret = inotify_rm_watch(fd, wd); - if (ret < 0) throw new MonitorException("inotify_rm_watch failed"); - wdToDirName.remove(wd); - log.vlog("Monitored directory removed: ", dirname); - } - } - } - - // return the file path from an inotify event - private string getPath(const(inotify_event)* event) - { - string path = wdToDirName[event.wd]; - if (event.len > 0) path ~= fromStringz(event.name.ptr); - log.vdebug("inotify path event for: ", path); - return path; - } - - void update(bool useCallbacks = true) - { - pollfd fds = { - fd: fd, - events: POLLIN - }; - - while (true) { - int ret = poll(&fds, 1, 0); - if (ret == -1) throw new MonitorException("poll failed"); - else if (ret == 0) break; // no events available - - size_t length = read(fd, buffer.ptr, buffer.length); - if (length == -1) throw new MonitorException("read failed"); - - int i = 0; - while (i < length) { - inotify_event *event = cast(inotify_event*) &buffer[i]; - string path; - string evalPath; - // inotify event debug - log.vdebug("inotify event wd: ", event.wd); - log.vdebug("inotify event mask: ", event.mask); - log.vdebug("inotify event cookie: ", event.cookie); - log.vdebug("inotify event len: ", event.len); - log.vdebug("inotify event name: ", event.name); - if (event.mask & IN_ACCESS) log.vdebug("inotify event flag: IN_ACCESS"); - if (event.mask & IN_MODIFY) log.vdebug("inotify event flag: IN_MODIFY"); - if (event.mask & IN_ATTRIB) log.vdebug("inotify event flag: IN_ATTRIB"); - if (event.mask & IN_CLOSE_WRITE) log.vdebug("inotify event flag: IN_CLOSE_WRITE"); - if (event.mask & IN_CLOSE_NOWRITE) log.vdebug("inotify event flag: IN_CLOSE_NOWRITE"); - if (event.mask & IN_MOVED_FROM) log.vdebug("inotify event flag: IN_MOVED_FROM"); - if (event.mask & IN_MOVED_TO) log.vdebug("inotify event flag: IN_MOVED_TO"); - if (event.mask & IN_CREATE) log.vdebug("inotify event flag: IN_CREATE"); - if (event.mask & IN_DELETE) log.vdebug("inotify event flag: IN_DELETE"); - if (event.mask & IN_DELETE_SELF) log.vdebug("inotify event flag: IN_DELETE_SELF"); - if (event.mask & IN_MOVE_SELF) log.vdebug("inotify event flag: IN_MOVE_SELF"); - if (event.mask & IN_UNMOUNT) log.vdebug("inotify event flag: IN_UNMOUNT"); - if (event.mask & IN_Q_OVERFLOW) log.vdebug("inotify event flag: IN_Q_OVERFLOW"); - if (event.mask & IN_IGNORED) log.vdebug("inotify event flag: IN_IGNORED"); - if (event.mask & IN_CLOSE) log.vdebug("inotify event flag: IN_CLOSE"); - if (event.mask & IN_MOVE) log.vdebug("inotify event flag: IN_MOVE"); - if (event.mask & IN_ONLYDIR) log.vdebug("inotify event flag: IN_ONLYDIR"); - if (event.mask & IN_DONT_FOLLOW) log.vdebug("inotify event flag: IN_DONT_FOLLOW"); - if (event.mask & IN_EXCL_UNLINK) log.vdebug("inotify event flag: IN_EXCL_UNLINK"); - if (event.mask & IN_MASK_ADD) log.vdebug("inotify event flag: IN_MASK_ADD"); - if (event.mask & IN_ISDIR) log.vdebug("inotify event flag: IN_ISDIR"); - if (event.mask & IN_ONESHOT) log.vdebug("inotify event flag: IN_ONESHOT"); - if (event.mask & IN_ALL_EVENTS) log.vdebug("inotify event flag: IN_ALL_EVENTS"); - - // skip events that need to be ignored - if (event.mask & IN_IGNORED) { - // forget the directory associated to the watch descriptor - wdToDirName.remove(event.wd); - goto skip; - } else if (event.mask & IN_Q_OVERFLOW) { - throw new MonitorException("Inotify overflow, events missing"); - } - - // if the event is not to be ignored, obtain path - path = getPath(event); - // configure the skip_dir & skip skip_file comparison item - evalPath = path.strip('.'); - - // Skip events that should be excluded based on application configuration - // We cant use isDir or isFile as this information is missing from the inotify event itself - // Thus this causes a segfault when attempting to query this - https://github.com/abraunegg/onedrive/issues/995 - - // Based on the 'type' of event & object type (directory or file) check that path against the 'right' user exclusions - // Directory events should only be compared against skip_dir and file events should only be compared against skip_file - if (event.mask & IN_ISDIR) { - // The event in question contains IN_ISDIR event mask, thus highly likely this is an event on a directory - // This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched - if (selectiveSync.isDirNameExcluded(evalPath)) { - // The path to evaluate matches a path that the user has configured to skip - goto skip; - } - } else { - // The event in question missing the IN_ISDIR event mask, thus highly likely this is an event on a file - // This due to if the user has specified in skip_file an exclusive path: '/path/file' - that is what must be matched - if (selectiveSync.isFileNameExcluded(evalPath)) { - // The path to evaluate matches a file that the user has configured to skip - goto skip; - } - } - - // is the path, excluded via sync_list - if (selectiveSync.isPathExcludedViaSyncList(path)) { - // The path to evaluate matches a directory or file that the user has configured not to include in the sync - goto skip; - } - - // handle the inotify events - if (event.mask & IN_MOVED_FROM) { - log.vdebug("event IN_MOVED_FROM: ", path); - cookieToPath[event.cookie] = path; - } else if (event.mask & IN_MOVED_TO) { - log.vdebug("event IN_MOVED_TO: ", path); - if (event.mask & IN_ISDIR) addRecursive(path); - auto from = event.cookie in cookieToPath; - if (from) { - cookieToPath.remove(event.cookie); - if (useCallbacks) onMove(*from, path); - } else { - // item moved from the outside - if (event.mask & IN_ISDIR) { - if (useCallbacks) onDirCreated(path); - } else { - if (useCallbacks) onFileChanged(path); - } - } - } else if (event.mask & IN_CREATE) { - log.vdebug("event IN_CREATE: ", path); - if (event.mask & IN_ISDIR) { - addRecursive(path); - if (useCallbacks) onDirCreated(path); - } - } else if (event.mask & IN_DELETE) { - log.vdebug("event IN_DELETE: ", path); - if (useCallbacks) onDelete(path); - } else if ((event.mask & IN_CLOSE_WRITE) && !(event.mask & IN_ISDIR)) { - log.vdebug("event IN_CLOSE_WRITE and ...: ", path); - if (useCallbacks) onFileChanged(path); - } else { - log.vdebug("event unhandled: ", path); - assert(0); - } - - skip: - i += inotify_event.sizeof + event.len; - } - // assume that the items moved outside the watched directory have been deleted - foreach (cookie, path; cookieToPath) { - log.vdebug("deleting (post loop): ", path); - if (useCallbacks) onDelete(path); - remove(path); - cookieToPath.remove(cookie); - } - } - } -} diff --git a/src/notifications/README b/src/notifications/README deleted file mode 100644 index 7385cb31..00000000 --- a/src/notifications/README +++ /dev/null @@ -1,10 +0,0 @@ -The files in this directory have been obtained form the following places: - -dnotify.d - https://github.com/Dav1dde/dnotify/blob/master/dnotify.d - License: Creative Commons Zro 1.0 Universal - see https://github.com/Dav1dde/dnotify/blob/master/LICENSE - -notify.d - https://github.com/D-Programming-Deimos/libnotify/blob/master/deimos/notify/notify.d - License: GNU Lesser General Public License (LGPL) 2.1 or upwards, see file diff --git a/src/notifications/dnotify.d b/src/notifications/dnotify.d deleted file mode 100644 index 1cc09356..00000000 --- a/src/notifications/dnotify.d +++ /dev/null @@ -1,323 +0,0 @@ -module dnotify; - -private { - import std.string : toStringz; - import std.conv : to; - import std.traits : isPointer, isArray; - import std.variant : Variant; - import std.array : appender; - - import deimos.notify.notify; -} - -public import deimos.notify.notify : NOTIFY_EXPIRES_DEFAULT, NOTIFY_EXPIRES_NEVER, - NotifyUrgency; - - -version(NoPragma) { -} else { - pragma(lib, "notify"); - pragma(lib, "gmodule"); - pragma(lib, "glib-2.0"); -} - -extern (C) { - private void g_free(void* mem); - private void g_list_free(GList* glist); -} - -version(NoGdk) { -} else { - version(NoPragma) { - } else { - pragma(lib, "gdk_pixbuf"); - } - - private: - extern (C) { - GdkPixbuf* gdk_pixbuf_new_from_file(const(char)* filename, GError **error); - } -} - -class NotificationError : Exception { - string message; - GError* gerror; - - this(GError* gerror) { - this.message = to!(string)(gerror.message); - this.gerror = gerror; - - super(this.message); - } - - this(string message) { - this.message = message; - - super(message); - } -} - -bool check_availability() { - // notify_init might return without dbus server actually started - // try to check for running dbus server - char **ret_name; - char **ret_vendor; - char **ret_version; - char **ret_spec_version; - bool ret; - try { - return notify_get_server_info(ret_name, ret_vendor, ret_version, ret_spec_version); - } catch (NotificationError e) { - throw new NotificationError("Cannot find dbus server!"); - } -} - -void init(in char[] name) { - notify_init(name.toStringz()); -} - -alias notify_is_initted is_initted; -alias notify_uninit uninit; - -static this() { - init(__FILE__); -} - -static ~this() { - uninit(); -} - -string get_app_name() { - return to!(string)(notify_get_app_name()); -} - -void set_app_name(in char[] app_name) { - notify_set_app_name(app_name.toStringz()); -} - -string[] get_server_caps() { - auto result = appender!(string[])(); - - GList* list = notify_get_server_caps(); - if(list !is null) { - for(GList* c = list; c !is null; c = c.next) { - result.put(to!(string)(cast(char*)c.data)); - g_free(c.data); - } - - g_list_free(list); - } - - return result.data; -} - -struct ServerInfo { - string name; - string vendor; - string version_; - string spec_version; -} - -ServerInfo get_server_info() { - char* name; - char* vendor; - char* version_; - char* spec_version; - notify_get_server_info(&name, &vendor, &version_, &spec_version); - - scope(exit) { - g_free(name); - g_free(vendor); - g_free(version_); - g_free(spec_version); - } - - return ServerInfo(to!string(name), to!string(vendor), to!string(version_), to!string(spec_version)); -} - - -struct Action { - const(char[]) id; - const(char[]) label; - NotifyActionCallback callback; - void* user_ptr; -} - - -class Notification { - NotifyNotification* notify_notification; - - const(char)[] summary; - const(char)[] body_; - const(char)[] icon; - - bool closed = true; - - private int _timeout = NOTIFY_EXPIRES_DEFAULT; - const(char)[] _category; - NotifyUrgency _urgency; - GdkPixbuf* _image; - Variant[const(char)[]] _hints; - const(char)[] _app_name; - Action[] _actions; - - this(in char[] summary, in char[] body_, in char[] icon="") - in { assert(is_initted(), "call dnotify.init() before using Notification"); } - do { - this.summary = summary; - this.body_ = body_; - this.icon = icon; - notify_notification = notify_notification_new(summary.toStringz(), body_.toStringz(), icon.toStringz()); - } - - bool update(in char[] summary, in char[] body_, in char[] icon="") { - this.summary = summary; - this.body_ = body_; - this.icon = icon; - return notify_notification_update(notify_notification, summary.toStringz(), body_.toStringz(), icon.toStringz()); - } - - void show() { - GError* ge; - - if(!notify_notification_show(notify_notification, &ge)) { - throw new NotificationError(ge); - } - } - - @property int timeout() { return _timeout; } - @property void timeout(int timeout) { - this._timeout = timeout; - notify_notification_set_timeout(notify_notification, timeout); - } - - @property const(char[]) category() { return _category; } - @property void category(in char[] category) { - this._category = category; - notify_notification_set_category(notify_notification, category.toStringz()); - } - - @property NotifyUrgency urgency() { return _urgency; } - @property void urgency(NotifyUrgency urgency) { - this._urgency = urgency; - notify_notification_set_urgency(notify_notification, urgency); - } - - - void set_image(GdkPixbuf* pixbuf) { - notify_notification_set_image_from_pixbuf(notify_notification, pixbuf); - //_image = pixbuf; - } - - version(NoGdk) { - } else { - void set_image(in char[] filename) { - GError* ge; - // TODO: free pixbuf - GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file(filename.toStringz(), &ge); - - if(pixbuf is null) { - if(ge is null) { - throw new NotificationError("Unable to load file: " ~ filename.idup); - } else { - throw new NotificationError(ge); - } - } - assert(notify_notification !is null); - notify_notification_set_image_from_pixbuf(notify_notification, pixbuf); // TODO: fix segfault - //_image = pixbuf; - } - } - - @property GdkPixbuf* image() { return _image; } - - // using deprecated set_hint_* functions (GVariant is an opaque structure, which needs the glib) - void set_hint(T)(in char[] key, T value) { - static if(is(T == int)) { - notify_notification_set_hint_int32(notify_notification, key, value); - } else static if(is(T == uint)) { - notify_notification_set_hint_uint32(notify_notification, key, value); - } else static if(is(T == double)) { - notify_notification_set_hint_double(notify_notification, key, value); - } else static if(is(T : const(char)[])) { - notify_notification_set_hint_string(notify_notification, key, value.toStringz()); - } else static if(is(T == ubyte)) { - notify_notification_set_hint_byte(notify_notification, key, value); - } else static if(is(T == ubyte[])) { - notify_notification_set_hint_byte_array(notify_notification, key, value.ptr, value.length); - } else { - static assert(false, "unsupported value for Notification.set_hint"); - } - - _hints[key] = Variant(value); - } - - // unset hint? - - Variant get_hint(in char[] key) { - return _hints[key]; - } - - @property const(char)[] app_name() { return _app_name; } - @property void app_name(in char[] name) { - this._app_name = app_name; - notify_notification_set_app_name(notify_notification, app_name.toStringz()); - } - - void add_action(T)(in char[] action, in char[] label, NotifyActionCallback callback, T user_data) { - static if(isPointer!T) { - void* user_ptr = cast(void*)user_data; - } else static if(isArray!T) { - void* user_ptr = cast(void*)user_data.ptr; - } else { - void* user_ptr = cast(void*)&user_data; - } - - notify_notification_add_action(notify_notification, action.toStringz(), label.toStringz(), - callback, user_ptr, null); - - _actions ~= Action(action, label, callback, user_ptr); - } - - void add_action()(Action action) { - notify_notification_add_action(notify_notification, action.id.toStringz(), action.label.toStringz(), - action.callback, action.user_ptr, null); - - _actions ~= action; - } - - @property Action[] actions() { return _actions; } - - void clear_actions() { - notify_notification_clear_actions(notify_notification); - } - - void close() { - GError* ge; - - if(!notify_notification_close(notify_notification, &ge)) { - throw new NotificationError(ge); - } - } - - @property int closed_reason() { - return notify_notification_get_closed_reason(notify_notification); - } -} - - -version(TestMain) { - import std.stdio; - - void main() { - writeln(get_app_name()); - set_app_name("bla"); - writeln(get_app_name()); - writeln(get_server_caps()); - writeln(get_server_info()); - - auto n = new Notification("foo", "bar", "notification-message-im"); - n.timeout = 3; - n.show(); - } -} diff --git a/src/notifications/notify.d b/src/notifications/notify.d deleted file mode 100644 index c549e397..00000000 --- a/src/notifications/notify.d +++ /dev/null @@ -1,195 +0,0 @@ -/** - * Copyright (C) 2004-2006 Christian Hammond - * Copyright (C) 2010 Red Hat, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - */ - -module deimos.notify.notify; - - -enum NOTIFY_VERSION_MAJOR = 0; -enum NOTIFY_VERSION_MINOR = 7; -enum NOTIFY_VERSION_MICRO = 5; - -template NOTIFY_CHECK_VERSION(int major, int minor, int micro) { - enum NOTIFY_CHECK_VERSION = ((NOTIFY_VERSION_MAJOR > major) || - (NOTIFY_VERSION_MAJOR == major && NOTIFY_VERSION_MINOR > minor) || - (NOTIFY_VERSION_MAJOR == major && NOTIFY_VERSION_MINOR == minor && - NOTIFY_VERSION_MICRO >= micro)); -} - - -alias ulong GType; -alias void function(void*) GFreeFunc; - -struct GError { - uint domain; - int code; - char* message; -} - -struct GList { - void* data; - GList* next; - GList* prev; -} - -// dummies -struct GdkPixbuf {} -struct GObject {} -struct GObjectClass {} -struct GVariant {} - -GType notify_urgency_get_type(); - -/** - * NOTIFY_EXPIRES_DEFAULT: - * - * The default expiration time on a notification. - */ -enum NOTIFY_EXPIRES_DEFAULT = -1; - -/** - * NOTIFY_EXPIRES_NEVER: - * - * The notification never expires. It stays open until closed by the calling API - * or the user. - */ -enum NOTIFY_EXPIRES_NEVER = 0; - -// #define NOTIFY_TYPE_NOTIFICATION (notify_notification_get_type ()) -// #define NOTIFY_NOTIFICATION(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), NOTIFY_TYPE_NOTIFICATION, NotifyNotification)) -// #define NOTIFY_NOTIFICATION_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), NOTIFY_TYPE_NOTIFICATION, NotifyNotificationClass)) -// #define NOTIFY_IS_NOTIFICATION(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), NOTIFY_TYPE_NOTIFICATION)) -// #define NOTIFY_IS_NOTIFICATION_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), NOTIFY_TYPE_NOTIFICATION)) -// #define NOTIFY_NOTIFICATION_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), NOTIFY_TYPE_NOTIFICATION, NotifyNotificationClass)) - -extern (C) { - struct NotifyNotificationPrivate; - - struct NotifyNotification { - /*< private >*/ - GObject parent_object; - - NotifyNotificationPrivate *priv; - } - - struct NotifyNotificationClass { - GObjectClass parent_class; - - /* Signals */ - void function(NotifyNotification *notification) closed; - } - - - /** - * NotifyUrgency: - * @NOTIFY_URGENCY_LOW: Low urgency. Used for unimportant notifications. - * @NOTIFY_URGENCY_NORMAL: Normal urgency. Used for most standard notifications. - * @NOTIFY_URGENCY_CRITICAL: Critical urgency. Used for very important notifications. - * - * The urgency level of the notification. - */ - enum NotifyUrgency { - NOTIFY_URGENCY_LOW, - NOTIFY_URGENCY_NORMAL, - NOTIFY_URGENCY_CRITICAL, - - } - - /** - * NotifyActionCallback: - * @notification: - * @action: - * @user_data: - * - * An action callback function. - */ - alias void function(NotifyNotification* notification, char* action, void* user_data) NotifyActionCallback; - - - GType notify_notification_get_type(); - - NotifyNotification* notify_notification_new(const(char)* summary, const(char)* body_, const(char)* icon); - - bool notify_notification_update(NotifyNotification* notification, const(char)* summary, const(char)* body_, const(char)* icon); - - bool notify_notification_show(NotifyNotification* notification, GError** error); - - void notify_notification_set_timeout(NotifyNotification* notification, int timeout); - - void notify_notification_set_category(NotifyNotification* notification, const(char)* category); - - void notify_notification_set_urgency(NotifyNotification* notification, NotifyUrgency urgency); - - void notify_notification_set_image_from_pixbuf(NotifyNotification* notification, GdkPixbuf* pixbuf); - - void notify_notification_set_icon_from_pixbuf(NotifyNotification* notification, GdkPixbuf* icon); - - void notify_notification_set_hint_int32(NotifyNotification* notification, const(char)* key, int value); - void notify_notification_set_hint_uint32(NotifyNotification* notification, const(char)* key, uint value); - - void notify_notification_set_hint_double(NotifyNotification* notification, const(char)* key, double value); - - void notify_notification_set_hint_string(NotifyNotification* notification, const(char)* key, const(char)* value); - - void notify_notification_set_hint_byte(NotifyNotification* notification, const(char)* key, ubyte value); - - void notify_notification_set_hint_byte_array(NotifyNotification* notification, const(char)* key, const(ubyte)* value, ulong len); - - void notify_notification_set_hint(NotifyNotification* notification, const(char)* key, GVariant* value); - - void notify_notification_set_app_name(NotifyNotification* notification, const(char)* app_name); - - void notify_notification_clear_hints(NotifyNotification* notification); - - void notify_notification_add_action(NotifyNotification* notification, const(char)* action, const(char)* label, - NotifyActionCallback callback, void* user_data, GFreeFunc free_func); - - void notify_notification_clear_actions(NotifyNotification* notification); - bool notify_notification_close(NotifyNotification* notification, GError** error); - - int notify_notification_get_closed_reason(const NotifyNotification* notification); - - - - bool notify_init(const(char)* app_name); - void notify_uninit(); - bool notify_is_initted(); - - const(char)* notify_get_app_name(); - void notify_set_app_name(const(char)* app_name); - - GList *notify_get_server_caps(); - - bool notify_get_server_info(char** ret_name, char** ret_vendor, char** ret_version, char** ret_spec_version); -} - -version(MainTest) { - import std.string; - - void main() { - - notify_init("test".toStringz()); - - auto n = notify_notification_new("summary".toStringz(), "body".toStringz(), "none".toStringz()); - GError* ge; - notify_notification_show(n, &ge); - - scope(success) notify_uninit(); - } -} diff --git a/src/onedrive.d b/src/onedrive.d deleted file mode 100644 index 29d33a46..00000000 --- a/src/onedrive.d +++ /dev/null @@ -1,1887 +0,0 @@ -import std.net.curl; -import etc.c.curl: CurlOption; -import std.datetime, std.datetime.systime, std.exception, std.file, std.json, std.path; -import std.stdio, std.string, std.uni, std.uri, std.file, std.uuid; -import std.array: split; -import core.atomic : atomicOp; -import core.stdc.stdlib; -import core.thread, std.conv, std.math; -import std.algorithm.searching; -import std.concurrency; -import progress; -import config; -import util; -import arsd.cgi; -import std.datetime; -static import log; -shared bool debugResponse = false; -private bool dryRun = false; -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"; - string globalGraphEndpoint = "https://graph.microsoft.com"; - - // US Government L4 - string usl4AuthEndpoint = "https://login.microsoftonline.us"; - string usl4GraphEndpoint = "https://graph.microsoft.us"; - - // US Government L5 - string usl5AuthEndpoint = "https://login.microsoftonline.us"; - string usl5GraphEndpoint = "https://dod-graph.microsoft.us"; - - // Germany - string deAuthEndpoint = "https://login.microsoftonline.de"; - string deGraphEndpoint = "https://graph.microsoft.de"; - - // China - string cnAuthEndpoint = "https://login.chinacloudapi.cn"; - string cnGraphEndpoint = "https://microsoftgraph.chinacloudapi.cn"; -} - -private { - // Client ID / Application ID - string clientId = clientIdDefault; - - // Default User Agent configuration - string isvTag = "ISV"; - string companyName = "abraunegg"; - // Application name as per Microsoft Azure application registration - string appTitle = "OneDrive Client for Linux"; - - // Default Drive ID - string driveId = ""; - - // API Query URL's, based on using defaults, but can be updated by config option 'azure_ad_endpoint' - // Authentication - string authUrl = globalAuthEndpoint ~ "/common/oauth2/v2.0/authorize"; - string redirectUrl = globalAuthEndpoint ~ "/common/oauth2/nativeclient"; - string tokenUrl = globalAuthEndpoint ~ "/common/oauth2/v2.0/token"; - - // Drive Queries - string driveUrl = globalGraphEndpoint ~ "/v1.0/me/drive"; - string driveByIdUrl = globalGraphEndpoint ~ "/v1.0/drives/"; - - // What is 'shared with me' Query - string sharedWithMeUrl = globalGraphEndpoint ~ "/v1.0/me/drive/sharedWithMe"; - - // Item Queries - string itemByIdUrl = globalGraphEndpoint ~ "/v1.0/me/drive/items/"; - string itemByPathUrl = globalGraphEndpoint ~ "/v1.0/me/drive/root:/"; - - // Office 365 / SharePoint Queries - string siteSearchUrl = globalGraphEndpoint ~ "/v1.0/sites?search"; - string siteDriveUrl = globalGraphEndpoint ~ "/v1.0/sites/"; - - // Subscriptions - string subscriptionUrl = globalGraphEndpoint ~ "/v1.0/subscriptions"; -} - -class OneDriveException: Exception -{ - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/errors - int httpStatusCode; - JSONValue error; - - @safe pure this(int httpStatusCode, string reason, string file = __FILE__, size_t line = __LINE__) - { - this.httpStatusCode = httpStatusCode; - this.error = error; - string msg = format("HTTP request returned status code %d (%s)", httpStatusCode, reason); - super(msg, file, line); - } - - this(int httpStatusCode, string reason, ref const JSONValue error, string file = __FILE__, size_t line = __LINE__) - { - this.httpStatusCode = httpStatusCode; - this.error = error; - string msg = format("HTTP request returned status code %d (%s)\n%s", httpStatusCode, reason, toJSON(error, true)); - super(msg, file, line); - } -} - -class OneDriveWebhook { - // We need OneDriveWebhook.serve to be a static function, otherwise we would hit the member function - // "requires a dual-context, which is deprecated" warning. The root cause is described here: - // - https://issues.dlang.org/show_bug.cgi?id=5710 - // - https://forum.dlang.org/post/fkyppfxzegenniyzztos@forum.dlang.org - // The problem is deemed a bug and should be fixed in the compilers eventually. The singleton stuff - // could be undone when it is fixed. - // - // Following the singleton pattern described here: https://wiki.dlang.org/Low-Lock_Singleton_Pattern - // Cache instantiation flag in thread-local bool - // Thread local - private static bool instantiated_; - - // Thread global - private __gshared OneDriveWebhook instance_; - - private string host; - private ushort port; - private Tid parentTid; - private shared uint count; - - static OneDriveWebhook getOrCreate(string host, ushort port, Tid parentTid) { - if (!instantiated_) { - synchronized(OneDriveWebhook.classinfo) { - if (!instance_) { - instance_ = new OneDriveWebhook(host, port, parentTid); - } - - instantiated_ = true; - } - } - - return instance_; - } - - private this(string host, ushort port, Tid parentTid) { - this.host = host; - this.port = port; - this.parentTid = parentTid; - this.count = 0; - } - - // The static serve() is necessary because spawn() does not like instance methods - static serve() { - // we won't create the singleton instance if it hasn't been created already - // such case is a bug which should crash the program and gets fixed - instance_.serveImpl(); - } - - // The static handle() is necessary to work around the dual-context warning mentioned above - private static void handle(Cgi cgi) { - // we won't create the singleton instance if it hasn't been created already - // such case is a bug which should crash the program and gets fixed - instance_.handleImpl(cgi); - } - - private void serveImpl() { - auto server = new RequestServer(host, port); - server.serveEmbeddedHttp!handle(); - } - - private void handleImpl(Cgi cgi) { - if (.debugResponse) { - log.log("Webhook request: ", cgi.requestMethod, " ", cgi.requestUri); - if (!cgi.postBody.empty) { - log.log("Webhook post body: ", cgi.postBody); - } - } - - cgi.setResponseContentType("text/plain"); - - if ("validationToken" in cgi.get) { - // For validation requests, respond with the validation token passed in the query string - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/webhook-receiver-validation-request - cgi.write(cgi.get["validationToken"]); - log.log("Webhook: handled validation request"); - } else { - // Notifications don't include any information about the changes that triggered them. - // Put a refresh signal in the queue and let the main monitor loop process it. - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/using-webhooks - count.atomicOp!"+="(1); - send(parentTid, to!ulong(count)); - cgi.write("OK"); - log.log("Webhook: sent refresh signal #", count); - } - } -} - -final class OneDriveApi -{ - private Config cfg; - private string refreshToken, accessToken, subscriptionId; - private SysTime accessTokenExpiration; - private HTTP http; - private OneDriveWebhook webhook; - private SysTime subscriptionExpiration; - private Duration subscriptionExpirationInterval, subscriptionRenewalInterval; - private string notificationUrl; - - // if true, every new access token is printed - bool printAccessToken; - - this(Config cfg) - { - this.cfg = cfg; - http = HTTP(); - // Curl Timeout Handling - // libcurl dns_cache_timeout timeout - http.dnsTimeout = (dur!"seconds"(cfg.getValueLong("dns_timeout"))); - // Timeout for HTTPS connections - http.connectTimeout = (dur!"seconds"(cfg.getValueLong("connect_timeout"))); - // with the following settings we force - // - if there is no data flow for 10min, abort - // - if the download time for one item exceeds 1h, abort - // - // timeout for activity on connection - // this translates into Curl's CURLOPT_LOW_SPEED_TIME - // which says - // It contains the time in number seconds that the - // transfer speed should be below the CURLOPT_LOW_SPEED_LIMIT - // for the library to consider it too slow and abort. - http.dataTimeout = (dur!"seconds"(cfg.getValueLong("data_timeout"))); - // maximum time an operation is allowed to take - // This includes dns resolution, connecting, data transfer, etc. - http.operationTimeout = (dur!"seconds"(cfg.getValueLong("operation_timeout"))); - // What IP protocol version should be used when using Curl - IPv4 & IPv6, IPv4 or IPv6 - http.handle.set(CurlOption.ipresolve,cfg.getValueLong("ip_protocol_version")); // 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only - // Specify how many redirects should be allowed - http.maxRedirects(cfg.defaultMaxRedirects); - - // Do we enable curl debugging? - if (cfg.getValueBool("debug_https")) { - http.verbose = true; - .debugResponse = true; - - // Output what options we are using so that in the debug log this can be tracked - log.vdebug("http.dnsTimeout = ", cfg.getValueLong("dns_timeout")); - log.vdebug("http.connectTimeout = ", cfg.getValueLong("connect_timeout")); - log.vdebug("http.dataTimeout = ", cfg.getValueLong("data_timeout")); - log.vdebug("http.operationTimeout = ", cfg.getValueLong("operation_timeout")); - log.vdebug("http.CurlOption.ipresolve = ", cfg.getValueLong("ip_protocol_version")); - log.vdebug("http.maxRedirects = ", cfg.defaultMaxRedirects); - } - - // 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"; - if (cfg.getValueString("azure_tenant_id") != "") { - // 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 "": - 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"; - 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/"; - // Item Queries - itemByIdUrl = usl4GraphEndpoint ~ "/v1.0/me/drive/items/"; - itemByPathUrl = usl4GraphEndpoint ~ "/v1.0/me/drive/root:/"; - // Office 365 / SharePoint Queries - siteSearchUrl = usl4GraphEndpoint ~ "/v1.0/sites?search"; - siteDriveUrl = usl4GraphEndpoint ~ "/v1.0/sites/"; - // Shared With Me - sharedWithMeUrl = usl4GraphEndpoint ~ "/v1.0/me/drive/sharedWithMe"; - // Subscriptions - subscriptionUrl = usl4GraphEndpoint ~ "/v1.0/subscriptions"; - break; - case "USL5": - log.log("Configuring Azure AD for US Government Endpoints (DOD)"); - // Authentication - authUrl = usl5AuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize"; - 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/"; - // Item Queries - itemByIdUrl = usl5GraphEndpoint ~ "/v1.0/me/drive/items/"; - itemByPathUrl = usl5GraphEndpoint ~ "/v1.0/me/drive/root:/"; - // Office 365 / SharePoint Queries - siteSearchUrl = usl5GraphEndpoint ~ "/v1.0/sites?search"; - siteDriveUrl = usl5GraphEndpoint ~ "/v1.0/sites/"; - // Shared With Me - sharedWithMeUrl = usl5GraphEndpoint ~ "/v1.0/me/drive/sharedWithMe"; - // Subscriptions - subscriptionUrl = usl5GraphEndpoint ~ "/v1.0/subscriptions"; - break; - case "DE": - log.log("Configuring Azure AD Germany"); - // Authentication - authUrl = deAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize"; - 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/"; - // Item Queries - itemByIdUrl = deGraphEndpoint ~ "/v1.0/me/drive/items/"; - itemByPathUrl = deGraphEndpoint ~ "/v1.0/me/drive/root:/"; - // Office 365 / SharePoint Queries - siteSearchUrl = deGraphEndpoint ~ "/v1.0/sites?search"; - siteDriveUrl = deGraphEndpoint ~ "/v1.0/sites/"; - // Shared With Me - sharedWithMeUrl = deGraphEndpoint ~ "/v1.0/me/drive/sharedWithMe"; - // Subscriptions - subscriptionUrl = deGraphEndpoint ~ "/v1.0/subscriptions"; - break; - case "CN": - log.log("Configuring AD China operated by 21Vianet"); - // Authentication - authUrl = cnAuthEndpoint ~ "/" ~ tenantId ~ "/oauth2/v2.0/authorize"; - 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/"; - // Item Queries - itemByIdUrl = cnGraphEndpoint ~ "/v1.0/me/drive/items/"; - itemByPathUrl = cnGraphEndpoint ~ "/v1.0/me/drive/root:/"; - // Office 365 / SharePoint Queries - siteSearchUrl = cnGraphEndpoint ~ "/v1.0/sites?search"; - siteDriveUrl = cnGraphEndpoint ~ "/v1.0/sites/"; - // Shared With Me - sharedWithMeUrl = cnGraphEndpoint ~ "/v1.0/me/drive/sharedWithMe"; - // Subscriptions - subscriptionUrl = cnGraphEndpoint ~ "/v1.0/subscriptions"; - break; - // Default - all other entries - default: - log.log("Unknown Azure AD Endpoint request - using Global Azure AD Endpoints"); - } - - // Debug output of configured URL's - // Authentication - log.vdebug("Configured authUrl: ", authUrl); - log.vdebug("Configured redirectUrl: ", redirectUrl); - log.vdebug("Configured tokenUrl: ", tokenUrl); - - // Drive Queries - log.vdebug("Configured driveUrl: ", driveUrl); - log.vdebug("Configured driveByIdUrl: ", driveByIdUrl); - - // Shared With Me - log.vdebug("Configured sharedWithMeUrl: ", sharedWithMeUrl); - - // Item Queries - log.vdebug("Configured itemByIdUrl: ", itemByIdUrl); - log.vdebug("Configured itemByPathUrl: ", itemByPathUrl); - - // SharePoint Queries - log.vdebug("Configured siteSearchUrl: ", siteSearchUrl); - log.vdebug("Configured siteDriveUrl: ", siteDriveUrl); - - // Configure the User Agent string - if (cfg.getValueString("user_agent") == "") { - // Application User Agent string defaults - // Comply with OneDrive traffic decoration requirements - // https://docs.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online - // - Identify as ISV and include Company Name, App Name separated by a pipe character and then adding Version number separated with a slash character - // Note: If you've created an application, the recommendation is to register and use AppID and AppTitle - // The issue here is that currently the application is still using the 'skilion' application ID, thus no idea what the AppTitle used was. - http.setUserAgent = isvTag ~ "|" ~ companyName ~ "|" ~ appTitle ~ "/" ~ strip(import("version")); - } else { - // Use the value entered by the user - http.setUserAgent = cfg.getValueString("user_agent"); - } - - // What version of HTTP protocol do we use? - // Curl >= 7.62.0 defaults to http2 for a significant number of operations - if (cfg.getValueBool("force_http_11")) { - // Downgrade to curl to use HTTP 1.1 for all operations - log.vlog("Downgrading all HTTP operations to HTTP/1.1 due to user configuration"); - // Downgrade to HTTP 1.1 - yes version = 2 is HTTP 1.1 - http.handle.set(CurlOption.http_version,2); - } else { - // Use curl defaults - log.vlog("Using Curl defaults for all HTTP operations"); - } - - // Configure upload / download rate limits if configured - long userRateLimit = cfg.getValueLong("rate_limit"); - // 131072 = 128 KB/s - minimum for basic application operations to prevent timeouts - // A 0 value means rate is unlimited, and is the curl default - - if (userRateLimit > 0) { - // User configured rate limit - writeln("User Configured Rate Limit: ", userRateLimit); - - // If user provided rate limit is < 131072, flag that this is too low, setting to the minimum of 131072 - if (userRateLimit < 131072) { - // user provided limit too low - log.log("WARNING: User configured rate limit too low for normal application processing and preventing application timeouts. Overriding to default minimum of 131072 (128KB/s)"); - userRateLimit = 131072; - } - - // set rate limit - http.handle.set(CurlOption.max_send_speed_large,userRateLimit); - http.handle.set(CurlOption.max_recv_speed_large,userRateLimit); - } - - // Explicitly set libcurl options - // https://curl.se/libcurl/c/CURLOPT_NOSIGNAL.html - // Ensure that nosignal is set to 0 - Setting CURLOPT_NOSIGNAL to 0 makes libcurl ask the system to ignore SIGPIPE signals - http.handle.set(CurlOption.nosignal,0); - // https://curl.se/libcurl/c/CURLOPT_TCP_NODELAY.html - // Ensure that TCP_NODELAY is set to 0 to ensure that TCP NAGLE is enabled - http.handle.set(CurlOption.tcp_nodelay,0); - // https://curl.se/libcurl/c/CURLOPT_FORBID_REUSE.html - // Ensure that we ARE reusing connections - setting to 0 ensures that we are reusing connections - http.handle.set(CurlOption.forbid_reuse,0); - - // Do we set the dryRun handlers? - if (cfg.getValueBool("dry_run")) { - .dryRun = true; - if (cfg.getValueBool("logout")) { - .simulateNoRefreshTokenFile = true; - } - } - - subscriptionExpiration = Clock.currTime(UTC()); - subscriptionExpirationInterval = dur!"seconds"(cfg.getValueLong("webhook_expiration_interval")); - subscriptionRenewalInterval = dur!"seconds"(cfg.getValueLong("webhook_renewal_interval")); - notificationUrl = cfg.getValueString("webhook_public_url"); - } - - // Shutdown OneDrive HTTP construct - void shutdown() - { - // delete subscription if there exists any - deleteSubscription(); - - // reset any values to defaults, freeing any set objects - http.clearRequestHeaders(); - http.onSend = null; - http.onReceive = null; - http.onReceiveHeader = null; - http.onReceiveStatusLine = null; - http.contentLength = 0; - // shut down the curl instance - http.shutdown(); - } - - bool init() - { - static import std.utf; - // detail what we are using for applicaion identification - log.vdebug("clientId = ", clientId); - log.vdebug("companyName = ", companyName); - log.vdebug("appTitle = ", appTitle); - - try { - driveId = cfg.getValueString("drive_id"); - if (driveId.length) { - driveUrl = driveByIdUrl ~ driveId; - itemByIdUrl = driveUrl ~ "/items"; - itemByPathUrl = driveUrl ~ "/root:/"; - } - } catch (Exception e) {} - - if (!.dryRun) { - // original code - try { - refreshToken = readText(cfg.refreshTokenFilePath); - } catch (FileException e) { - try { - return authorize(); - } catch (CurlException e) { - log.error("Cannot authorize with Microsoft OneDrive Service"); - return false; - } - } catch (std.utf.UTFException e) { - // path contains characters which generate a UTF exception - log.error("Cannot read refreshToken from: ", cfg.refreshTokenFilePath); - log.error(" Error Reason:", e.msg); - return false; - } - return true; - } else { - // --dry-run - if (!.simulateNoRefreshTokenFile) { - try { - refreshToken = readText(cfg.refreshTokenFilePath); - } catch (FileException e) { - return authorize(); - } catch (std.utf.UTFException e) { - // path contains characters which generate a UTF exception - log.error("Cannot read refreshToken from: ", cfg.refreshTokenFilePath); - log.error(" Error Reason:", e.msg); - return false; - } - return true; - } else { - // --dry-run & --reauth - return authorize(); - } - } - } - - bool authorize() - { - import std.stdio, std.regex; - char[] response; - string authScope; - // What authentication scope to use? - if (cfg.getValueBool("read_only_auth_scope")) { - // read-only authentication scopes has been requested - authScope = "&scope=Files.Read%20Files.Read.All%20Sites.Read.All%20offline_access&response_type=code&prompt=login&redirect_uri="; - } else { - // read-write authentication scopes will be used (default) - authScope = "&scope=Files.ReadWrite%20Files.ReadWrite.All%20Sites.ReadWrite.All%20offline_access&response_type=code&prompt=login&redirect_uri="; - } - - string url = authUrl ~ "?client_id=" ~ clientId ~ authScope ~ redirectUrl; - string authFilesString = cfg.getValueString("auth_files"); - string authResponseString = cfg.getValueString("auth_response"); - if (authResponseString != "") { - response = cast(char[]) authResponseString; - } else if (authFilesString != "") { - string[] authFiles = authFilesString.split(":"); - string authUrl = authFiles[0]; - string responseUrl = authFiles[1]; - - try { - // Try and write out the auth URL to the nominated file - auto authUrlFile = File(authUrl, "w"); - authUrlFile.write(url); - authUrlFile.close(); - } catch (std.exception.ErrnoException e) { - // There was a file system error - // display the error message - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - return false; - } - - while (!exists(responseUrl)) { - Thread.sleep(dur!("msecs")(100)); - } - - // read response from OneDrive - try { - response = cast(char[]) read(responseUrl); - } catch (OneDriveException e) { - // exception generated - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return false; - } - - // try to remove old files - try { - std.file.remove(authUrl); - std.file.remove(responseUrl); - } catch (FileException e) { - log.error("Cannot remove files ", authUrl, " ", responseUrl); - return false; - } - } else { - log.log("Authorize this app visiting:\n"); - write(url, "\n\n", "Enter the response uri: "); - readln(response); - cfg.applicationAuthorizeResponseUri = true; - } - // match the authorization code - auto c = matchFirst(response, r"(?:[\?&]code=)([\w\d-.]+)"); - if (c.empty) { - log.log("Invalid response uri entered"); - return false; - } - c.popFront(); // skip the whole match - redeemToken(c.front); - return true; - } - - string getSiteSearchUrl() - { - // Return the actual siteSearchUrl being used and/or requested when performing 'siteQuery = onedrive.o365SiteSearch(nextLink);' call - return .siteSearchUrl; - } - - ulong getRetryAfterValue() - { - // Return the current value of retryAfterValue if it has been set to something other than 0 - return .retryAfterValue; - } - - void resetRetryAfterValue() - { - // Reset the current value of retryAfterValue to 0 after it has been used - .retryAfterValue = 0; - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/drive_get - JSONValue getDefaultDrive() - { - checkAccessTokenExpired(); - const(char)[] url; - url = driveUrl; - return get(driveUrl); - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get - JSONValue getDefaultRoot() - { - checkAccessTokenExpired(); - const(char)[] url; - url = driveUrl ~ "/root"; - return get(url); - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get - JSONValue getDriveIdRoot(const(char)[] driveId) - { - checkAccessTokenExpired(); - const(char)[] url; - url = driveByIdUrl ~ driveId ~ "/root"; - return get(url); - } - - // https://docs.microsoft.com/en-us/graph/api/drive-sharedwithme - JSONValue getSharedWithMe() - { - checkAccessTokenExpired(); - return get(sharedWithMeUrl); - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/drive_get - JSONValue getDriveQuota(const(char)[] driveId) - { - checkAccessTokenExpired(); - const(char)[] url; - url = driveByIdUrl ~ driveId ~ "/"; - url ~= "?select=quota"; - return get(url); - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delta - JSONValue viewChangesByItemId(const(char)[] driveId, const(char)[] id, const(char)[] deltaLink) - { - checkAccessTokenExpired(); - const(char)[] url; - // configure deltaLink to query - if (deltaLink.empty) { - url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/delta"; - url ~= "?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size"; - } else { - url = deltaLink; - } - return get(url); - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delta - JSONValue viewChangesByDriveId(const(char)[] driveId, const(char)[] deltaLink) - { - checkAccessTokenExpired(); - const(char)[] url = deltaLink; - if (url == null) { - url = driveByIdUrl ~ driveId ~ "/root/delta"; - url ~= "?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size"; - } - return get(url); - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_list_children - JSONValue listChildren(const(char)[] driveId, const(char)[] id, const(char)[] nextLink) - { - checkAccessTokenExpired(); - const(char)[] url; - // configure URL to query - if (nextLink.empty) { - url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/children"; - url ~= "?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size"; - } else { - url = nextLink; - } - return get(url); - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get_content - void downloadById(const(char)[] driveId, const(char)[] id, string saveToPath, long fileSize) - { - checkAccessTokenExpired(); - scope(failure) { - if (exists(saveToPath)) { - // try and remove the file, catch error - try { - remove(saveToPath); - } catch (FileException e) { - // display the error message - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - } - } - } - - // 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, getFunctionName!({})); - } - } - - 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 successfully - 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 - JSONValue simpleUpload(string localPath, string parentDriveId, string parentId, string filename, const(char)[] eTag = null) - { - checkAccessTokenExpired(); - string url = driveByIdUrl ~ parentDriveId ~ "/items/" ~ parentId ~ ":/" ~ encodeComponent(filename) ~ ":/content"; - // TODO: investigate why this fails for remote folders - //if (eTag) http.addRequestHeader("If-Match", eTag); - /*else http.addRequestHeader("If-None-Match", "*");*/ - return upload(localPath, url); - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content - JSONValue simpleUploadReplace(string localPath, string driveId, string id, const(char)[] eTag = null) - { - checkAccessTokenExpired(); - string url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/content"; - if (eTag) http.addRequestHeader("If-Match", eTag); - return upload(localPath, url); - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_update - JSONValue updateById(const(char)[] driveId, const(char)[] id, JSONValue data, const(char)[] eTag = null) - { - checkAccessTokenExpired(); - const(char)[] url = driveByIdUrl ~ driveId ~ "/items/" ~ id; - if (eTag) http.addRequestHeader("If-Match", eTag); - http.addRequestHeader("Content-Type", "application/json"); - return patch(url, data.toString()); - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delete - void deleteById(const(char)[] driveId, const(char)[] id, const(char)[] eTag = null) - { - checkAccessTokenExpired(); - const(char)[] url = driveByIdUrl ~ driveId ~ "/items/" ~ id; - //TODO: investigate why this always fail with 412 (Precondition Failed) - //if (eTag) http.addRequestHeader("If-Match", eTag); - del(url); - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_post_children - JSONValue createById(const(char)[] parentDriveId, const(char)[] parentId, JSONValue item) - { - checkAccessTokenExpired(); - const(char)[] url = driveByIdUrl ~ parentDriveId ~ "/items/" ~ parentId ~ "/children"; - http.addRequestHeader("Content-Type", "application/json"); - return post(url, item.toString()); - } - - // Return the details of the specified path - JSONValue getPathDetails(const(string) path) - { - checkAccessTokenExpired(); - const(char)[] url; - if ((path == ".")||(path == "/")) url = driveUrl ~ "/root/"; - else url = itemByPathUrl ~ encodeComponent(path) ~ ":/"; - url ~= "?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size"; - return get(url); - } - - // Return the details of the specified id - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get - JSONValue getPathDetailsById(const(char)[] driveId, const(char)[] id) - { - checkAccessTokenExpired(); - const(char)[] url; - url = driveByIdUrl ~ driveId ~ "/items/" ~ id; - url ~= "?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size"; - return get(url); - } - - // Return the requested details of the specified path on the specified drive id and path - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get?view=odsp-graph-online - JSONValue getPathDetailsByDriveId(const(char)[] driveId, const(string) path) - { - checkAccessTokenExpired(); - const(char)[] url; - // string driveByIdUrl = "https://graph.microsoft.com/v1.0/drives/"; - // Required format: /drives/{drive-id}/root:/{item-path} - url = driveByIdUrl ~ driveId ~ "/root:/" ~ encodeComponent(path); - url ~= "?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size"; - return get(url); - } - - // Return the requested details of the specified path on the specified drive id and item id - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get?view=odsp-graph-online - JSONValue getPathDetailsByDriveIdAndItemId(const(char)[] driveId, const(char)[] itemId) - { - checkAccessTokenExpired(); - const(char)[] url; - // string driveByIdUrl = "https://graph.microsoft.com/v1.0/drives/"; - // Required format: /drives/{drive-id}/items/{item-id} - url = driveByIdUrl ~ driveId ~ "/items/" ~ itemId; - url ~= "?select=id,name,eTag,cTag,deleted,file,folder,root,fileSystemInfo,remoteItem,parentReference,size"; - return get(url); - } - - // Return the requested details of the specified id - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get - JSONValue getFileDetails(const(char)[] driveId, const(char)[] id) - { - checkAccessTokenExpired(); - const(char)[] url; - url = driveByIdUrl ~ driveId ~ "/items/" ~ id; - url ~= "?select=size,malware,file,webUrl,lastModifiedBy,lastModifiedDateTime"; - return get(url); - } - - // Create an anonymous read-only shareable link for an existing file on OneDrive - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createlink - JSONValue createShareableLink(const(char)[] driveId, const(char)[] id, JSONValue accessScope) - { - checkAccessTokenExpired(); - const(char)[] url; - url = driveByIdUrl ~ driveId ~ "/items/" ~ id ~ "/createLink"; - http.addRequestHeader("Content-Type", "application/json"); - return post(url, accessScope.toString()); - } - - // https://dev.onedrive.com/items/move.htm - JSONValue moveByPath(const(char)[] sourcePath, JSONValue moveData) - { - // Need to use itemByPathUrl - checkAccessTokenExpired(); - string url = itemByPathUrl ~ encodeComponent(sourcePath); - http.addRequestHeader("Content-Type", "application/json"); - return move(url, moveData.toString()); - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession - JSONValue createUploadSession(const(char)[] parentDriveId, const(char)[] parentId, const(char)[] filename, const(char)[] eTag = null, JSONValue item = null) - { - checkAccessTokenExpired(); - const(char)[] url = driveByIdUrl ~ parentDriveId ~ "/items/" ~ parentId ~ ":/" ~ encodeComponent(filename) ~ ":/createUploadSession"; - if (eTag) http.addRequestHeader("If-Match", eTag); - http.addRequestHeader("Content-Type", "application/json"); - return post(url, item.toString()); - } - - // https://dev.onedrive.com/items/upload_large_files.htm - JSONValue uploadFragment(const(char)[] uploadUrl, string filepath, long offset, long offsetSize, long fileSize) - { - checkAccessTokenExpired(); - // open file as read-only in binary mode - auto file = File(filepath, "rb"); - file.seek(offset); - string contentRange = "bytes " ~ to!string(offset) ~ "-" ~ to!string(offset + offsetSize - 1) ~ "/" ~ to!string(fileSize); - log.vdebugNewLine("contentRange: ", contentRange); - - // function scopes - scope(exit) { - http.clearRequestHeaders(); - http.onSend = null; - http.onReceive = null; - http.onReceiveHeader = null; - http.onReceiveStatusLine = null; - http.contentLength = 0; - // close file if open - if (file.isOpen()){ - // close open file - file.close(); - } - } - - http.method = HTTP.Method.put; - http.url = uploadUrl; - http.addRequestHeader("Content-Range", contentRange); - http.onSend = data => file.rawRead(data).length; - // convert offsetSize to ulong - http.contentLength = to!ulong(offsetSize); - auto response = perform(); - // TODO: retry on 5xx errors - checkHttpCode(response); - return response; - } - - // https://dev.onedrive.com/items/upload_large_files.htm - JSONValue requestUploadStatus(const(char)[] uploadUrl) - { - checkAccessTokenExpired(); - // when using microsoft graph the auth code is different - return get(uploadUrl, true); - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/site_search?view=odsp-graph-online - JSONValue o365SiteSearch(const(char)[] nextLink){ - checkAccessTokenExpired(); - const(char)[] url; - // configure URL to query - if (nextLink.empty) { - url = siteSearchUrl ~ "=*"; - } else { - url = nextLink; - } - return get(url); - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/drive_list?view=odsp-graph-online - JSONValue o365SiteDrives(string site_id){ - checkAccessTokenExpired(); - const(char)[] url; - url = siteDriveUrl ~ site_id ~ "/drives"; - return get(url); - } - - // Create a new subscription or renew the existing subscription - void createOrRenewSubscription() { - checkAccessTokenExpired(); - - // Kick off the webhook server first - if (webhook is null) { - webhook = OneDriveWebhook.getOrCreate( - cfg.getValueString("webhook_listening_host"), - to!ushort(cfg.getValueLong("webhook_listening_port")), - thisTid - ); - spawn(&OneDriveWebhook.serve); - } - - if (!hasValidSubscription()) { - createSubscription(); - } else if (isSubscriptionUpForRenewal()) { - try { - renewSubscription(); - } catch (OneDriveException e) { - if (e.httpStatusCode == 404) { - log.log("The subscription is not found on the server. Recreating subscription ..."); - createSubscription(); - } - } - } - } - - private bool hasValidSubscription() { - return !subscriptionId.empty && subscriptionExpiration > Clock.currTime(UTC()); - } - - private bool isSubscriptionUpForRenewal() { - return subscriptionExpiration < Clock.currTime(UTC()) + subscriptionRenewalInterval; - } - - private void createSubscription() { - log.log("Initializing subscription for updates ..."); - - auto expirationDateTime = Clock.currTime(UTC()) + subscriptionExpirationInterval; - const(char)[] url; - url = subscriptionUrl; - // Create a resource item based on if we have a driveId - string resourceItem; - if (driveId.length) { - resourceItem = "/drives/" ~ driveId ~ "/root"; - } else { - resourceItem = "/me/drive/root"; - } - - // create JSON request to create webhook subscription - const JSONValue request = [ - "changeType": "updated", - "notificationUrl": notificationUrl, - "resource": resourceItem, - "expirationDateTime": expirationDateTime.toISOExtString(), - "clientState": randomUUID().toString() - ]; - http.addRequestHeader("Content-Type", "application/json"); - JSONValue response; - - try { - response = post(url, request.toString()); - } catch (OneDriveException e) { - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - - // We need to exit here, user needs to fix issue - log.error("ERROR: Unable to initialize subscriptions for updates. Please fix this issue."); - shutdown(); - exit(-1); - } - - // Save important subscription metadata including id and expiration - subscriptionId = response["id"].str; - subscriptionExpiration = SysTime.fromISOExtString(response["expirationDateTime"].str); - } - - private void renewSubscription() { - log.log("Renewing subscription for updates ..."); - - auto expirationDateTime = Clock.currTime(UTC()) + subscriptionExpirationInterval; - const(char)[] url; - url = subscriptionUrl ~ "/" ~ subscriptionId; - const JSONValue request = [ - "expirationDateTime": expirationDateTime.toISOExtString() - ]; - http.addRequestHeader("Content-Type", "application/json"); - JSONValue response = patch(url, request.toString()); - - // Update subscription expiration from the response - subscriptionExpiration = SysTime.fromISOExtString(response["expirationDateTime"].str); - } - - private void deleteSubscription() { - if (!hasValidSubscription()) { - return; - } - - const(char)[] url; - url = subscriptionUrl ~ "/" ~ subscriptionId; - del(url); - log.log("Deleted subscription"); - } - - private void redeemToken(const(char)[] authCode) - { - const(char)[] postData = - "client_id=" ~ clientId ~ - "&redirect_uri=" ~ redirectUrl ~ - "&code=" ~ authCode ~ - "&grant_type=authorization_code"; - acquireToken(postData); - } - - private void newToken() - { - string postData = - "client_id=" ~ clientId ~ - "&redirect_uri=" ~ redirectUrl ~ - "&refresh_token=" ~ refreshToken ~ - "&grant_type=refresh_token"; - acquireToken(postData); - } - - private void acquireToken(const(char)[] postData) - { - JSONValue response; - - try { - response = post(tokenUrl, postData); - } catch (OneDriveException e) { - // an error was generated - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - } - - if (response.type() == JSONType.object) { - // Has the client been configured to use read_only_auth_scope - if (cfg.getValueBool("read_only_auth_scope")) { - // read_only_auth_scope has been configured - if ("scope" in response){ - string effectiveScopes = response["scope"].str(); - // Display the effective authentication scopes - writeln(); - writeln("Effective API Authentication Scopes: ", effectiveScopes); - // if we have any write scopes, we need to tell the user to update an remove online prior authentication and exit application - if (canFind(effectiveScopes, "Write")) { - // effective scopes contain write scopes .. so not a read-only configuration - writeln(); - writeln("ERROR: You have authentication scopes that allow write operations. You need to remove your existing application access consent"); - writeln(); - writeln("Please login to https://account.live.com/consent/Manage and remove your existing application access consent"); - writeln(); - // force exit - shutdown(); - exit(-1); - } - } - } - - if ("access_token" in response){ - accessToken = "bearer " ~ response["access_token"].str(); - refreshToken = response["refresh_token"].str(); - accessTokenExpiration = Clock.currTime() + dur!"seconds"(response["expires_in"].integer()); - if (!.dryRun) { - try { - // try and update the refresh_token file - 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, getFunctionName!({})); - } - } - if (printAccessToken) writeln("New access token: ", accessToken); - } else { - log.error("\nInvalid authentication response from OneDrive. Please check the response uri\n"); - // re-authorize - authorize(); - } - } else { - log.vdebug("Invalid JSON response from OneDrive unable to initialize application"); - } - } - - private void checkAccessTokenExpired() - { - try { - if (Clock.currTime() >= accessTokenExpiration) { - newToken(); - } - } catch (OneDriveException e) { - if (e.httpStatusCode == 400 || e.httpStatusCode == 401) { - // flag error and notify - writeln(); - log.errorAndNotify("ERROR: Refresh token invalid, use --reauth to authorize the client again."); - writeln(); - // set error message - e.msg ~= "\nRefresh token invalid, use --reauth to authorize the client again"; - } - } - } - - private void addAccessTokenHeader() - { - http.addRequestHeader("Authorization", accessToken); - } - - private JSONValue get(const(char)[] url, bool skipToken = false) - { - scope(exit) http.clearRequestHeaders(); - log.vdebug("Request URL = ", url); - http.method = HTTP.Method.get; - http.url = url; - if (!skipToken) addAccessTokenHeader(); // HACK: requestUploadStatus - JSONValue response; - response = perform(); - checkHttpCode(response); - // OneDrive API Response Debugging if --https-debug is being used - if (.debugResponse){ - log.vdebug("OneDrive API Response: ", response); - } - return response; - } - - private void del(const(char)[] url) - { - scope(exit) http.clearRequestHeaders(); - http.method = HTTP.Method.del; - http.url = url; - addAccessTokenHeader(); - auto response = perform(); - checkHttpCode(response); - } - - private void download(const(char)[] url, string filename, long fileSize) - { - // Threshold for displaying download bar - long thresholdFileSize = 4 * 2^^20; // 4 MiB - - // To support marking of partially-downloaded files, - string originalFilename = filename; - string downloadFilename = filename ~ ".partial"; - - // open downloadFilename as write in binary mode - auto file = File(downloadFilename, "wb"); - - // function scopes - scope(exit) { - http.clearRequestHeaders(); - http.onSend = null; - http.onReceive = null; - http.onReceiveHeader = null; - http.onReceiveStatusLine = null; - http.contentLength = 0; - // Reset onProgress to not display anything for next download - http.onProgress = delegate int(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) - { - return 0; - }; - // close file if open - if (file.isOpen()){ - // close open file - file.close(); - } - } - - http.method = HTTP.Method.get; - http.url = url; - addAccessTokenHeader(); - - http.onReceive = (ubyte[] data) { - file.rawWrite(data); - return data.length; - }; - - if (fileSize >= thresholdFileSize){ - // Download Progress Bar - size_t iteration = 20; - Progress p = new Progress(iteration); - p.title = "Downloading"; - writeln(); - bool barInit = false; - real previousProgressPercent = -1.0; - real percentCheck = 5.0; - long segmentCount = 1; - // Setup progress bar to display - http.onProgress = delegate int(size_t dltotal, size_t dlnow, size_t ultotal, size_t ulnow) - { - // For each onProgress, what is the % of dlnow to dltotal - // floor - rounds down to nearest whole number - real currentDLPercent = floor(double(dlnow)/dltotal*100); - // Have we started downloading? - if (currentDLPercent > 0){ - // We have started downloading - log.vdebugNewLine("Data Received = ", dlnow); - log.vdebug("Expected Total = ", dltotal); - log.vdebug("Percent Complete = ", currentDLPercent); - // Every 5% download we need to increment the download bar - - // Has the user set a data rate limit? - // when using rate_limit, we will get odd download rates, for example: - // Percent Complete = 24 - // Data Received = 13080163 - // Expected Total = 52428800 - // Percent Complete = 24 - // Data Received = 13685777 - // Expected Total = 52428800 - // Percent Complete = 26 <---- jumps to 26% missing 25%, thus fmod misses incrementing progress bar - // Data Received = 13685777 - // Expected Total = 52428800 - // Percent Complete = 26 - - if (cfg.getValueLong("rate_limit") > 0) { - // User configured rate limit - // How much data should be in each segment to qualify for 5% - long dataPerSegment = to!long(floor(double(dltotal)/iteration)); - // How much data received do we need to validate against - long thisSegmentData = dataPerSegment * segmentCount; - long nextSegmentData = dataPerSegment * (segmentCount + 1); - // Has the data that has been received in a 5% window that we need to increment the progress bar at - if ((dlnow > thisSegmentData) && (dlnow < nextSegmentData) && (previousProgressPercent != currentDLPercent) || (dlnow == dltotal)) { - // Downloaded data equals approx 5% - log.vdebug("Incrementing Progress Bar using calculated 5% of data received"); - // Downloading 50% |oooooooooooooooooooo | ETA 00:01:40 - // increment progress bar - p.next(); - // update values - log.vdebug("Setting previousProgressPercent to ", currentDLPercent); - previousProgressPercent = currentDLPercent; - log.vdebug("Incrementing segmentCount"); - segmentCount++; - } - } else { - // Is currentDLPercent divisible by 5 leaving remainder 0 and does previousProgressPercent not equal currentDLPercent - if ((isIdentical(fmod(currentDLPercent, percentCheck), 0.0)) && (previousProgressPercent != currentDLPercent)) { - // currentDLPercent matches a new increment - log.vdebug("Incrementing Progress Bar using fmod match"); - // Downloading 50% |oooooooooooooooooooo | ETA 00:01:40 - // increment progress bar - p.next(); - // update values - previousProgressPercent = currentDLPercent; - } - } - } else { - if ((currentDLPercent == 0) && (!barInit)) { - // Initialise the download bar at 0% - // Downloading 0% | | ETA --:--:--: - p.next(); - barInit = true; - } - } - return 0; - }; - - // Perform download & display progress bar - try { - // try and catch any curl error - http.perform(); - // Check the HTTP Response headers - needed for correct 429 handling - // check will be performed in checkHttpCode() - writeln(); - // Reset onProgress to not display anything for next download done using exit scope - } catch (CurlException e) { - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - } - // free progress bar memory - p = null; - } else { - // No progress bar - try { - // try and catch any curl error - http.perform(); - // Check the HTTP Response headers - needed for correct 429 handling - // check will be performed in checkHttpCode() - } catch (CurlException e) { - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - } - } - - // Rename downloaded file - rename(downloadFilename, originalFilename); - - // Check the HTTP response code, which, if a 429, will also check response headers - checkHttpCode(); - } - - private auto patch(T)(const(char)[] url, const(T)[] patchData) - { - scope(exit) http.clearRequestHeaders(); - http.method = HTTP.Method.patch; - http.url = url; - addAccessTokenHeader(); - auto response = perform(patchData); - checkHttpCode(response); - return response; - } - - private auto post(T)(const(char)[] url, const(T)[] postData) - { - scope(exit) http.clearRequestHeaders(); - http.method = HTTP.Method.post; - http.url = url; - addAccessTokenHeader(); - auto response = perform(postData); - checkHttpCode(response); - return response; - } - - private auto move(T)(const(char)[] url, const(T)[] postData) - { - scope(exit) http.clearRequestHeaders(); - http.method = HTTP.Method.patch; - http.url = url; - addAccessTokenHeader(); - auto response = perform(postData); - // Check the HTTP response code, which, if a 429, will also check response headers - checkHttpCode(); - return response; - } - - private JSONValue upload(string filepath, string url) - { - checkAccessTokenExpired(); - // open file as read-only in binary mode - auto file = File(filepath, "rb"); - - // function scopes - scope(exit) { - http.clearRequestHeaders(); - http.onSend = null; - http.onReceive = null; - http.onReceiveHeader = null; - http.onReceiveStatusLine = null; - http.contentLength = 0; - // close file if open - if (file.isOpen()){ - // close open file - file.close(); - } - } - - http.method = HTTP.Method.put; - http.url = url; - addAccessTokenHeader(); - http.addRequestHeader("Content-Type", "application/octet-stream"); - http.onSend = data => file.rawRead(data).length; - http.contentLength = file.size; - auto response = perform(); - checkHttpCode(response); - return response; - } - - private JSONValue perform(const(void)[] sendData) - { - scope(exit) { - http.onSend = null; - http.contentLength = 0; - } - if (sendData) { - http.contentLength = sendData.length; - http.onSend = (void[] buf) { - import std.algorithm: min; - size_t minLen = min(buf.length, sendData.length); - if (minLen == 0) return 0; - buf[0 .. minLen] = sendData[0 .. minLen]; - sendData = sendData[minLen .. $]; - return minLen; - }; - } else { - http.onSend = buf => 0; - } - auto response = perform(); - return response; - } - - private JSONValue perform() - { - scope(exit) http.onReceive = null; - char[] content; - JSONValue json; - - http.onReceive = (ubyte[] data) { - content ~= data; - // HTTP Server Response Code Debugging if --https-debug is being used - if (.debugResponse){ - log.vdebug("onedrive.perform() => OneDrive HTTP Server Response: ", http.statusLine.code); - } - return data.length; - }; - - try { - http.perform(); - // Check the HTTP Response headers - needed for correct 429 handling - checkHTTPResponseHeaders(); - } catch (CurlException e) { - // Parse and display error message received from OneDrive - log.vdebug("onedrive.perform() Generated a OneDrive CurlException"); - auto errorArray = splitLines(e.msg); - string errorMessage = errorArray[0]; - - // what is contained in the curl error message? - if (canFind(errorMessage, "Couldn't connect to server on handle") || canFind(errorMessage, "Couldn't resolve host name on handle") || canFind(errorMessage, "Timeout was reached on handle")) { - // This is a curl timeout - // or is this a 408 request timeout - // https://github.com/abraunegg/onedrive/issues/694 - // Back off & retry with incremental delay - int retryCount = 10000; - int retryAttempts = 0; - int backoffInterval = 0; - int maxBackoffInterval = 3600; - int timestampAlign = 0; - bool retrySuccess = false; - SysTime currentTime; - - // what caused the initial curl exception? - if (canFind(errorMessage, "Couldn't connect to server on handle")) log.vdebug("Unable to connect to server - HTTPS access blocked?"); - if (canFind(errorMessage, "Couldn't resolve host name on handle")) log.vdebug("Unable to resolve server - DNS access blocked?"); - if (canFind(errorMessage, "Timeout was reached on handle")) log.vdebug("A timeout was triggered - data too slow, no response ... use --debug-https to diagnose further"); - - while (!retrySuccess){ - try { - // configure libcurl to perform a fresh connection - log.vdebug("Configuring libcurl to use a fresh connection for re-try"); - http.handle.set(CurlOption.fresh_connect,1); - // try the access - http.perform(); - // Check the HTTP Response headers - needed for correct 429 handling - checkHTTPResponseHeaders(); - // no error from http.perform() on re-try - log.log("Internet connectivity to Microsoft OneDrive service has been restored"); - // unset the fresh connect option as this then creates performance issues if left enabled - log.vdebug("Unsetting libcurl to use a fresh connection as this causes a performance impact if left enabled"); - http.handle.set(CurlOption.fresh_connect,0); - // connectivity restored - retrySuccess = true; - } catch (CurlException e) { - // when was the exception generated - currentTime = Clock.currTime(); - // Increment retry attempts - retryAttempts++; - if (canFind(e.msg, "Couldn't connect to server on handle") || canFind(e.msg, "Couldn't resolve host name on handle") || canFind(errorMessage, "Timeout was reached on handle")) { - // no access to Internet - writeln(); - log.error("ERROR: There was a timeout in accessing the Microsoft OneDrive service - Internet connectivity issue?"); - // what is the error reason to assis the user as what to check - if (canFind(e.msg, "Couldn't connect to server on handle")) { - log.log(" - Check HTTPS access or Firewall Rules"); - timestampAlign = 9; - } - if (canFind(e.msg, "Couldn't resolve host name on handle")) { - log.log(" - Check DNS resolution or Firewall Rules"); - timestampAlign = 0; - } - - // increment backoff interval - backoffInterval++; - int thisBackOffInterval = retryAttempts*backoffInterval; - - // display retry information - currentTime.fracSecs = Duration.zero; - auto timeString = currentTime.toString(); - log.vlog(" Retry attempt: ", retryAttempts); - log.vlog(" This attempt timestamp: ", timeString); - if (thisBackOffInterval > maxBackoffInterval) { - thisBackOffInterval = maxBackoffInterval; - } - - // detail when the next attempt will be tried - // factor in the delay for curl to generate the exception - otherwise the next timestamp appears to be 'out' even though technically correct - auto nextRetry = currentTime + dur!"seconds"(thisBackOffInterval) + dur!"seconds"(timestampAlign); - log.vlog(" Next retry in approx: ", (thisBackOffInterval + timestampAlign), " seconds"); - log.vlog(" Next retry approx: ", nextRetry); - - // thread sleep - Thread.sleep(dur!"seconds"(thisBackOffInterval)); - } - if (retryAttempts == retryCount) { - // we have attempted to re-connect X number of times - // false set this to true to break out of while loop - retrySuccess = true; - } - } - } - if (retryAttempts >= retryCount) { - log.error(" ERROR: Unable to reconnect to the Microsoft OneDrive service after ", retryCount, " attempts lasting over 1.2 years!"); - throw new OneDriveException(408, "Request Timeout - HTTP 408 or Internet down?"); - } - } else { - // Log that an error was returned - log.error("ERROR: OneDrive returned an error with the following message:"); - // Some other error was returned - log.error(" Error Message: ", errorMessage); - log.error(" Calling Function: ", getFunctionName!({})); - - // Was this a curl initialization error? - if (canFind(errorMessage, "Failed initialization on handle")) { - // initialization error ... prevent a run-away process if we have zero disk space - ulong localActualFreeSpace = to!ulong(getAvailableDiskSpace(".")); - if (localActualFreeSpace == 0) { - // force exit - shutdown(); - exit(-1); - } - } - } - // return an empty JSON for handling - return json; - } - - try { - json = content.parseJSON(); - } catch (JSONException e) { - // Log that a JSON Exception was caught, dont output the HTML response from OneDrive - log.vdebug("JSON Exception caught when performing HTTP operations - use --debug-https to diagnose further"); - } - return json; - } - - private void checkHTTPResponseHeaders() - { - // Get the HTTP Response headers - needed for correct 429 handling - auto responseHeaders = http.responseHeaders(); - if (.debugResponse){ - log.vdebug("http.perform() => HTTP Response Headers: ", responseHeaders); - } - - // is retry-after in the response headers - if ("retry-after" in http.responseHeaders) { - // Set the retry-after value - log.vdebug("http.perform() => Received a 'Retry-After' Header Response with the following value: ", http.responseHeaders["retry-after"]); - log.vdebug("http.perform() => Setting retryAfterValue to: ", http.responseHeaders["retry-after"]); - .retryAfterValue = to!ulong(http.responseHeaders["retry-after"]); - } - } - - private void checkHttpCode() - { - // https://dev.onedrive.com/misc/errors.htm - // https://developer.overdrive.com/docs/reference-guide - - /* - HTTP/1.1 Response handling - - Errors in the OneDrive API are returned using standard HTTP status codes, as well as a JSON error response object. The following HTTP status codes should be expected. - - Status code Status message Description - 100 Continue Continue - 200 OK Request was handled OK - 201 Created This means you've made a successful POST to checkout, lock in a format, or place a hold - 204 No Content This means you've made a successful DELETE to remove a hold or return a title - - 400 Bad Request Cannot process the request because it is malformed or incorrect. - 401 Unauthorized Required authentication information is either missing or not valid for the resource. - 403 Forbidden Access is denied to the requested resource. The user might not have enough permission. - 404 Not Found The requested resource doesn’t exist. - 405 Method Not Allowed The HTTP method in the request is not allowed on the resource. - 406 Not Acceptable This service doesn’t support the format requested in the Accept header. - 408 Request Time out Not expected from OneDrive, but can be used to handle Internet connection failures the same (fallback and try again) - 409 Conflict The current state conflicts with what the request expects. For example, the specified parent folder might not exist. - 410 Gone The requested resource is no longer available at the server. - 411 Length Required A Content-Length header is required on the request. - 412 Precondition Failed A precondition provided in the request (such as an if-match header) does not match the resource's current state. - 413 Request Entity Too Large The request size exceeds the maximum limit. - 415 Unsupported Media Type The content type of the request is a format that is not supported by the service. - 416 Requested Range Not Satisfiable The specified byte range is invalid or unavailable. - 422 Unprocessable Entity Cannot process the request because it is semantically incorrect. - 429 Too Many Requests Client application has been throttled and should not attempt to repeat the request until an amount of time has elapsed. - - 500 Internal Server Error There was an internal server error while processing the request. - 501 Not Implemented The requested feature isn’t implemented. - 502 Bad Gateway The service was unreachable - 503 Service Unavailable The service is temporarily unavailable. You may repeat the request after a delay. There may be a Retry-After header. - 507 Insufficient Storage The maximum storage quota has been reached. - 509 Bandwidth Limit Exceeded Your app has been throttled for exceeding the maximum bandwidth cap. Your app can retry the request again after more time has elapsed. - - HTTP/2 Response handling - - 0 OK - - */ - - switch(http.statusLine.code) - { - // 0 - OK ... HTTP2 version of 200 OK - case 0: - break; - // 100 - Continue - case 100: - break; - // 200 - OK - case 200: - // No Log .. - break; - // 201 - Created OK - // 202 - Accepted - // 204 - Deleted OK - case 201,202,204: - // No actions, but log if verbose logging - //log.vlog("OneDrive Response: '", http.statusLine.code, " - ", http.statusLine.reason, "'"); - break; - - // 302 - resource found and available at another location, redirect - case 302: - break; - - // 400 - Bad Request - case 400: - // Bad Request .. how should we act? - log.vlog("OneDrive returned a 'HTTP 400 - Bad Request' - gracefully handling error"); - break; - - // 403 - Forbidden - case 403: - // OneDrive responded that the user is forbidden - log.vlog("OneDrive returned a 'HTTP 403 - Forbidden' - gracefully handling error"); - break; - - // 404 - Item not found - case 404: - // Item was not found - do not throw an exception - log.vlog("OneDrive returned a 'HTTP 404 - Item not found' - gracefully handling error"); - break; - - // 408 - Request Timeout - case 408: - // Request to connect to OneDrive service timed out - log.vlog("Request Timeout - gracefully handling error"); - throw new OneDriveException(408, "Request Timeout - HTTP 408 or Internet down?"); - - // 409 - Conflict - case 409: - // Conflict handling .. how should we act? This only really gets triggered if we are using --local-first & we remove items.db as the DB thinks the file is not uploaded but it is - log.vlog("OneDrive returned a 'HTTP 409 - Conflict' - gracefully handling error"); - break; - - // 412 - Precondition Failed - case 412: - // A precondition provided in the request (such as an if-match header) does not match the resource's current state. - log.vlog("OneDrive returned a 'HTTP 412 - Precondition Failed' - gracefully handling error"); - break; - - // 415 - Unsupported Media Type - case 415: - // Unsupported Media Type ... sometimes triggered on image files, especially PNG - log.vlog("OneDrive returned a 'HTTP 415 - Unsupported Media Type' - gracefully handling error"); - break; - - // 429 - Too Many Requests - case 429: - // Too many requests in a certain time window - // Check the HTTP Response headers - needed for correct 429 handling - checkHTTPResponseHeaders(); - // https://docs.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online - log.vlog("OneDrive returned a 'HTTP 429 - Too Many Requests' - gracefully handling error"); - throw new OneDriveException(http.statusLine.code, http.statusLine.reason); - - // Server side (OneDrive) Errors - // 500 - Internal Server Error - // 502 - Bad Gateway - // 503 - Service Unavailable - // 504 - Gateway Timeout (Issue #320) - case 500: - // No actions - log.vlog("OneDrive returned a 'HTTP 500 Internal Server Error' - gracefully handling error"); - break; - - case 502: - // No actions - log.vlog("OneDrive returned a 'HTTP 502 Bad Gateway Error' - gracefully handling error"); - break; - - case 503: - // No actions - log.vlog("OneDrive returned a 'HTTP 503 Service Unavailable Error' - gracefully handling error"); - break; - - case 504: - // No actions - log.vlog("OneDrive returned a 'HTTP 504 Gateway Timeout Error' - gracefully handling error"); - break; - - // "else" - default: - throw new OneDriveException(http.statusLine.code, http.statusLine.reason); - } - } - - private void checkHttpCode(ref const JSONValue response) - { - switch(http.statusLine.code) - { - // 0 - OK ... HTTP2 version of 200 OK - case 0: - break; - // 100 - Continue - case 100: - break; - // 200 - OK - case 200: - // No Log .. - break; - // 201 - Created OK - // 202 - Accepted - // 204 - Deleted OK - case 201,202,204: - // No actions, but log if verbose logging - //log.vlog("OneDrive Response: '", http.statusLine.code, " - ", http.statusLine.reason, "'"); - break; - - // 302 - resource found and available at another location, redirect - case 302: - break; - - // 400 - Bad Request - case 400: - // Bad Request .. how should we act? - // make sure this is thrown so that it is caught - throw new OneDriveException(http.statusLine.code, http.statusLine.reason, response); - - // 403 - Forbidden - case 403: - // OneDrive responded that the user is forbidden - log.vlog("OneDrive returned a 'HTTP 403 - Forbidden' - gracefully handling error"); - // Throw this as a specific exception so this is caught when performing 'siteQuery = onedrive.o365SiteSearch(nextLink);' call - throw new OneDriveException(http.statusLine.code, http.statusLine.reason, response); - - // 412 - Precondition Failed - case 412: - // Throw this as a specific exception so this is caught when performing sync.uploadLastModifiedTime - throw new OneDriveException(http.statusLine.code, http.statusLine.reason, response); - - // Server side (OneDrive) Errors - // 500 - Internal Server Error - // 502 - Bad Gateway - // 503 - Service Unavailable - // 504 - Gateway Timeout (Issue #320) - case 500: - // Throw this as a specific exception so this is caught - throw new OneDriveException(http.statusLine.code, http.statusLine.reason, response); - - case 502: - // Throw this as a specific exception so this is caught - throw new OneDriveException(http.statusLine.code, http.statusLine.reason, response); - - case 503: - // Throw this as a specific exception so this is caught - throw new OneDriveException(http.statusLine.code, http.statusLine.reason, response); - - case 504: - // Throw this as a specific exception so this is caught - throw new OneDriveException(http.statusLine.code, http.statusLine.reason, response); - - // Default - all other errors that are not a 2xx or a 302 - default: - if (http.statusLine.code / 100 != 2 && http.statusLine.code != 302) { - throw new OneDriveException(http.statusLine.code, http.statusLine.reason, response); - } - } - } -} - -unittest -{ - string configDirName = expandTilde("~/.config/onedrive"); - auto cfg = new config.Config(configDirName); - cfg.init(); - OneDriveApi onedrive = new OneDriveApi(cfg); - onedrive.init(); - std.file.write("/tmp/test", "test"); - - // simpleUpload - auto item = onedrive.simpleUpload("/tmp/test", "/test"); - try { - item = onedrive.simpleUpload("/tmp/test", "/test"); - } catch (OneDriveException e) { - assert(e.httpStatusCode == 409); - } - try { - item = onedrive.simpleUpload("/tmp/test", "/test", "123"); - } catch (OneDriveException e) { - assert(e.httpStatusCode == 412); - } - item = onedrive.simpleUpload("/tmp/test", "/test", item["eTag"].str); - - // deleteById - try { - onedrive.deleteById(item["id"].str, "123"); - } catch (OneDriveException e) { - assert(e.httpStatusCode == 412); - } - onedrive.deleteById(item["id"].str, item["eTag"].str); - onedrive.http.shutdown(); -} diff --git a/src/progress.d b/src/progress.d deleted file mode 100644 index 9277ae12..00000000 --- a/src/progress.d +++ /dev/null @@ -1,156 +0,0 @@ -module progress; - -import std.stdio; -import std.range; -import std.format; -import std.datetime; -import core.sys.posix.unistd; -import core.sys.posix.sys.ioctl; - -class Progress -{ - private: - - immutable static size_t default_width = 80; - size_t max_width = 40; - size_t width = default_width; - - ulong start_time; - string caption = "Progress"; - size_t iterations; - size_t counter; - - - size_t getTerminalWidth() { - size_t column = default_width; - version (CRuntime_Musl) { - } else version(Android) { - } else { - winsize ws; - if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1 && ws.ws_col > 0) { - column = ws.ws_col; - } - } - - return column; - } - - - void clear() { - write("\r"); - for(auto i = 0; i < width; i++) write(" "); - write("\r"); - } - - - int calc_eta() { - immutable auto ratio = cast(double)counter / iterations; - auto current_time = Clock.currTime.toUnixTime(); - auto duration = cast(int)(current_time - start_time); - int hours, minutes, seconds; - double elapsed = (current_time - start_time); - int eta_sec = cast(int)((elapsed / ratio) - elapsed); - - // Return an ETA or Duration? - if (eta_sec != 0){ - return eta_sec; - } else { - return duration; - } - } - - - string progressbarText(string header_text, string footer_text) { - immutable auto ratio = cast(double)counter / iterations; - string result = ""; - - double bar_length = width - header_text.length - footer_text.length; - if(bar_length > max_width && max_width > 0) { - bar_length = max_width; - } - size_t i = 0; - for(; i < ratio * bar_length; i++) result ~= "o"; - for(; i < bar_length; i++) result ~= " "; - - return header_text ~ result ~ footer_text; - } - - - void print() { - immutable auto ratio = cast(double)counter / iterations; - auto header = appender!string(); - auto footer = appender!string(); - - header.formattedWrite("%s %3d%% |", caption, cast(int)(ratio * 100)); - - if(counter <= 0 || ratio == 0.0) { - footer.formattedWrite("| ETA --:--:--:"); - } else { - int h, m, s; - dur!"seconds"(calc_eta()) - .split!("hours", "minutes", "seconds")(h, m, s); - if (counter != iterations){ - footer.formattedWrite("| ETA %02d:%02d:%02d ", h, m, s); - } else { - footer.formattedWrite("| DONE IN %02d:%02d:%02d ", h, m, s); - } - } - - write(progressbarText(header.data, footer.data)); - } - - - void update() { - width = getTerminalWidth(); - - clear(); - - print(); - stdout.flush(); - } - - - public: - - this(size_t iterations) { - if(iterations <= 0) iterations = 1; - - counter = -1; - this.iterations = iterations; - start_time = Clock.currTime.toUnixTime; - } - - @property { - string title() { return caption; } - string title(string text) { return caption = text; } - } - - @property { - size_t count() { return counter; } - size_t count(size_t val) { - if(val > iterations) val = iterations; - return counter = val; - } - } - - @property { - size_t maxWidth() { return max_width; } - size_t maxWidth(size_t w) { - return max_width = w; - } - } - - void reset() { - counter = -1; - start_time = Clock.currTime.toUnixTime; - } - - void next() { - counter++; - if(counter > iterations) counter = iterations; - - update(); - } - - -} diff --git a/src/qxor.d b/src/qxor.d deleted file mode 100644 index 63e8f0f5..00000000 --- a/src/qxor.d +++ /dev/null @@ -1,88 +0,0 @@ -import std.algorithm; -import std.digest; - -// implementation of the QuickXorHash algorithm in D -// https://github.com/OneDrive/onedrive-api-docs/blob/live/docs/code-snippets/quickxorhash.md -struct QuickXor -{ - private enum int widthInBits = 160; - private enum size_t lengthInBytes = (widthInBits - 1) / 8 + 1; - private enum size_t lengthInQWords = (widthInBits - 1) / 64 + 1; - private enum int bitsInLastCell = widthInBits % 64; // 32 - private enum int shift = 11; - - private ulong[lengthInQWords] _data; - private ulong _lengthSoFar; - private int _shiftSoFar; - - nothrow @safe void put(scope const(ubyte)[] array...) - { - int vectorArrayIndex = _shiftSoFar / 64; - int vectorOffset = _shiftSoFar % 64; - immutable size_t iterations = min(array.length, widthInBits); - - for (size_t i = 0; i < iterations; i++) { - immutable bool isLastCell = vectorArrayIndex == _data.length - 1; - immutable int bitsInVectorCell = isLastCell ? bitsInLastCell : 64; - - if (vectorOffset <= bitsInVectorCell - 8) { - for (size_t j = i; j < array.length; j += widthInBits) { - _data[vectorArrayIndex] ^= cast(ulong) array[j] << vectorOffset; - } - } else { - int index1 = vectorArrayIndex; - int index2 = isLastCell ? 0 : (vectorArrayIndex + 1); - ubyte low = cast(ubyte) (bitsInVectorCell - vectorOffset); - - ubyte xoredByte = 0; - for (size_t j = i; j < array.length; j += widthInBits) { - xoredByte ^= array[j]; - } - - _data[index1] ^= cast(ulong) xoredByte << vectorOffset; - _data[index2] ^= cast(ulong) xoredByte >> low; - } - - vectorOffset += shift; - if (vectorOffset >= bitsInVectorCell) { - vectorArrayIndex = isLastCell ? 0 : vectorArrayIndex + 1; - vectorOffset -= bitsInVectorCell; - } - } - - _shiftSoFar = cast(int) (_shiftSoFar + shift * (array.length % widthInBits)) % widthInBits; - _lengthSoFar += array.length; - - } - - nothrow @safe void start() - { - _data = _data.init; - _shiftSoFar = 0; - _lengthSoFar = 0; - } - - nothrow @trusted ubyte[lengthInBytes] finish() - { - ubyte[lengthInBytes] tmp; - tmp[0 .. lengthInBytes] = (cast(ubyte*) _data)[0 .. lengthInBytes]; - for (size_t i = 0; i < 8; i++) { - tmp[lengthInBytes - 8 + i] ^= (cast(ubyte*) &_lengthSoFar)[i]; - } - return tmp; - } -} - -unittest -{ - assert(isDigest!QuickXor); -} - -unittest -{ - QuickXor qxor; - qxor.put(cast(ubyte[]) "The quick brown fox jumps over the lazy dog"); - assert(qxor.finish().toHexString() == "6CC4A56F2B26C492FA4BBE57C1F31C4193A972BE"); -} - -alias QuickXorDigest = WrapperDigest!(QuickXor); diff --git a/src/selective.d b/src/selective.d deleted file mode 100644 index 55be94eb..00000000 --- a/src/selective.d +++ /dev/null @@ -1,422 +0,0 @@ -import std.algorithm; -import std.array; -import std.file; -import std.path; -import std.regex; -import std.stdio; -import std.string; -import util; -import log; - -final class SelectiveSync -{ - private string[] paths; - private string[] businessSharedFoldersList; - private Regex!char mask; - private Regex!char dirmask; - private bool skipDirStrictMatch = false; - private bool skipDotfiles = false; - - // load sync_list file - void load(string filepath) - { - if (exists(filepath)) { - // open file as read only - auto file = File(filepath, "r"); - auto range = file.byLine(); - foreach (line; range) { - // Skip comments in file - if (line.length == 0 || line[0] == ';' || line[0] == '#') continue; - paths ~= buildNormalizedPath(line); - } - file.close(); - } - } - - // Configure skipDirStrictMatch if function is called - // By default, skipDirStrictMatch = false; - void setSkipDirStrictMatch() - { - skipDirStrictMatch = true; - } - - // load business_shared_folders file - void loadSharedFolders(string filepath) - { - if (exists(filepath)) { - // open file as read only - auto file = File(filepath, "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(); - } - } - - void setFileMask(const(char)[] mask) - { - this.mask = wild2regex(mask); - } - - void setDirMask(const(char)[] dirmask) - { - this.dirmask = wild2regex(dirmask); - } - - // Configure skipDotfiles if function is called - // By default, skipDotfiles = false; - void setSkipDotfiles() - { - skipDotfiles = true; - } - - // return value of skipDotfiles - bool getSkipDotfiles() - { - return skipDotfiles; - } - - // config file skip_dir parameter - bool isDirNameExcluded(string name) - { - // Does the directory name match skip_dir config entry? - // Returns true if the name matches a skip_dir config entry - // Returns false if no match - log.vdebug("skip_dir evaluation for: ", name); - - // 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) { - 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 - return false; - } - - // config file skip_file parameter - bool isFileNameExcluded(string name) - { - // Does the file name match skip_file config entry? - // Returns true if the name matches a skip_file config entry - // Returns false if no match - log.vdebug("skip_file evaluation for: ", name); - - // Try full path match first - if (!name.matchFirst(mask).empty) { - return true; - } else { - // check just the file name - string filename = baseName(name); - if(!filename.matchFirst(mask).empty) { - return true; - } - } - // no match - return false; - } - - // Match against sync_list only - bool isPathExcludedViaSyncList(string path) - { - // Debug output that we are performing a 'sync_list' inclusion / exclusion test - return .isPathExcluded(path, paths); - } - - // Match against skip_dir, skip_file & sync_list entries - bool isPathExcludedMatchAll(string path) - { - return .isPathExcluded(path, paths) || .isPathMatched(path, mask) || .isPathMatched(path, dirmask); - } - - // is the path a dotfile? - bool isDotFile(string path) - { - // always allow the root - if (path == ".") return false; - - path = buildNormalizedPath(path); - auto paths = pathSplitter(path); - foreach(base; paths) { - if (startsWith(base, ".")){ - return true; - } - } - return false; - } - - // is business shared folder matched - bool isSharedFolderMatched(string name) - { - // if there are no shared folder always return false - if (businessSharedFoldersList.empty) return false; - - if (!name.matchFirst(businessSharedFoldersList).empty) { - return true; - } else { - // try a direct comparison just in case - foreach (userFolder; businessSharedFoldersList) { - if (userFolder == name) { - // direct match - log.vdebug("'matchFirst' failed to match, however direct comparison was matched: ", name); - return true; - } - } - return false; - } - } - - // is business shared folder included - bool isPathIncluded(string path, string[] allowedPaths) - { - // always allow the root - if (path == ".") return true; - // if there are no allowed paths always return true - if (allowedPaths.empty) return true; - - path = buildNormalizedPath(path); - foreach (allowed; allowedPaths) { - auto comm = commonPrefix(path, allowed); - if (comm.length == path.length) { - // the given path is contained in an allowed path - return true; - } - if (comm.length == allowed.length && path[comm.length] == '/') { - // the given path is a subitem of an allowed path - return true; - } - } - return false; - } -} - -// test if the given path is not included in the allowed paths -// if there are no allowed paths always return false -private bool isPathExcluded(string path, string[] allowedPaths) -{ - // function variables - bool exclude = false; - bool exludeDirectMatch = false; // will get updated to true, if there is a pattern match to sync_list entry - bool excludeMatched = false; // will get updated to true, if there is a pattern match to sync_list entry - bool finalResult = true; // will get updated to false, if pattern match to sync_list entry - int offset; - string wildcard = "*"; - - // always allow the root - if (path == ".") return false; - // if there are no allowed paths always return false - if (allowedPaths.empty) return false; - path = buildNormalizedPath(path); - log.vdebug("Evaluation against 'sync_list' for this path: ", path); - log.vdebug("[S]exclude = ", exclude); - log.vdebug("[S]exludeDirectMatch = ", exludeDirectMatch); - log.vdebug("[S]excludeMatched = ", excludeMatched); - - // unless path is an exact match, entire sync_list entries need to be processed to ensure - // negative matches are also correctly detected - foreach (allowedPath; allowedPaths) { - // is this an inclusion path or finer grained exclusion? - switch (allowedPath[0]) { - case '-': - // sync_list path starts with '-', this user wants to exclude this path - exclude = true; - // If the sync_list entry starts with '-/' offset needs to be 2, else 1 - if (startsWith(allowedPath, "-/")){ - // Offset needs to be 2 - offset = 2; - } else { - // Offset needs to be 1 - offset = 1; - } - break; - case '!': - // sync_list path starts with '!', this user wants to exclude this path - exclude = true; - // If the sync_list entry starts with '!/' offset needs to be 2, else 1 - if (startsWith(allowedPath, "!/")){ - // Offset needs to be 2 - offset = 2; - } else { - // Offset needs to be 1 - offset = 1; - } - break; - case '/': - // sync_list path starts with '/', this user wants to include this path - // but a '/' at the start causes matching issues, so use the offset for comparison - exclude = false; - offset = 1; - break; - - default: - // no negative pattern, default is to not exclude - exclude = false; - offset = 0; - } - - // What are we comparing against? - log.vdebug("Evaluation against 'sync_list' entry: ", allowedPath); - - // Generate the common prefix from the path vs the allowed path - auto comm = commonPrefix(path, allowedPath[offset..$]); - - // Is path is an exact match of the allowed path? - if (comm.length == path.length) { - // we have a potential exact match - // strip any potential '/*' from the allowed path, to avoid a potential lesser common match - string strippedAllowedPath = strip(allowedPath[offset..$], "/*"); - - if (path == strippedAllowedPath) { - // we have an exact path match - log.vdebug("exact path match"); - if (!exclude) { - log.vdebug("Evaluation against 'sync_list' result: direct match"); - finalResult = false; - // direct match, break and go sync - break; - } else { - log.vdebug("Evaluation against 'sync_list' result: direct match - path to be excluded"); - // do not set excludeMatched = true here, otherwise parental path also gets excluded - // flag exludeDirectMatch so that a 'wildcard match' will not override this exclude - exludeDirectMatch = true; - // final result - finalResult = true; - } - } else { - // no exact path match, but something common does match - log.vdebug("something 'common' matches the input path"); - auto splitAllowedPaths = pathSplitter(strippedAllowedPath); - string pathToEvaluate = ""; - foreach(base; splitAllowedPaths) { - pathToEvaluate ~= base; - if (path == pathToEvaluate) { - // The input path matches what we want to evaluate against as a direct match - if (!exclude) { - log.vdebug("Evaluation against 'sync_list' result: direct match for parental path item"); - finalResult = false; - // direct match, break and go sync - break; - } else { - log.vdebug("Evaluation against 'sync_list' result: direct match for parental path item but to be excluded"); - finalResult = true; - // do not set excludeMatched = true here, otherwise parental path also gets excluded - } - } - pathToEvaluate ~= dirSeparator; - } - } - } - - // Is path is a subitem/sub-folder of the allowed path? - if (comm.length == allowedPath[offset..$].length) { - // The given path is potentially a subitem of an allowed path - // We want to capture sub-folders / files of allowed paths here, but not explicitly match other items - // if there is no wildcard - auto subItemPathCheck = allowedPath[offset..$] ~ "/"; - if (canFind(path, subItemPathCheck)) { - // The 'path' includes the allowed path, and is 'most likely' a sub-path item - if (!exclude) { - log.vdebug("Evaluation against 'sync_list' result: parental path match"); - finalResult = false; - // parental path matches, break and go sync - break; - } else { - log.vdebug("Evaluation against 'sync_list' result: parental path match but must be excluded"); - finalResult = true; - excludeMatched = true; - } - } - } - - // Does the allowed path contain a wildcard? (*) - if (canFind(allowedPath[offset..$], wildcard)) { - // allowed path contains a wildcard - // manually replace '*' for '.*' to be compatible with regex - string regexCompatiblePath = replace(allowedPath[offset..$], "*", ".*"); - auto allowedMask = regex(regexCompatiblePath); - if (matchAll(path, allowedMask)) { - // regex wildcard evaluation matches - // if we have a prior pattern match for an exclude, excludeMatched = true - if (!exclude && !excludeMatched && !exludeDirectMatch) { - // nothing triggered an exclusion before evaluation against wildcard match attempt - log.vdebug("Evaluation against 'sync_list' result: wildcard pattern match"); - finalResult = false; - } else { - log.vdebug("Evaluation against 'sync_list' result: wildcard pattern matched but must be excluded"); - finalResult = true; - excludeMatched = true; - } - } - } - } - // Interim results - log.vdebug("[F]exclude = ", exclude); - log.vdebug("[F]exludeDirectMatch = ", exludeDirectMatch); - log.vdebug("[F]excludeMatched = ", excludeMatched); - - // If exclude or excludeMatched is true, then finalResult has to be true - if ((exclude) || (excludeMatched) || (exludeDirectMatch)) { - finalResult = true; - } - - // results - if (finalResult) { - log.vdebug("Evaluation against 'sync_list' final result: EXCLUDED"); - } else { - log.vdebug("Evaluation against 'sync_list' final result: included for sync"); - } - return finalResult; -} - -// test if the given path is matched by the regex expression. -// recursively test up the tree. -private bool isPathMatched(string path, Regex!char mask) { - path = buildNormalizedPath(path); - auto paths = pathSplitter(path); - - string prefix = ""; - foreach(base; paths) { - prefix ~= base; - if (!path.matchFirst(mask).empty) { - // the given path matches something which we should skip - return true; - } - prefix ~= dirSeparator; - } - return false; -} - -// unit tests -unittest -{ - assert(isPathExcluded("Documents2", ["Documents"])); - assert(!isPathExcluded("Documents", ["Documents"])); - assert(!isPathExcluded("Documents/a.txt", ["Documents"])); - assert(isPathExcluded("Hello/World", ["Hello/John"])); - assert(!isPathExcluded(".", ["Documents"])); -} diff --git a/src/sqlite.d b/src/sqlite.d deleted file mode 100644 index 5e1839ec..00000000 --- a/src/sqlite.d +++ /dev/null @@ -1,256 +0,0 @@ -module sqlite; -import std.stdio; -import etc.c.sqlite3; -import std.string: fromStringz, toStringz; -import core.stdc.stdlib; -import std.conv; -static import log; - -extern (C) immutable(char)* sqlite3_errstr(int); // missing from the std library - -static this() -{ - if (sqlite3_libversion_number() < 3006019) { - throw new SqliteException("sqlite 3.6.19 or newer is required"); - } -} - -private string ifromStringz(const(char)* cstr) -{ - return fromStringz(cstr).dup; -} - -class SqliteException: Exception -{ - @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) - { - super(msg, file, line, next); - } - - @safe pure nothrow this(string msg, Throwable next, string file = __FILE__, size_t line = __LINE__) - { - super(msg, file, line, next); - } -} - -struct Database -{ - private sqlite3* pDb; - - this(const(char)[] filename) - { - open(filename); - } - - ~this() - { - close(); - } - - int db_checkpoint() - { - return sqlite3_wal_checkpoint(pDb, null); - } - - void dump_open_statements() - { - log.log("Dumpint open statements: \n"); - auto p = sqlite3_next_stmt(pDb, null); - while (p != null) { - log.log (" - " ~ ifromStringz(sqlite3_sql(p)) ~ "\n"); - p = sqlite3_next_stmt(pDb, p); - } - } - - - void open(const(char)[] filename) - { - // https://www.sqlite.org/c3ref/open.html - int rc = sqlite3_open(toStringz(filename), &pDb); - if (rc == SQLITE_CANTOPEN) { - // Database cannot be opened - log.error("\nThe database cannot be opened. Please check the permissions of ~/.config/onedrive/items.sqlite3\n"); - close(); - exit(-1); - } - if (rc != SQLITE_OK) { - log.error("\nA database access error occurred: " ~ getErrorMessage() ~ "\n"); - close(); - exit(-1); - } - sqlite3_extended_result_codes(pDb, 1); // always use extended result codes - } - - void exec(const(char)[] sql) - { - // https://www.sqlite.org/c3ref/exec.html - int rc = sqlite3_exec(pDb, toStringz(sql), null, null, null); - if (rc != SQLITE_OK) { - log.error("\nA database execution error occurred: "~ getErrorMessage() ~ "\n"); - log.error("Please retry your command with --resync to fix any local database corruption issues.\n"); - close(); - exit(-1); - } - } - - int getVersion() - { - int userVersion; - extern (C) int callback(void* user_version, int count, char** column_text, char** column_name) { - import core.stdc.stdlib: atoi; - *(cast(int*) user_version) = atoi(*column_text); - return 0; - } - int rc = sqlite3_exec(pDb, "PRAGMA user_version", &callback, &userVersion, null); - if (rc != SQLITE_OK) { - throw new SqliteException(ifromStringz(sqlite3_errmsg(pDb))); - } - return userVersion; - } - - string getErrorMessage() - { - return ifromStringz(sqlite3_errmsg(pDb)); - } - - void setVersion(int userVersion) - { - import std.conv: to; - exec("PRAGMA user_version=" ~ to!string(userVersion)); - } - - Statement prepare(const(char)[] zSql) - { - Statement s; - // https://www.sqlite.org/c3ref/prepare.html - int rc = sqlite3_prepare_v2(pDb, zSql.ptr, cast(int) zSql.length, &s.pStmt, null); - if (rc != SQLITE_OK) { - throw new SqliteException(ifromStringz(sqlite3_errmsg(pDb))); - } - return s; - } - - void close() - { - // https://www.sqlite.org/c3ref/close.html - sqlite3_close_v2(pDb); - pDb = null; - } -} - -struct Statement -{ - struct Result - { - private sqlite3_stmt* pStmt; - private const(char)[][] row; - - private this(sqlite3_stmt* pStmt) - { - this.pStmt = pStmt; - step(); // initialize the range - } - - @property bool empty() - { - return row.length == 0; - } - - @property auto front() - { - return row; - } - - alias step popFront; - - void step() - { - // https://www.sqlite.org/c3ref/step.html - int rc = sqlite3_step(pStmt); - if (rc == SQLITE_BUSY) { - // Database is locked by another onedrive process - log.error("The database is currently locked by another process - cannot sync"); - return; - } - if (rc == SQLITE_DONE) { - row.length = 0; - } else if (rc == SQLITE_ROW) { - // https://www.sqlite.org/c3ref/data_count.html - int count = 0; - count = sqlite3_data_count(pStmt); - row = new const(char)[][count]; - foreach (size_t i, ref column; row) { - // https://www.sqlite.org/c3ref/column_blob.html - column = fromStringz(sqlite3_column_text(pStmt, to!int(i))); - } - } else { - string errorMessage = ifromStringz(sqlite3_errmsg(sqlite3_db_handle(pStmt))); - log.error("\nA database statement execution error occurred: "~ errorMessage ~ "\n"); - log.error("Please retry your command with --resync to fix any local database corruption issues.\n"); - exit(-1); - } - } - } - - private sqlite3_stmt* pStmt; - - ~this() - { - // https://www.sqlite.org/c3ref/finalize.html - sqlite3_finalize(pStmt); - } - - void bind(int index, const(char)[] value) - { - reset(); - // https://www.sqlite.org/c3ref/bind_blob.html - int rc = sqlite3_bind_text(pStmt, index, value.ptr, cast(int) value.length, SQLITE_STATIC); - if (rc != SQLITE_OK) { - throw new SqliteException(ifromStringz(sqlite3_errmsg(sqlite3_db_handle(pStmt)))); - } - } - - Result exec() - { - reset(); - return Result(pStmt); - } - - private void reset() - { - // https://www.sqlite.org/c3ref/reset.html - int rc = sqlite3_reset(pStmt); - if (rc != SQLITE_OK) { - throw new SqliteException(ifromStringz(sqlite3_errmsg(sqlite3_db_handle(pStmt)))); - } - } -} - -unittest -{ - auto db = Database(":memory:"); - db.exec("CREATE TABLE test( - id TEXT PRIMARY KEY, - value TEXT - )"); - - assert(db.getVersion() == 0); - db.setVersion(1); - assert(db.getVersion() == 1); - - auto s = db.prepare("INSERT INTO test VALUES (?, ?)"); - s.bind(1, "key1"); - s.bind(2, "value"); - s.exec(); - s.bind(1, "key2"); - s.bind(2, null); - s.exec(); - - s = db.prepare("SELECT * FROM test ORDER BY id ASC"); - auto r = s.exec(); - assert(r.front[0] == "key1"); - r.popFront(); - assert(r.front[1] == null); - r.popFront(); - assert(r.empty); -} diff --git a/src/sync.d b/src/sync.d deleted file mode 100644 index 346d8c00..00000000 --- a/src/sync.d +++ /dev/null @@ -1,7302 +0,0 @@ -import std.algorithm; -import std.array: array; -import std.datetime; -import std.exception: enforce; -import std.file, std.json, std.path; -import std.regex; -import std.stdio, std.string, std.uni, std.uri; -import std.conv; -import std.encoding; -import core.time, core.thread; -import core.stdc.stdlib; -import config, itemdb, onedrive, selective, upload, util; -static import log; - -// threshold after which files will be uploaded using an upload session -private long thresholdFileSize = 4 * 2^^20; // 4 MiB - -// flag to set whether local files should be deleted from OneDrive -private bool noRemoteDelete = false; - -// flag to set whether the local file should be deleted once it is successfully uploaded to OneDrive -private bool localDeleteAfterUpload = false; - -// flag to set if we are running as uploadOnly -private bool uploadOnly = false; - -// Do we configure to disable the upload validation routine -private bool disableUploadValidation = false; - -// Do we configure to disable the download validation routine -private bool disableDownloadValidation = false; - -// Do we perform a local cleanup of files that are 'extra' on the local file system, when using --download-only -private bool cleanupLocalFiles = false; - -private bool isItemFolder(const ref JSONValue item) -{ - return ("folder" in item) != null; -} - -private bool isItemFile(const ref JSONValue item) -{ - return ("file" in item) != null; -} - -private bool isItemDeleted(const ref JSONValue item) -{ - return ("deleted" in item) != null; -} - -private bool isItemRoot(const ref JSONValue item) -{ - return ("root" in item) != null; -} - -private bool isItemRemote(const ref JSONValue item) -{ - return ("remoteItem" in item) != null; -} - -private bool hasParentReference(const ref JSONValue item) -{ - return ("parentReference" in item) != null; -} - -private bool hasParentReferenceId(const ref JSONValue item) -{ - return ("id" in item["parentReference"]) != null; -} - -private bool hasParentReferencePath(const ref JSONValue item) -{ - return ("path" in item["parentReference"]) != null; -} - -private bool isMalware(const ref JSONValue item) -{ - return ("malware" in item) != null; -} - -private bool hasFileSize(const ref JSONValue item) -{ - return ("size" in item) != null; -} - -private bool hasId(const ref JSONValue item) -{ - return ("id" in item) != null; -} - -private bool hasHashes(const ref JSONValue item) -{ - return ("hashes" in item["file"]) != null; -} - -private bool hasQuickXorHash(const ref JSONValue item) -{ - return ("quickXorHash" in item["file"]["hashes"]) != null; -} - -private bool hasSHA256Hash(const ref JSONValue item) -{ - return ("sha256Hash" in item["file"]["hashes"]) != null; -} - -private bool isDotFile(const(string) path) -{ - // always allow the root - if (path == ".") return false; - auto paths = pathSplitter(buildNormalizedPath(path)); - foreach(base; paths) { - if (startsWith(base, ".")){ - return true; - } - } - return false; -} - -// construct an Item struct from a JSON driveItem -private Item makeDatabaseItem(const ref JSONValue driveItem) -{ - Item item = { - id: driveItem["id"].str, - name: "name" in driveItem ? driveItem["name"].str : null, // name may be missing for deleted files in OneDrive Biz - eTag: "eTag" in driveItem ? driveItem["eTag"].str : null, // eTag is not returned for the root in OneDrive Biz - cTag: "cTag" in driveItem ? driveItem["cTag"].str : null, // cTag is missing in old files (and all folders in OneDrive Biz) - }; - - // OneDrive API Change: https://github.com/OneDrive/onedrive-api-docs/issues/834 - // OneDrive no longer returns lastModifiedDateTime if the item is deleted by OneDrive - if(isItemDeleted(driveItem)){ - // Set mtime to SysTime(0) - item.mtime = SysTime(0); - } else { - // Item is not in a deleted state - // 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 - // Normally, the 'remoteItem' field will contain 'fileSystemInfo' however, if the user uses the 'Add Shortcut ..' option in OneDrive WebUI - // to create a 'link', this object, whilst remote, does not have 'fileSystemInfo' in the expected place, thus leading to a application crash - // See: https://github.com/abraunegg/onedrive/issues/1533 - if ("fileSystemInfo" in driveItem["remoteItem"]) { - // 'fileSystemInfo' is in 'remoteItem' which will be the majority of cases - item.mtime = SysTime.fromISOExtString(driveItem["remoteItem"]["fileSystemInfo"]["lastModifiedDateTime"].str); - } else { - // is a remote item, but 'fileSystemInfo' is missing from 'remoteItem' - item.mtime = SysTime.fromISOExtString(driveItem["fileSystemInfo"]["lastModifiedDateTime"].str); - } - } else { - // item exists on account default drive id - item.mtime = SysTime.fromISOExtString(driveItem["fileSystemInfo"]["lastModifiedDateTime"].str); - } - } - - if (isItemFile(driveItem)) { - item.type = ItemType.file; - } else if (isItemFolder(driveItem)) { - item.type = ItemType.dir; - } else if (isItemRemote(driveItem)) { - item.type = ItemType.remote; - } else { - // do not throw exception, item will be removed in applyDifferences() - } - - // root and remote items do not have parentReference - if (!isItemRoot(driveItem) && ("parentReference" in driveItem) != null) { - item.driveId = driveItem["parentReference"]["driveId"].str; - if (hasParentReferenceId(driveItem)) { - item.parentId = driveItem["parentReference"]["id"].str; - } - } - - // extract the file hash - if (isItemFile(driveItem) && ("hashes" in driveItem["file"])) { - // Get quickXorHash - if ("quickXorHash" in driveItem["file"]["hashes"]) { - item.quickXorHash = driveItem["file"]["hashes"]["quickXorHash"].str; - } else { - log.vdebug("quickXorHash is missing from ", driveItem["id"].str); - } - // sha256Hash - if ("sha256Hash" in driveItem["file"]["hashes"]) { - item.sha256Hash = driveItem["file"]["hashes"]["sha256Hash"].str; - } else { - log.vdebug("sha256Hash is missing from ", driveItem["id"].str); - } - } - - if (isItemRemote(driveItem)) { - item.remoteDriveId = driveItem["remoteItem"]["parentReference"]["driveId"].str; - item.remoteId = driveItem["remoteItem"]["id"].str; - } - - // National Cloud Deployments do not support /delta as a query - // Thus we need to track in the database that this item is in sync - // As we are making an item, set the syncStatus to Y - // ONLY when using a National Cloud Deployment, all the existing DB entries will get set to N - // so when processing /children, it can be identified what the 'deleted' difference is - item.syncStatus = "Y"; - - return item; -} - -private bool testFileHash(const(string) path, const ref Item item) -{ - // Generate QuickXORHash first before others - if (item.quickXorHash) { - if (item.quickXorHash == computeQuickXorHash(path)) return true; - } else if (item.sha256Hash) { - if (item.sha256Hash == computeSHA256Hash(path)) return true; - } - return false; -} - -class SyncException: Exception -{ - @nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__) - { - super(msg, file, line); - } -} - -final class SyncEngine -{ - private Config cfg; - private OneDriveApi onedrive; - private ItemDatabase itemdb; - private UploadSession session; - private SelectiveSync selectiveSync; - // list of items to skip while applying the changes - private string[] skippedItems; - // list of items to delete after the changes has been downloaded - private string[2][] idsToDelete; - // list of items we fake created when using --dry-run - private string[2][] idsFaked; - // list of directory names changed online, but not changed locally when using --dry-run - private string[] pathsRenamed; - // default drive id - private string defaultDriveId; - // default root id - private string defaultRootId; - // type of OneDrive account - private string accountType; - // free space remaining at init() - private long remainingFreeSpace; - // file size limit for a new file - private long newSizeLimit; - // is file malware flag - private bool malwareDetected = false; - // download filesystem issue flag - private bool downloadFailed = false; - // upload failure - OneDrive or filesystem issue (reading data) - private bool uploadFailed = false; - // initialization has been done - private bool initDone = false; - // sync engine dryRun flag - private bool dryRun = false; - // quota details available - private bool quotaAvailable = true; - // quota details restricted - private bool quotaRestricted = false; - // sync business shared folders flag - private bool syncBusinessFolders = false; - // single directory scope flag - private bool singleDirectoryScope = false; - // is sync_list configured - private bool syncListConfigured = false; - // sync_list new folder added, trigger delta scan override - private bool oneDriveFullScanTrigger = false; - // is bypass_data_preservation set via config file - // Local data loss MAY occur in this scenario - private bool bypassDataPreservation = false; - // is National Cloud Deployments configured - private bool nationalCloudDeployment = false; - // has performance processing timings been requested - private bool displayProcessingTime = false; - // array of all OneDrive driveId's for use with OneDrive Business Folders - private string[] driveIDsArray; - - this(Config cfg, OneDriveApi onedrive, ItemDatabase itemdb, SelectiveSync selectiveSync) - { - assert(onedrive && itemdb && selectiveSync); - this.cfg = cfg; - this.onedrive = onedrive; - this.itemdb = itemdb; - this.selectiveSync = selectiveSync; - // session = UploadSession(onedrive, cfg.uploadStateFilePath); - this.dryRun = cfg.getValueBool("dry_run"); - this.newSizeLimit = cfg.getValueLong("skip_size") * 2^^20; - this.newSizeLimit = (this.newSizeLimit == 0) ? long.max : this.newSizeLimit; - } - - void reset() - { - initDone=false; - } - - void init() - { - // Set accountType, defaultDriveId, defaultRootId & remainingFreeSpace once and reuse where possible - JSONValue oneDriveDetails; - JSONValue oneDriveRootDetails; - - if (initDone) { - return; - } - - session = UploadSession(onedrive, cfg.uploadStateFilePath); - - // Need to catch 400 or 5xx server side errors at initialization - // Get Default Drive - try { - oneDriveDetails = onedrive.getDefaultDrive(); - } catch (OneDriveException e) { - log.vdebug("oneDriveDetails = onedrive.getDefaultDrive() generated a OneDriveException"); - if (e.httpStatusCode == 400) { - // OneDrive responded with 400 error: Bad Request - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - - // Check this - if (cfg.getValueString("drive_id").length) { - writeln(); - log.error("ERROR: Check your 'drive_id' entry in your configuration file as it may be incorrect"); - writeln(); - } - // Must exit here - onedrive.shutdown(); - exit(-1); - } - if (e.httpStatusCode == 401) { - // HTTP request returned status code 401 (Unauthorized) - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - handleClientUnauthorised(); - } - 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 init();"); - init(); - // return back to original call - return; - } - if (e.httpStatusCode >= 500) { - // There was a HTTP 5xx Server Side Error - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - // Must exit here - onedrive.shutdown(); - exit(-1); - } - } - - // Get Default Root - try { - oneDriveRootDetails = onedrive.getDefaultRoot(); - } catch (OneDriveException e) { - log.vdebug("oneDriveRootDetails = onedrive.getDefaultRoot() generated a OneDriveException"); - if (e.httpStatusCode == 400) { - // OneDrive responded with 400 error: Bad Request - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - // Check this - if (cfg.getValueString("drive_id").length) { - writeln(); - log.error("ERROR: Check your 'drive_id' entry in your configuration file as it may be incorrect"); - writeln(); - } - // Must exit here - onedrive.shutdown(); - exit(-1); - } - if (e.httpStatusCode == 401) { - // HTTP request returned status code 401 (Unauthorized) - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - handleClientUnauthorised(); - } - 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 init();"); - init(); - // return back to original call - return; - } - if (e.httpStatusCode >= 500) { - // There was a HTTP 5xx Server Side Error - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - // Must exit here - onedrive.shutdown(); - exit(-1); - } - } - - if ((oneDriveDetails.type() == JSONType.object) && (oneDriveRootDetails.type() == JSONType.object) && (hasId(oneDriveDetails)) && (hasId(oneDriveRootDetails))) { - // JSON elements are valid - // Debug OneDrive Account details response - log.vdebug("OneDrive Account Details: ", oneDriveDetails); - log.vdebug("OneDrive Account Root Details: ", oneDriveRootDetails); - - // Successfully got details from OneDrive without a server side error such as 'HTTP/1.1 500 Internal Server Error' or 'HTTP/1.1 504 Gateway Timeout' - accountType = oneDriveDetails["driveType"].str; - defaultDriveId = oneDriveDetails["id"].str; - defaultRootId = oneDriveRootDetails["id"].str; - - // get the remaining size from OneDrive API - if ("remaining" in oneDriveDetails["quota"]){ - // use the value provided - remainingFreeSpace = oneDriveDetails["quota"]["remaining"].integer; - } else { - // set at zero - remainingFreeSpace = 0; - } - - // Make sure that defaultDriveId is in our driveIDs array to use when checking if item is in database - // Keep the driveIDsArray with unique entries only - if (!canFind(driveIDsArray, defaultDriveId)) { - // Add this drive id to the array to search with - driveIDsArray ~= defaultDriveId; - } - - // In some cases OneDrive Business configurations 'restrict' quota details thus is empty / blank / negative value / zero - if (remainingFreeSpace <= 0) { - // free space is <= 0 .. why ? - if ("remaining" in oneDriveDetails["quota"]){ - // json response contained a 'remaining' value - if (accountType == "personal"){ - // zero space available - log.error("ERROR: OneDrive account currently has zero space available. Please free up some space online."); - quotaAvailable = false; - } else { - // zero space available is being reported, maybe being restricted? - log.error("WARNING: OneDrive quota information is being restricted or providing a zero value. Please fix by speaking to your OneDrive / Office 365 Administrator."); - quotaRestricted = true; - } - } else { - // json response was missing a 'remaining' value - if (accountType == "personal"){ - log.error("ERROR: OneDrive quota information is missing. Potentially your OneDrive account currently has zero space available. Please free up some space online."); - quotaAvailable = false; - } else { - // quota details not available - log.error("ERROR: OneDrive quota information is being restricted. Please fix by speaking to your OneDrive / Office 365 Administrator."); - quotaRestricted = true; - } - } - } - - // Display accountType, defaultDriveId, defaultRootId & remainingFreeSpace for verbose logging purposes - log.vlog("Application version: ", strip(import("version"))); - log.vlog("Account Type: ", accountType); - log.vlog("Default Drive ID: ", defaultDriveId); - log.vlog("Default Root ID: ", defaultRootId); - - // What do we display here - if (remainingFreeSpace > 0) { - // Display the actual value - log.vlog("Remaining Free Space: ", remainingFreeSpace); - } else { - // zero or non-zero value or restricted - if (!quotaRestricted){ - log.vlog("Remaining Free Space: 0"); - } else { - log.vlog("Remaining Free Space: Not Available"); - } - } - - // If account type is documentLibrary - then most likely this is a SharePoint repository - // and files 'may' be modified after upload. See: https://github.com/abraunegg/onedrive/issues/205 - if(accountType == "documentLibrary") { - // set this flag for SharePoint regardless of --disable-upload-validation being used - setDisableUploadValidation(); - } - - // Check the local database to ensure the OneDrive Root details are in the database - checkDatabaseForOneDriveRoot(); - - // Check if there is an interrupted upload session - if (session.restore()) { - log.log("Continuing the upload session ..."); - string uploadSessionLocalFilePath = session.getUploadSessionLocalFilePath(); - auto item = session.upload(); - - // is 'item' a valid JSON response and not null - if (item.type() == JSONType.object) { - // Upload did not fail, JSON response contains data - // Are we in an --upload-only & --remove-source-files scenario? - // Use actual config values as we are doing an upload session recovery - if ((cfg.getValueBool("upload_only")) && (cfg.getValueBool("remove_source_files"))) { - // Log that we are deleting a local item - log.log("Removing local file as --upload-only & --remove-source-files configured"); - // are we in a --dry-run scenario? - if (!dryRun) { - // No --dry-run ... process local file delete - if (!uploadSessionLocalFilePath.empty) { - // only perform the delete if we have a valid file path - if (exists(uploadSessionLocalFilePath)) { - // file exists - log.vdebug("Removing local file: ", uploadSessionLocalFilePath); - safeRemove(uploadSessionLocalFilePath); - } - } - } - // as file is removed, we have nothing to add to the local database - log.vdebug("Skipping adding to database as --upload-only & --remove-source-files configured"); - } else { - // save the item - saveItem(item); - } - } else { - // JSON response was not valid, upload failed - log.error("ERROR: File failed to upload. Increase logging verbosity to determine why."); - } - } - initDone = true; - } else { - // init failure - initDone = false; - // log why - log.error("ERROR: Unable to query OneDrive to initialize application"); - // Debug OneDrive Account details response - log.vdebug("OneDrive Account Details: ", oneDriveDetails); - log.vdebug("OneDrive Account Root Details: ", oneDriveRootDetails); - // Must exit here - onedrive.shutdown(); - exit(-1); - } - } - - // Configure uploadOnly if function is called - // By default, uploadOnly = false; - void setUploadOnly() - { - uploadOnly = true; - } - - // Configure noRemoteDelete if function is called - // By default, noRemoteDelete = false; - // Meaning we will process local deletes to delete item on OneDrive - void setNoRemoteDelete() - { - noRemoteDelete = true; - } - - // Configure localDeleteAfterUpload if function is called - // By default, localDeleteAfterUpload = false; - // Meaning we will not delete any local file after upload is successful - void setLocalDeleteAfterUpload() - { - localDeleteAfterUpload = true; - } - - // set the flag that we are going to sync business shared folders - void setSyncBusinessFolders() - { - syncBusinessFolders = true; - } - - // Configure singleDirectoryScope if function is called - // By default, singleDirectoryScope = false - void setSingleDirectoryScope() - { - singleDirectoryScope = true; - } - - // Configure disableUploadValidation if function is called - // By default, disableUploadValidation = false; - // Meaning we will always validate our uploads - // However, when uploading a file that can contain metadata SharePoint will associate some - // metadata from the library the file is uploaded to directly in the file - // which breaks this validation. See https://github.com/abraunegg/onedrive/issues/205 - void setDisableUploadValidation() - { - disableUploadValidation = true; - log.vdebug("documentLibrary account type - flagging to disable upload validation checks due to Microsoft SharePoint file modification enrichments"); - } - - // Configure disableDownloadValidation if function is called - // By default, disableDownloadValidation = false; - // Meaning we will always validate our downloads - // However, when downloading files from SharePoint, the OneDrive API will not advise the correct file size - // which means that the application thinks the file download has failed as the size is different / hash is different - // See: https://github.com/abraunegg/onedrive/discussions/1667 - void setDisableDownloadValidation() - { - disableDownloadValidation = true; - log.vdebug("Flagging to disable download validation checks due to user request"); - } - - // Issue #658 Handling - // If an existing folder is moved into a sync_list valid path (where it previously was out of scope due to sync_list), - // then set this flag to true, so that on the second 'true-up' sync, we force a rescan of the OneDrive path to capture any 'files' - void setOneDriveFullScanTrigger() - { - oneDriveFullScanTrigger = true; - log.vdebug("Setting oneDriveFullScanTrigger = true due to new folder creation request in a location that is now in-scope which may have previously out of scope"); - } - - // unset method - void unsetOneDriveFullScanTrigger() - { - oneDriveFullScanTrigger = false; - log.vdebug("Setting oneDriveFullScanTrigger = false"); - } - - // set syncListConfigured to true - void setSyncListConfigured() - { - syncListConfigured = true; - log.vdebug("Setting syncListConfigured = true"); - } - - // set bypassDataPreservation to true - void setBypassDataPreservation() - { - bypassDataPreservation = true; - log.vdebug("Setting bypassDataPreservation = true"); - } - - // set nationalCloudDeployment to true - void setNationalCloudDeployment() - { - nationalCloudDeployment = true; - log.vdebug("Setting nationalCloudDeployment = true"); - } - - // set performance timing flag - void setPerformanceProcessingOutput() - { - displayProcessingTime = true; - log.vdebug("Setting displayProcessingTime = true"); - } - - // get performance timing flag - bool getPerformanceProcessingOutput() - { - return displayProcessingTime; - } - - // set cleanupLocalFiles to true - void setCleanupLocalFiles() - { - cleanupLocalFiles = true; - log.vdebug("Setting cleanupLocalFiles = true"); - } - - // return the OneDrive Account Type - auto getAccountType() - { - // return account type in use - return accountType; - } - - // download all new changes from OneDrive - void applyDifferences(bool performFullItemScan) - { - // Set defaults for the root folder - // Use the global's as initialised via init() rather than performing unnecessary additional HTTPS calls - string driveId = defaultDriveId; - string rootId = defaultRootId; - applyDifferences(driveId, rootId, performFullItemScan); - - // Check OneDrive Personal Shared Folders - if (accountType == "personal"){ - // https://github.com/OneDrive/onedrive-api-docs/issues/764 - Item[] items = itemdb.selectRemoteItems(); - foreach (item; items) { - // Only check path if config is != "" - if (cfg.getValueString("skip_dir") != "") { - // The path that needs to be checked needs to include the '/' - // This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched - if (selectiveSync.isDirNameExcluded(item.name)) { - // This directory name is excluded - log.vlog("Skipping item - excluded by skip_dir config: ", item.name); - continue; - } - } - // Directory name is not excluded or skip_dir is not populated - log.vdebug("------------------------------------------------------------------"); - if (!cfg.getValueBool("monitor")) { - log.log("Syncing this OneDrive Personal Shared Folder: ", item.name); - } else { - log.vlog("Syncing this OneDrive Personal Shared Folder: ", item.name); - } - // Check this OneDrive Personal Shared Folders - applyDifferences(item.remoteDriveId, item.remoteId, performFullItemScan); - // Keep the driveIDsArray with unique entries only - if (!canFind(driveIDsArray, item.remoteDriveId)) { - // Add this OneDrive Personal Shared Folder driveId array - driveIDsArray ~= item.remoteDriveId; - } - } - } - - // Check OneDrive Business Shared Folders, if configured to do so - if (syncBusinessFolders){ - // query OneDrive Business Shared Folders shared with me - log.vlog("Attempting to sync OneDrive Business Shared Folders"); - JSONValue graphQuery; - try { - graphQuery = onedrive.getSharedWithMe(); - } catch (OneDriveException e) { - if (e.httpStatusCode == 401) { - // HTTP request returned status code 401 (Unauthorized) - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - handleClientUnauthorised(); - } - 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) - graphQuery = onedrive.getSharedWithMe();"); - graphQuery = onedrive.getSharedWithMe(); - } - if (e.httpStatusCode >= 500) { - // There was a HTTP 5xx Server Side Error - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - // Must exit here - onedrive.shutdown(); - exit(-1); - } - } - - if (graphQuery.type() == JSONType.object) { - string sharedFolderName; - foreach (searchResult; graphQuery["value"].array) { - // 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; - } - 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; - // Output Shared Folder Name early - log.vdebug("Shared Folder Name: ", sharedFolderName); - // 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; - - // "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: MATCHED to any entry in 'business_shared_folders'"); - log.vdebug("Parent Drive Id: ", searchResult["remoteItem"]["parentReference"]["driveId"].str); - log.vdebug("Shared Item Id: ", searchResult["remoteItem"]["id"].str); - Item databaseItem; - - // for each driveid in the existing driveIDsArray - foreach (searchDriveId; driveIDsArray) { - log.vdebug("searching database for: ", searchDriveId, " ", sharedFolderName); - if (itemdb.idInLocalDatabase(searchDriveId, searchResult["remoteItem"]["id"].str)){ - // Shared folder is present - log.vdebug("Found shared folder name in database"); - itemInDatabase = true; - // Query the DB for the details of this item - itemdb.selectByPath(sharedFolderName, searchDriveId, databaseItem); - 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))); - - 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 { - // 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 { - log.vdebug("Shared Folder Name: NO MATCH to any entry in 'business_shared_folders'"); - } - } 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 File - Shared By: ", sharedByName, " (", sharedByEmail, ")"); - } else { - if (sharedByName != "") { - log.vlog("OneDrive Business Shared File - Shared By: ", sharedByName); - } - } - } else { - // something else entirely - log.log("WARNING: Not syncing this OneDrive Business Shared item: ", searchResult["name"].str); - } - } - } - } else { - // Log that an invalid JSON object was returned - log.error("ERROR: onedrive.getSharedWithMe call returned an invalid JSON Object"); - } - } - } - - // download all new changes from a specified folder on OneDrive - void applyDifferencesSingleDirectory(const(string) path) - { - // Ensure we check the 'right' location for this directory on OneDrive - // It could come from the following places: - // 1. My OneDrive Root - // 2. My OneDrive Root as an Office 365 Shared Library - // 3. A OneDrive Business Shared Folder - // If 1 & 2, the configured default items are what we need - // If 3, we need to query OneDrive - - string driveId = defaultDriveId; - string rootId = defaultRootId; - string folderId; - string itemId; - JSONValue onedrivePathDetails; - - // Check OneDrive Business Shared Folders, if configured to do so - if (syncBusinessFolders){ - log.vlog("Attempting to sync OneDrive Business Shared Folders"); - // query OneDrive Business Shared Folders shared with me - JSONValue graphQuery; - try { - graphQuery = onedrive.getSharedWithMe(); - } catch (OneDriveException e) { - if (e.httpStatusCode == 401) { - // HTTP request returned status code 401 (Unauthorized) - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - handleClientUnauthorised(); - } - 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) - graphQuery = onedrive.getSharedWithMe();"); - graphQuery = onedrive.getSharedWithMe(); - } - if (e.httpStatusCode >= 500) { - // There was a HTTP 5xx Server Side Error - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - // Must exit here - onedrive.shutdown(); - exit(-1); - } - } - - if (graphQuery.type() == JSONType.object) { - // valid response from OneDrive - string sharedFolderName; - foreach (searchResult; graphQuery["value"].array) { - // set sharedFolderName - sharedFolderName = searchResult["name"].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; - } - if ("email" in searchResult["remoteItem"]["shared"]["sharedBy"]["user"]) { - sharedByEmail = searchResult["remoteItem"]["shared"]["sharedBy"]["user"]["email"].str; - } - } - - // Compare this to values in business_shared_folders - if(selectiveSync.isSharedFolderMatched(sharedFolderName)){ - // Matched sharedFolderName to item in business_shared_folders - log.vdebug("Matched sharedFolderName in business_shared_folders: ", sharedFolderName); - // But is this shared folder what we are looking for as part of --single-directory? - // User could be using 'directory' or 'directory/directory1/directory2/directory3/' - // Can we find 'sharedFolderName' in the given 'path' - if (canFind(path, sharedFolderName)) { - // Found 'sharedFolderName' in the given 'path' - log.vdebug("Matched 'sharedFolderName' in the given 'path'"); - // What was the matched folder JSON - log.vdebug("Matched sharedFolderName in business_shared_folders JSON: ", searchResult); - // Path we want to sync is on a OneDrive Business Shared Folder - // Set the correct driveId - driveId = searchResult["remoteItem"]["parentReference"]["driveId"].str; - // Set this items id - itemId = searchResult["remoteItem"]["id"].str; - log.vdebug("Updated the driveId to a new value: ", driveId); - log.vdebug("Updated the itemId to a new value: ", itemId); - // Keep the driveIDsArray with unique entries only - if (!canFind(driveIDsArray, driveId)) { - // Add this drive id to the array to search with - driveIDsArray ~= driveId; - } - - // 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); - } - } - } - } - } - } else { - // Log that an invalid JSON object was returned - log.error("ERROR: onedrive.getSharedWithMe call returned an invalid JSON Object"); - } - } - - // Test if the path we are going to sync from actually exists on OneDrive - log.vlog("Getting path details from OneDrive ..."); - try { - // Need to use different calls here - one call for majority, another if this is a OneDrive Business Shared Folder - if (!syncBusinessFolders){ - // Not a OneDrive Business Shared Folder - log.vdebug("Calling onedrive.getPathDetailsByDriveId(driveId, path) with: ", driveId, ", ", path); - onedrivePathDetails = onedrive.getPathDetailsByDriveId(driveId, path); - } else { - // OneDrive Business Shared Folder - Use another API call using the folders correct driveId and itemId - log.vdebug("Calling onedrive.getPathDetailsByDriveIdAndItemId(driveId, itemId) with: ", driveId, ", ", itemId); - onedrivePathDetails = onedrive.getPathDetailsByDriveIdAndItemId(driveId, itemId); - } - } catch (OneDriveException e) { - log.vdebug("onedrivePathDetails = onedrive.getPathDetails(path) generated a OneDriveException"); - if (e.httpStatusCode == 404) { - // The directory was not found - if (syncBusinessFolders){ - // 404 was returned when trying to use a specific driveId and itemId .. which 'should' work .... but didnt - // Try the query with the path as a backup failsafe - log.vdebug("Calling onedrive.getPathDetailsByDriveId(driveId, path) as backup with: ", driveId, ", ", path); - try { - // try calling using the path - onedrivePathDetails = onedrive.getPathDetailsByDriveId(driveId, path); - } catch (OneDriveException e) { - - if (e.httpStatusCode == 404) { - log.error("ERROR: The requested single directory to sync was not found on OneDrive - Check folder permissions and sharing status with folder owner"); - 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 applyDifferencesSingleDirectory(path);"); - applyDifferencesSingleDirectory(path); - // return back to original call - return; - } - - if (e.httpStatusCode >= 500) { - // OneDrive returned a 'HTTP 5xx Server Side Error' - gracefully handling error - error message already logged - return; - } - } - } else { - // Not a OneDrive Business Shared folder operation - 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 applyDifferencesSingleDirectory(path);"); - applyDifferencesSingleDirectory(path); - // return back to original call - return; - } - - if (e.httpStatusCode >= 500) { - // OneDrive returned a 'HTTP 5xx Server Side Error' - gracefully handling error - error message already logged - return; - } - } - - // OK - the path on OneDrive should exist, get the driveId and rootId for this folder - // Was the response a valid JSON Object? - if (onedrivePathDetails.type() == JSONType.object) { - // OneDrive Personal Shared Folder handling - // Is this item a remote item? - if(isItemRemote(onedrivePathDetails)){ - // 2 step approach: - // 1. Ensure changes for the root remote path are captured - // 2. Download changes specific to the remote path - - // root remote - applyDifferences(defaultDriveId, onedrivePathDetails["id"].str, false); - - // remote changes - driveId = onedrivePathDetails["remoteItem"]["parentReference"]["driveId"].str; // Should give something like 66d53be8a5056eca - folderId = onedrivePathDetails["remoteItem"]["id"].str; // Should give something like BC7D88EC1F539DCF!107 - - // Apply any differences found on OneDrive for this path (download data) - applyDifferences(driveId, folderId, false); - } else { - // use the item id as folderId - folderId = onedrivePathDetails["id"].str; // Should give something like 12345ABCDE1234A1!101 - // Apply any differences found on OneDrive for this path (download data) - // Use driveId rather than defaultDriveId as this will be updated if path was matched to another parent driveId - applyDifferences(driveId, folderId, false); - } - } else { - // Log that an invalid JSON object was returned - log.vdebug("onedrive.getPathDetails call returned an invalid JSON Object"); - } - } - - // make sure the OneDrive root is in our database - auto checkDatabaseForOneDriveRoot() - { - log.vlog("Fetching details for OneDrive Root"); - JSONValue rootPathDetails = onedrive.getDefaultRoot(); // Returns a JSON Value - - // validate object is a JSON value - if (rootPathDetails.type() == JSONType.object) { - // valid JSON object - Item rootPathItem = makeItem(rootPathDetails); - // configure driveId and rootId for the OneDrive Root - // Set defaults for the root folder - string driveId = rootPathDetails["parentReference"]["driveId"].str; // Should give something like 12345abcde1234a1 - string rootId = rootPathDetails["id"].str; // Should give something like 12345ABCDE1234A1!101 - - // Query the database - if (!itemdb.selectById(driveId, rootId, rootPathItem)) { - log.vlog("OneDrive Root does not exist in the database. We need to add it."); - applyDifference(rootPathDetails, driveId, true); - log.vlog("Added OneDrive Root to the local database"); - } else { - log.vlog("OneDrive Root exists in the database"); - } - } else { - // Log that an invalid JSON object was returned - log.error("ERROR: Unable to query OneDrive for account details"); - log.vdebug("onedrive.getDefaultRoot call returned an invalid JSON Object"); - // Must exit here as we cant configure our required variables - onedrive.shutdown(); - exit(-1); - } - } - - // create a directory on OneDrive without syncing - auto createDirectoryNoSync(const(string) path) - { - // Attempt to create the requested path within OneDrive without performing a sync - log.vlog("Attempting to create the requested path within OneDrive"); - - // Handle the remote folder creation and updating of the local database without performing a sync - uploadCreateDir(path); - } - - // delete a directory on OneDrive without syncing - auto deleteDirectoryNoSync(const(string) path) - { - // Use the global's as initialised via init() rather than performing unnecessary additional HTTPS calls - const(char)[] rootId = defaultRootId; - - // Attempt to delete the requested path within OneDrive without performing a sync - log.vlog("Attempting to delete the requested path within OneDrive"); - - // test if the path we are going to exists on OneDrive - try { - onedrive.getPathDetails(path); - } catch (OneDriveException e) { - log.vdebug("onedrive.getPathDetails(path) generated a OneDriveException"); - if (e.httpStatusCode == 404) { - // The directory was not found on OneDrive - no need to delete it - log.vlog("The requested directory to delete was not found on OneDrive - skipping removing the remote directory as it doesn't exist"); - 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 deleteDirectoryNoSync(path);"); - deleteDirectoryNoSync(path); - // return back to original call - return; - } - - if (e.httpStatusCode >= 500) { - // OneDrive returned a 'HTTP 5xx Server Side Error' - gracefully handling error - error message already logged - return; - } - } - - Item item; - // Need to check all driveid's we know about, not just the defaultDriveId - bool itemInDB = false; - foreach (searchDriveId; driveIDsArray) { - if (itemdb.selectByPath(path, searchDriveId, item)) { - // item was found in the DB - itemInDB = true; - break; - } - } - // Was the item found in the DB - if (!itemInDB) { - // this is odd .. this directory is not in the local database - just go delete it - log.vlog("The requested directory to delete was not found in the local database - pushing delete request direct to OneDrive"); - uploadDeleteItem(item, path); - } else { - // the folder was in the local database - // Handle the deletion and saving any update to the local database - log.vlog("The requested directory to delete was found in the local database. Processing the deletion normally"); - deleteByPath(path); - } - } - - // rename a directory on OneDrive without syncing - auto renameDirectoryNoSync(string source, string destination) - { - try { - // test if the local path exists on OneDrive - onedrive.getPathDetails(source); - } catch (OneDriveException e) { - log.vdebug("onedrive.getPathDetails(source); generated a OneDriveException"); - if (e.httpStatusCode == 404) { - // The directory was not found - log.vlog("The requested directory to rename 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 renameDirectoryNoSync(source, destination);"); - renameDirectoryNoSync(source, destination); - // return back to original call - return; - } - - if (e.httpStatusCode >= 500) { - // OneDrive returned a 'HTTP 5xx Server Side Error' - gracefully handling error - error message already logged - return; - } - } - // The OneDrive API returned a 200 OK status, so the folder exists - // Rename the requested directory on OneDrive without performing a sync - moveByPath(source, destination); - } - - // download the new changes of a specific item - // id is the root of the drive or a shared folder - private void applyDifferences(string driveId, const(char)[] id, bool performFullItemScan) - { - log.vlog("Applying changes of Path ID: " ~ id); - // function variables - char[] idToQuery; - JSONValue changes; - JSONValue changesAvailable; - JSONValue idDetails; - JSONValue currentDriveQuota; - string syncFolderName; - string syncFolderPath; - string syncFolderChildPath; - string deltaLink; - string deltaLinkAvailable; - bool nationalCloudChildrenScan = false; - - // Tracking processing performance - SysTime startFunctionProcessingTime; - SysTime endFunctionProcessingTime; - SysTime startBundleProcessingTime; - SysTime endBundleProcessingTime; - ulong cumulativeOneDriveItemCount = 0; - - if (displayProcessingTime) { - writeln("============================================================"); - writeln("Querying OneDrive API for relevant 'changes|items' stored online for this account"); - startFunctionProcessingTime = Clock.currTime(); - writeln("Start Function Processing Time: ", startFunctionProcessingTime); - } - - // Update the quota details for this driveId, as this could have changed since we started the application - the user could have added / deleted data online, or purchased additional storage - // Quota details are ONLY available for the main default driveId, as the OneDrive API does not provide quota details for shared folders - try { - currentDriveQuota = onedrive.getDriveQuota(driveId); - } catch (OneDriveException e) { - log.vdebug("currentDriveQuota = onedrive.getDriveQuota(driveId) generated a OneDriveException"); - 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 applyDifferences(driveId, id, performFullItemScan);"); - applyDifferences(driveId, id, performFullItemScan); - // return back to original call - return; - } - if (e.httpStatusCode >= 500) { - // OneDrive returned a 'HTTP 5xx Server Side Error' - gracefully handling error - error message already logged - return; - } - } - - // validate that currentDriveQuota is a JSON value - if (currentDriveQuota.type() == JSONType.object) { - // Response from API contains valid data - // If 'personal' accounts, if driveId == defaultDriveId, then we will have data - // If 'personal' accounts, if driveId != defaultDriveId, then we will not have quota data - // If 'business' accounts, if driveId == defaultDriveId, then we will have data - // If 'business' accounts, if driveId != defaultDriveId, then we will have data, but it will be 0 values - if ("quota" in currentDriveQuota){ - if (driveId == defaultDriveId) { - // We potentially have updated quota remaining details available - // However in some cases OneDrive Business configurations 'restrict' quota details thus is empty / blank / negative value / zero - if ("remaining" in currentDriveQuota["quota"]){ - // We have valid quota details returned for the drive id - remainingFreeSpace = currentDriveQuota["quota"]["remaining"].integer; - if (remainingFreeSpace <= 0) { - if (accountType == "personal"){ - // zero space available - log.error("ERROR: OneDrive account currently has zero space available. Please free up some space online."); - quotaAvailable = false; - } else { - // zero space available is being reported, maybe being restricted? - log.error("WARNING: OneDrive quota information is being restricted or providing a zero value. Please fix by speaking to your OneDrive / Office 365 Administrator."); - quotaRestricted = true; - } - } else { - // Display the updated value - log.vlog("Updated Remaining Free Space: ", remainingFreeSpace); - } - } - } else { - // quota details returned, but for a drive id that is not ours - if ("remaining" in currentDriveQuota["quota"]){ - // remaining is in the quota JSON response - if (currentDriveQuota["quota"]["remaining"].integer <= 0) { - // value returned is 0 or less than 0 - log.vlog("OneDrive quota information is set at zero, as this is not our drive id, ignoring"); - } - } - } - } else { - // No quota details returned - if (driveId == defaultDriveId) { - // no quota details returned for current drive id - log.error("ERROR: OneDrive quota information is missing. Potentially your OneDrive account currently has zero space available. Please free up some space online."); - } else { - // quota details not available - log.vdebug("OneDrive quota information is being restricted as this is not our drive id."); - } - } - } - - // Query OneDrive API for the name of this folder id - try { - idDetails = onedrive.getPathDetailsById(driveId, id); - } catch (OneDriveException e) { - log.vdebug("idDetails = onedrive.getPathDetailsById(driveId, id) generated a OneDriveException"); - if (e.httpStatusCode == 404) { - // id was not found - possibly a remote (shared) folder - log.vlog("No details returned for given Path ID"); - 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 applyDifferences(driveId, id, performFullItemScan);"); - applyDifferences(driveId, id, performFullItemScan); - // return back to original call - return; - } - - if (e.httpStatusCode >= 500) { - // OneDrive returned a 'HTTP 5xx Server Side Error' - gracefully handling error - error message already logged - return; - } - } - - // validate that idDetails is a JSON value - if (idDetails.type() == JSONType.object) { - // Get the name of this 'Path ID' - if (("id" in idDetails) != null) { - // valid response from onedrive.getPathDetailsById(driveId, id) - a JSON item object present - if ((idDetails["id"].str == id) && (!isItemFile(idDetails))){ - // Is a Folder or Remote Folder - syncFolderName = idDetails["name"].str; - } - - // Debug output of path details as queried from OneDrive - log.vdebug("OneDrive Path Details: ", idDetails); - - // OneDrive Personal Folder Item Reference (24/4/2019) - // "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#drives('66d53be8a5056eca')/items/$entity", - // "cTag": "adDo2NkQ1M0JFOEE1MDU2RUNBITEwMS42MzY5MTY5NjQ1ODcwNzAwMDA", - // "eTag": "aNjZENTNCRThBNTA1NkVDQSExMDEuMQ", - // "fileSystemInfo": { - // "createdDateTime": "2018-06-06T20:45:24.436Z", - // "lastModifiedDateTime": "2019-04-24T07:09:31.29Z" - // }, - // "folder": { - // "childCount": 3, - // "view": { - // "sortBy": "takenOrCreatedDateTime", - // "sortOrder": "ascending", - // "viewType": "thumbnails" - // } - // }, - // "id": "66D53BE8A5056ECA!101", - // "name": "root", - // "parentReference": { - // "driveId": "66d53be8a5056eca", - // "driveType": "personal" - // }, - // "root": {}, - // "size": 0 - - // OneDrive Personal Remote / Shared Folder Item Reference (4/9/2019) - // "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#drives('driveId')/items/$entity", - // "cTag": "cTag", - // "eTag": "eTag", - // "id": "itemId", - // "name": "shared", - // "parentReference": { - // "driveId": "driveId", - // "driveType": "personal", - // "id": "parentItemId", - // "path": "/drive/root:" - // }, - // "remoteItem": { - // "fileSystemInfo": { - // "createdDateTime": "2019-01-14T18:54:43.2666667Z", - // "lastModifiedDateTime": "2019-04-24T03:47:22.53Z" - // }, - // "folder": { - // "childCount": 0, - // "view": { - // "sortBy": "takenOrCreatedDateTime", - // "sortOrder": "ascending", - // "viewType": "thumbnails" - // } - // }, - // "id": "remoteItemId", - // "parentReference": { - // "driveId": "remoteDriveId", - // "driveType": "personal" - // "id": "id", - // "name": "name", - // "path": "/drives//items/:/" - // }, - // "size": 0, - // "webUrl": "webUrl" - // } - - // OneDrive Business Folder & Shared Folder Item Reference (24/4/2019) - // "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#drives('driveId')/items/$entity", - // "@odata.etag": "\"{eTag},1\"", - // "cTag": "\"c:{cTag},0\"", - // "eTag": "\"{eTag},1\"", - // "fileSystemInfo": { - // "createdDateTime": "2019-04-17T04:00:43Z", - // "lastModifiedDateTime": "2019-04-17T04:00:43Z" - // }, - // "folder": { - // "childCount": 2 - // }, - // "id": "itemId", - // "name": "shared_folder", - // "parentReference": { - // "driveId": "parentDriveId", - // "driveType": "business", - // "id": "parentId", - // "path": "/drives/driveId/root:" - // }, - // "size": 0 - - // To evaluate a change received from OneDrive, this must be set correctly - if (hasParentReferencePath(idDetails)) { - // Path from OneDrive has a parentReference we can use - log.vdebug("Item details returned contains parent reference path - potentially shared folder object"); - syncFolderPath = idDetails["parentReference"]["path"].str; - syncFolderChildPath = syncFolderPath ~ "/" ~ idDetails["name"].str ~ "/"; - } else { - // No parentReference, set these to blank - log.vdebug("Item details returned no parent reference path"); - syncFolderPath = ""; - syncFolderChildPath = ""; - } - - // Debug Output - log.vdebug("Sync Folder Name: ", syncFolderName); - log.vdebug("Sync Folder Parent Path: ", syncFolderPath); - log.vdebug("Sync Folder Child Path: ", syncFolderChildPath); - } - } else { - // Log that an invalid JSON object was returned - log.vdebug("onedrive.getPathDetailsById call returned an invalid JSON Object"); - } - - // Issue #658 - // If we are using a sync_list file, using deltaLink will actually 'miss' changes (moves & deletes) on OneDrive as using sync_list discards changes - // Use the performFullItemScan boolean to control whether we perform a full object scan of use the delta link for the root folder - // When using --synchronize the normal process order is: - // 1. Scan OneDrive for changes - // 2. Scan local folder for changes - // 3. Scan OneDrive for changes - // When using sync_list and performing a full scan, what this means is a full scan is performed twice, which leads to massive processing & time overheads - // Control this via performFullItemScan - - // Get the current delta link - deltaLinkAvailable = itemdb.getDeltaLink(driveId, id); - // if sync_list is not configured, syncListConfigured should be false - log.vdebug("syncListConfigured = ", syncListConfigured); - // oneDriveFullScanTrigger should be false unless set by actions on OneDrive and only if sync_list or skip_dir is used - log.vdebug("oneDriveFullScanTrigger = ", oneDriveFullScanTrigger); - // should only be set if 10th scan in monitor mode or as final true up sync in stand alone mode - log.vdebug("performFullItemScan = ", performFullItemScan); - - // do we override performFullItemScan if it is currently false and oneDriveFullScanTrigger is true? - if ((!performFullItemScan) && (oneDriveFullScanTrigger)) { - // forcing a full scan earlier than potentially normal - // oneDriveFullScanTrigger = true due to new folder creation request in a location that is now in-scope which was previously out of scope - performFullItemScan = true; - log.vdebug("overriding performFullItemScan as oneDriveFullScanTrigger was set"); - } - - // depending on the scan type (--monitor or --synchronize) performFullItemScan is set depending on the number of sync passes performed (--monitor) or ALWAYS if just --synchronize is used - if (!performFullItemScan){ - // performFullItemScan == false - // use delta link - log.vdebug("performFullItemScan is false, using the deltaLink as per database entry"); - if (deltaLinkAvailable == ""){ - deltaLink = ""; - log.vdebug("deltaLink was requested to be used, but contains no data - resulting API query will be treated as a full scan of OneDrive"); - } else { - deltaLink = deltaLinkAvailable; - log.vdebug("deltaLink contains valid data - resulting API query will be treated as a delta scan of OneDrive"); - } - } else { - // performFullItemScan == true - // do not use delta-link - deltaLink = ""; - log.vdebug("performFullItemScan is true, not using the database deltaLink so that we query all objects on OneDrive to compare against all local objects"); - } - - for (;;) { - - if (displayProcessingTime) { - writeln("------------------------------------------------------------"); - startBundleProcessingTime = Clock.currTime(); - writeln("Start 'change|item' API Response Bundle Processing Time: ", startBundleProcessingTime); - } - - // Due to differences in OneDrive API's between personal and business we need to get changes only from defaultRootId - // If we used the 'id' passed in & when using --single-directory with a business account we get: - // 'HTTP request returned status code 501 (Not Implemented): view.delta can only be called on the root.' - // To view changes correctly, we need to use the correct path id for the request - if (driveId == defaultDriveId) { - // The drive id matches our users default drive id - log.vdebug("Configuring 'idToQuery' as defaultRootId duplicate"); - idToQuery = defaultRootId.dup; - } else { - // The drive id does not match our users default drive id - // Potentially the 'path id' we are requesting the details of is a Shared Folder (remote item) - // Use the 'id' that was passed in (folderId) - log.vdebug("Configuring 'idToQuery' as 'id' duplicate"); - idToQuery = id.dup; - } - // what path id are we going to query? - log.vdebug("Path object to query configured as 'idToQuery' = ", idToQuery); - long deltaChanges = 0; - - // What query do we use? - // National Cloud Deployments do not support /delta as a query - // https://docs.microsoft.com/en-us/graph/deployments#supported-features - // Are we running against a National Cloud Deployments that does not support /delta - if (nationalCloudDeployment) { - // National Cloud Deployment that does not support /delta query - // Have to query /children and build our own /delta response - nationalCloudChildrenScan = true; - log.vdebug("Using /children call to query drive for items to populate 'changes' and 'changesAvailable'"); - // In a OneDrive Business Shared Folder scenario + nationalCloudDeployment, if ALL items are downgraded, then this leads to local file deletion - // Downgrade ONLY files associated with this driveId and idToQuery - log.vdebug("Downgrading all children for this driveId (" ~ driveId ~ ") and idToQuery (" ~ idToQuery ~ ") to an out-of-sync state"); - - // Before we get any data, flag any object in the database as out-of-sync for this driveID & ID - auto drivePathChildren = itemdb.selectChildren(driveId, idToQuery); - if (count(drivePathChildren) > 0) { - // Children to process and flag as out-of-sync - foreach (drivePathChild; drivePathChildren) { - // Flag any object in the database as out-of-sync for this driveID & ID - log.vdebug("Downgrading item as out-of-sync: ", drivePathChild.id); - itemdb.downgradeSyncStatusFlag(drivePathChild.driveId, drivePathChild.id); - } - } - - // Build own 'changes' response to simulate a /delta response - try { - // we have to 'build' our own JSON response that looks like /delta - changes = generateDeltaResponse(driveId, idToQuery); - if (changes.type() == JSONType.object) { - log.vdebug("Query 'changes = generateDeltaResponse(driveId, idToQuery)' performed successfully"); - } - } catch (OneDriveException e) { - // OneDrive threw an error - log.vdebug("------------------------------------------------------------------"); - log.vdebug("Query Error: changes = generateDeltaResponse(driveId, idToQuery)"); - log.vdebug("driveId: ", driveId); - log.vdebug("idToQuery: ", idToQuery); - - // HTTP request returned status code 404 (Not Found) - if (e.httpStatusCode == 404) { - // Stop application - log.log("\n\nOneDrive returned a 'HTTP 404 - Item not found'"); - log.log("The item id to query was not found on OneDrive"); - log.log("\nRemove your '", cfg.databaseFilePath, "' file and try to sync again\n"); - return; - } - - // HTTP request returned status code 429 (Too Many Requests) - 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(); - log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to query OneDrive drive items"); - } - - // HTTP request returned status code 500 (Internal Server Error) - if (e.httpStatusCode == 500) { - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - - // HTTP request returned status code 504 (Gateway Timeout) or 429 retry - if ((e.httpStatusCode == 429) || (e.httpStatusCode == 504)) { - // If an error is returned when querying 'changes' and we recall the original function, we go into a never ending loop where the sync never ends - // re-try the specific changes queries - if (e.httpStatusCode == 504) { - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' when attempting to query OneDrive drive items - retrying applicable request"); - log.vdebug("changes = generateDeltaResponse(driveId, idToQuery) previously threw an error - retrying"); - // The server, while acting as a proxy, did not receive a timely response from the upstream server it needed to access in attempting to complete the request. - log.vdebug("Thread sleeping for 30 seconds as the server did not receive a timely response from the upstream server it needed to access in attempting to complete the request"); - Thread.sleep(dur!"seconds"(30)); - log.vdebug("Retrying Query - using original deltaLink after delay"); - } - // re-try original request - retried for 429 and 504 - try { - log.vdebug("Retrying Query: changes = generateDeltaResponse(driveId, idToQuery)"); - changes = generateDeltaResponse(driveId, idToQuery); - log.vdebug("Query 'changes = generateDeltaResponse(driveId, idToQuery)' performed successfully on re-try"); - } catch (OneDriveException e) { - // display what the error is - log.vdebug("Query Error: changes = generateDeltaResponse(driveId, idToQuery) on re-try after delay"); - // error was not a 504 this time - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } else { - // Default operation if not 404, 410, 429, 500 or 504 errors - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } - } else { - log.vdebug("Using /delta call to query drive for items to populate 'changes' and 'changesAvailable'"); - // query for changes = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink); - try { - // Fetch the changes relative to the path id we want to query - log.vdebug("Attempting query 'changes = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink)'"); - log.vdebug("driveId: ", driveId); - log.vdebug("idToQuery: ", idToQuery); - log.vdebug("Previous deltaLink: ", deltaLink); - // changes with or without deltaLink - changes = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink); - if (changes.type() == JSONType.object) { - log.vdebug("Query 'changes = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink)' performed successfully"); - log.vdebug("OneDrive API /delta response: ", changes); - } - } catch (OneDriveException e) { - // OneDrive threw an error - log.vdebug("------------------------------------------------------------------"); - log.vdebug("Query Error: changes = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink)"); - - // HTTP request returned status code 404 (Not Found) - if (e.httpStatusCode == 404) { - // Stop application - log.log("\n\nOneDrive returned a 'HTTP 404 - Item not found'"); - log.log("The item id to query was not found on OneDrive"); - log.log("\nRemove your '", cfg.databaseFilePath, "' file and try to sync again\n"); - return; - } - - // HTTP request returned status code 410 (The requested resource is no longer available at the server) - if (e.httpStatusCode == 410) { - log.vdebug("Delta link expired for 'onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink)', setting 'deltaLink = null'"); - deltaLink = null; - continue; - } - - // HTTP request returned status code 429 (Too Many Requests) - 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(); - log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to query changes from OneDrive using deltaLink"); - } - - // HTTP request returned status code 500 (Internal Server Error) - if (e.httpStatusCode == 500) { - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - - // HTTP request returned status code 504 (Gateway Timeout) or 429 retry - if ((e.httpStatusCode == 429) || (e.httpStatusCode == 504)) { - // If an error is returned when querying 'changes' and we recall the original function, we go into a never ending loop where the sync never ends - // re-try the specific changes queries - if (e.httpStatusCode == 504) { - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' when attempting to query for changes - retrying applicable request"); - log.vdebug("changes = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink) previously threw an error - retrying"); - // The server, while acting as a proxy, did not receive a timely response from the upstream server it needed to access in attempting to complete the request. - log.vdebug("Thread sleeping for 30 seconds as the server did not receive a timely response from the upstream server it needed to access in attempting to complete the request"); - Thread.sleep(dur!"seconds"(30)); - log.vdebug("Retrying Query - using original deltaLink after delay"); - } - // re-try original request - retried for 429 and 504 - try { - log.vdebug("Retrying Query: changes = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink)"); - changes = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink); - log.vdebug("Query 'changes = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink)' performed successfully on re-try"); - } catch (OneDriveException e) { - // display what the error is - log.vdebug("Query Error: changes = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink) on re-try after delay"); - if (e.httpStatusCode == 504) { - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' when attempting to query for changes - retrying applicable request"); - log.vdebug("changes = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink) previously threw an error - retrying with empty deltaLink"); - try { - // try query with empty deltaLink value - deltaLink = null; - changes = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink); - log.vdebug("Query 'changes = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink)' performed successfully on re-try"); - } catch (OneDriveException e) { - // Tried 3 times, give up - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } else { - // error was not a 504 this time - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } - } else { - // Default operation if not 404, 410, 429, 500 or 504 errors - // Issue #1174 handling where stored deltaLink is invalid - if ((e.httpStatusCode == 400) && (deltaLink != "")) { - // Set deltaLink to an empty entry so invalid URL is not reused - string emptyDeltaLink = ""; - itemdb.setDeltaLink(driveId, idToQuery, emptyDeltaLink); - } - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } - - // query for changesAvailable = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLinkAvailable); - try { - // Fetch the changes relative to the path id we want to query - log.vdebug("Attempting query 'changesAvailable = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLinkAvailable)'"); - log.vdebug("driveId: ", driveId); - log.vdebug("idToQuery: ", idToQuery); - log.vdebug("deltaLinkAvailable: ", deltaLinkAvailable); - // changes based on deltaLink - changesAvailable = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLinkAvailable); - if (changesAvailable.type() == JSONType.object) { - log.vdebug("Query 'changesAvailable = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLinkAvailable)' performed successfully"); - // are there any delta changes? - if (("value" in changesAvailable) != null) { - deltaChanges = count(changesAvailable["value"].array); - log.vdebug("changesAvailable query reports that there are " , deltaChanges , " changes that need processing on OneDrive"); - } - } - } catch (OneDriveException e) { - // OneDrive threw an error - log.vdebug("------------------------------------------------------------------"); - log.vdebug("Query Error: changesAvailable = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLinkAvailable)"); - - // HTTP request returned status code 404 (Not Found) - if (e.httpStatusCode == 404) { - // Stop application - log.log("\n\nOneDrive returned a 'HTTP 404 - Item not found'"); - log.log("The item id to query was not found on OneDrive"); - log.log("\nRemove your '", cfg.databaseFilePath, "' file and try to sync again\n"); - return; - } - - // HTTP request returned status code 410 (The requested resource is no longer available at the server) - if (e.httpStatusCode == 410) { - log.vdebug("Delta link expired for 'onedrive.viewChangesByItemId(driveId, idToQuery, deltaLinkAvailable)', setting 'deltaLinkAvailable = null'"); - deltaLinkAvailable = null; - continue; - } - - // HTTP request returned status code 429 (Too Many Requests) - 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(); - log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to query changes from OneDrive using deltaLinkAvailable"); - } - - // HTTP request returned status code 500 (Internal Server Error) - if (e.httpStatusCode == 500) { - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - - // HTTP request returned status code 504 (Gateway Timeout) or 429 retry - if ((e.httpStatusCode == 429) || (e.httpStatusCode == 504)) { - // If an error is returned when querying 'changes' and we recall the original function, we go into a never ending loop where the sync never ends - // re-try the specific changes queries - if (e.httpStatusCode == 504) { - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' when attempting to query for changes - retrying applicable request"); - log.vdebug("changesAvailable = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLinkAvailable) previously threw an error - retrying"); - // The server, while acting as a proxy, did not receive a timely response from the upstream server it needed to access in attempting to complete the request. - log.vdebug("Thread sleeping for 30 seconds as the server did not receive a timely response from the upstream server it needed to access in attempting to complete the request"); - Thread.sleep(dur!"seconds"(30)); - log.vdebug("Retrying Query - using original deltaLinkAvailable after delay"); - } - // re-try original request - retried for 429 and 504 - try { - log.vdebug("Retrying Query: changesAvailable = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLinkAvailable)"); - changesAvailable = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLinkAvailable); - log.vdebug("Query 'changesAvailable = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLinkAvailable)' performed successfully on re-try"); - if (changesAvailable.type() == JSONType.object) { - // are there any delta changes? - if (("value" in changesAvailable) != null) { - deltaChanges = count(changesAvailable["value"].array); - log.vdebug("changesAvailable query reports that there are " , deltaChanges , " changes that need processing on OneDrive"); - } - } - } catch (OneDriveException e) { - // display what the error is - log.vdebug("Query Error: changesAvailable = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLinkAvailable) on re-try after delay"); - if (e.httpStatusCode == 504) { - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' when attempting to query for changes - retrying applicable request"); - log.vdebug("changesAvailable = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLinkAvailable) previously threw an error - retrying with empty deltaLinkAvailable"); - // Increase delay and wait again before retry - log.vdebug("Thread sleeping for 90 seconds as the server did not receive a timely response from the upstream server it needed to access in attempting to complete the request"); - Thread.sleep(dur!"seconds"(90)); - log.vdebug("Retrying Query - using a null deltaLinkAvailable after delay"); - try { - // try query with empty deltaLinkAvailable value - deltaLinkAvailable = null; - changesAvailable = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLinkAvailable); - log.vdebug("Query 'changesAvailable = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLinkAvailable)' performed successfully on re-try"); - if (changesAvailable.type() == JSONType.object) { - // are there any delta changes? - if (("value" in changesAvailable) != null) { - deltaChanges = count(changesAvailable["value"].array); - log.vdebug("changesAvailable query reports that there are " , deltaChanges , " changes that need processing on OneDrive when using a null deltaLink value"); - } - } - } catch (OneDriveException e) { - // Tried 3 times, give up - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - - // OK .. if this was a 504, and running with --download-only & --cleanup-local-files - // need to exit to preserve local data, otherwise potential files will be deleted that should not be deleted - // leading to undesirable potential data loss scenarios - if ((e.httpStatusCode == 504) && (cleanupLocalFiles)) { - // log why we are exiting - log.log("Exiting application due to OneDrive API Gateway Timeout & --download-only & --cleanup-local-files configured to preserve local data"); - // Must exit here - onedrive.shutdown(); - exit(-1); - } - return; - } - } else { - // error was not a 504 this time - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } - } else { - // Default operation if not 404, 410, 429, 500 or 504 errors - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } - } - - // In some OneDrive Business scenarios, the shared folder /delta response lacks the 'root' drive details - // When this occurs, this creates the following error: A database statement execution error occurred: foreign key constraint failed - // Ensure we query independently the root details for this shared folder and ensure that it is added before we process the /delta response - - // However, if we are using a National Cloud Deployment, these deployments do not support /delta, so we generate a /delta response via generateDeltaResponse() - // This specifically adds the root drive details to the self generated /delta response - if ((!nationalCloudDeployment) && (driveId!= defaultDriveId) && (syncBusinessFolders)) { - // fetch this driveId root details to ensure we add this to the database for this remote drive - JSONValue rootData; - - try { - rootData = onedrive.getDriveIdRoot(driveId); - } catch (OneDriveException e) { - log.vdebug("rootData = onedrive.getDriveIdRoot(driveId) generated a OneDriveException"); - // HTTP request returned status code 504 (Gateway Timeout) or 429 retry - if ((e.httpStatusCode == 429) || (e.httpStatusCode == 504)) { - // 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. - if (e.httpStatusCode == 429) { - log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - retrying applicable request"); - handleOneDriveThrottleRequest(); - } - if (e.httpStatusCode == 504) { - log.vdebug("Retrying original request that generated the HTTP 504 (Gateway Timeout) - retrying applicable request"); - Thread.sleep(dur!"seconds"(30)); - } - // Retry original request by calling function again to avoid replicating any further error handling - rootData = onedrive.getDriveIdRoot(driveId); - - } else { - // There was a HTTP 5xx Server Side Error - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - // Must exit here - onedrive.shutdown(); - exit(-1); - } - } - - // apply this root drive data - applyDifference(rootData, driveId, true); - } - - // Process /delta response from OneDrive - // is changes a valid JSON response - if (changes.type() == JSONType.object) { - // Are there any changes to process? - if ((("value" in changes) != null) && ((deltaChanges > 0) || (oneDriveFullScanTrigger) || (nationalCloudChildrenScan) || (syncBusinessFolders) )) { - auto nrChanges = count(changes["value"].array); - auto changeCount = 0; - - // Display the number of changes or OneDrive objects we are processing - // OneDrive ships 'changes' in ~200 bundles. We display that we are processing X number of objects - // Do not display anything unless we are doing a verbose debug as due to #658 we are essentially doing a --resync each time when using sync_list - - // performance logging output - if (displayProcessingTime) { - writeln("Number of 'change|item' in this API Response Bundle from OneDrive to process: ", nrChanges); - } - - // is nrChanges >= min_notify_changes (default of min_notify_changes = 5) - if (nrChanges >= cfg.getValueLong("min_notify_changes")) { - // nrChanges is >= than min_notify_changes - // verbose log, no 'notify' .. it is over the top - if (!syncListConfigured) { - // sync_list is not being used - lets use the right messaging here - if (oneDriveFullScanTrigger) { - // full scan was triggered out of cycle - log.vlog("Processing ", nrChanges, " OneDrive items to ensure consistent local state due to a full scan being triggered by actions on OneDrive"); - // unset now the full scan trigger if set - unsetOneDriveFullScanTrigger(); - } else { - // no sync_list in use, oneDriveFullScanTrigger not set via sync_list or skip_dir - if (performFullItemScan){ - // performFullItemScan was set - log.vlog("Processing ", nrChanges, " OneDrive items to ensure consistent local state due to a full scan being requested"); - } else { - // default processing message - log.vlog("Processing ", nrChanges, " OneDrive items to ensure consistent local state"); - } - } - } else { - // sync_list is being used - why are we going through the entire OneDrive contents? - log.vlog("Processing ", nrChanges, " OneDrive items to ensure consistent local state due to sync_list being used"); - } - } else { - // There are valid changes but less than the min_notify_changes configured threshold - // We will only output the number of changes being processed to debug log if this is set to assist with debugging - // As this is debug logging, messaging can be the same, regardless of sync_list being used or not - - // is performFullItemScan set due to a full scan required? - // is oneDriveFullScanTrigger set due to a potentially out-of-scope item now being in-scope - if ((performFullItemScan) || (oneDriveFullScanTrigger)) { - // oneDriveFullScanTrigger should be false unless set by actions on OneDrive and only if sync_list or skip_dir is used - log.vdebug("performFullItemScan or oneDriveFullScanTrigger = true"); - // full scan was requested or triggered - // use the right message - if (oneDriveFullScanTrigger) { - log.vlog("Processing ", nrChanges, " OneDrive items to ensure consistent local state due to a full scan being triggered by actions on OneDrive"); - // unset now the full scan trigger if set - unsetOneDriveFullScanTrigger(); - } else { - log.vlog("Processing ", nrChanges, " OneDrive items to ensure consistent local state due to a full scan being requested"); - } - } else { - // standard message - log.vlog("Number of items from OneDrive to process: ", nrChanges); - } - } - - // Add nrChanges to cumulativeOneDriveItemCount so we can detail how may items in total were processed - cumulativeOneDriveItemCount = cumulativeOneDriveItemCount + nrChanges; - - foreach (item; changes["value"].array) { - bool isRoot = false; - string thisItemParentPath; - string thisItemFullPath; - changeCount++; - - // Change as reported by OneDrive - log.vdebug("------------------------------------------------------------------"); - log.vdebug("Processing change ", changeCount, " of ", nrChanges); - log.vdebug("OneDrive Change: ", item); - - // Deleted items returned from onedrive.viewChangesByItemId or onedrive.viewChangesByDriveId (/delta) do not have a 'name' attribute - // Thus we cannot name check for 'root' below on deleted items - if(!isItemDeleted(item)){ - // This is not a deleted item - log.vdebug("Not a OneDrive deleted item change"); - // Test is this is the OneDrive Users Root? - // Debug output of change evaluation items - log.vdebug("defaultRootId = ", defaultRootId); - log.vdebug("'search id' = ", id); - log.vdebug("id == defaultRootId = ", (id == defaultRootId)); - log.vdebug("isItemRoot(item) = ", (isItemRoot(item))); - log.vdebug("item['name'].str == 'root' = ", (item["name"].str == "root")); - log.vdebug("singleDirectoryScope = ", (singleDirectoryScope)); - - // Use the global's as initialised via init() rather than performing unnecessary additional HTTPS calls - // In a --single-directory scenario however, '(id == defaultRootId) = false' for root items - if ( ((id == defaultRootId) || (singleDirectoryScope)) && (isItemRoot(item)) && (item["name"].str == "root")) { - // This IS a OneDrive Root item - log.vdebug("Change will flagged as a 'root' item change"); - isRoot = true; - } - } - - // How do we handle this change? - if (isRoot || !hasParentReferenceId(item) || isItemDeleted(item)){ - // Is a root item, has no id in parentReference or is a OneDrive deleted item - log.vdebug("isRoot = ", isRoot); - log.vdebug("!hasParentReferenceId(item) = ", (!hasParentReferenceId(item))); - log.vdebug("isItemDeleted(item) = ", (isItemDeleted(item))); - log.vdebug("Handling change as 'root item', has no parent reference or is a deleted item"); - applyDifference(item, driveId, isRoot); - } else { - // What is this item's parent path? - if (hasParentReferencePath(item)) { - thisItemParentPath = item["parentReference"]["path"].str; - thisItemFullPath = thisItemParentPath ~ "/" ~ item["name"].str; - } else { - thisItemParentPath = ""; - } - - // Special case handling flags - bool singleDirectorySpecialCase = false; - bool sharedFoldersSpecialCase = false; - - // Debug output of change evaluation items - log.vdebug("'parentReference id' = ", item["parentReference"]["id"].str); - log.vdebug("search criteria: syncFolderName = ", syncFolderName); - log.vdebug("search criteria: syncFolderPath = ", syncFolderPath); - log.vdebug("search criteria: syncFolderChildPath = ", syncFolderChildPath); - log.vdebug("thisItemId = ", item["id"].str); - log.vdebug("thisItemParentPath = ", thisItemParentPath); - log.vdebug("thisItemFullPath = ", thisItemFullPath); - log.vdebug("'item id' matches search 'id' = ", (item["id"].str == id)); - log.vdebug("'parentReference id' matches search 'id' = ", (item["parentReference"]["id"].str == id)); - log.vdebug("'thisItemParentPath' contains 'syncFolderChildPath' = ", (canFind(thisItemParentPath, syncFolderChildPath))); - log.vdebug("'thisItemParentPath' contains search 'id' = ", (canFind(thisItemParentPath, id))); - - // Special case handling - --single-directory - // If we are in a --single-directory sync scenario, and, the DB does not contain any parent details, or --single-directory is used with --resync - // all changes will be discarded as 'Remote change discarded - not in --single-directory sync scope (not in DB)' even though, some of the changes - // are actually valid and required as they are part of the parental path - if (singleDirectoryScope){ - // What is the full path for this item from OneDrive - log.vdebug("'syncFolderChildPath' contains 'thisItemFullPath' = ", (canFind(syncFolderChildPath, thisItemFullPath))); - if (canFind(syncFolderChildPath, thisItemFullPath)) { - singleDirectorySpecialCase = true; - } - } - - // Special case handling - Shared Business Folders - // - IF we are syncing shared folders, and the shared folder is not the 'top level' folder being shared out - // canFind(thisItemParentPath, syncFolderChildPath) will never match: - // Syncing this OneDrive Business Shared Folder: MyFolderName - // OneDrive Business Shared By: Firstname Lastname (email@address) - // Applying changes of Path ID: pathId - // [DEBUG] Sync Folder Name: MyFolderName - // [DEBUG] Sync Folder Path: /drives/driveId/root:/TopLevel/ABCD - // [DEBUG] Sync Folder Child Path: /drives/driveId/root:/TopLevel/ABCD/MyFolderName/ - // ... - // [DEBUG] 'item id' matches search 'id' = false - // [DEBUG] 'parentReference id' matches search 'id' = false - // [DEBUG] 'thisItemParentPath' contains 'syncFolderChildPath' = false - // [DEBUG] 'thisItemParentPath' contains search 'id' = false - // [DEBUG] Change does not match any criteria to apply - // Remote change discarded - not in business shared folders sync scope - - if ((!canFind(thisItemParentPath, syncFolderChildPath)) && (syncBusinessFolders)) { - // Syncing Shared Business folders & we dont have a path match - // is this a reverse path match? - log.vdebug("'thisItemParentPath' contains 'syncFolderName' = ", (canFind(thisItemParentPath, syncFolderName))); - if (canFind(thisItemParentPath, syncFolderName)) { - sharedFoldersSpecialCase = true; - } - } - - // Check this item's path to see if this is a change on the path we want: - // 1. 'item id' matches 'id' - // 2. 'parentReference id' matches 'id' - // 3. 'item path' contains 'syncFolderChildPath' - // 4. 'item path' contains 'id' - // 5. Special Case was triggered - if ( (item["id"].str == id) || (item["parentReference"]["id"].str == id) || (canFind(thisItemParentPath, syncFolderChildPath)) || (canFind(thisItemParentPath, id)) || (singleDirectorySpecialCase) || (sharedFoldersSpecialCase) ){ - // This is a change we want to apply - if ((!singleDirectorySpecialCase) && (!sharedFoldersSpecialCase)) { - log.vdebug("Change matches search criteria to apply"); - } else { - if (singleDirectorySpecialCase) log.vdebug("Change matches search criteria to apply - special case criteria - reverse path matching used (--single-directory)"); - if (sharedFoldersSpecialCase) log.vdebug("Change matches search criteria to apply - special case criteria - reverse path matching used (Shared Business Folders)"); - } - // Apply OneDrive change - applyDifference(item, driveId, isRoot); - } else { - // No item ID match or folder sync match - log.vdebug("Change does not match any criteria to apply"); - - // Before discarding change - does this ID still exist on OneDrive - as in IS this - // potentially a --single-directory sync and the user 'moved' the file out of the 'sync-dir' to another OneDrive folder - // This is a corner edge case - https://github.com/skilion/onedrive/issues/341 - - // What is the original local path for this ID in the database? Does it match 'syncFolderChildPath' - if (itemdb.idInLocalDatabase(driveId, item["id"].str)){ - // item is in the database - string originalLocalPath = computeItemPath(driveId, item["id"].str); - - if (canFind(originalLocalPath, syncFolderChildPath)){ - JSONValue oneDriveMovedNotDeleted; - try { - oneDriveMovedNotDeleted = onedrive.getPathDetailsById(driveId, item["id"].str); - } catch (OneDriveException e) { - log.vdebug("oneDriveMovedNotDeleted = onedrive.getPathDetailsById(driveId, item['id'].str); generated a OneDriveException"); - if (e.httpStatusCode == 404) { - // No .. that ID is GONE - log.vlog("Remote change discarded - item cannot be found"); - } - 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 request after delay - log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - calling oneDriveMovedNotDeleted = onedrive.getPathDetailsById(driveId, item['id'].str);"); - try { - oneDriveMovedNotDeleted = onedrive.getPathDetailsById(driveId, item["id"].str); - } catch (OneDriveException e) { - // A further error was generated - // Rather than retry original function, retry the actual call and replicate error handling - if (e.httpStatusCode == 404) { - // No .. that ID is GONE - log.vlog("Remote change discarded - item cannot be found"); - } else { - // not a 404 - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - } - } - } else { - // not a 404 or a 429 - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - } - } - - // Yes .. ID is still on OneDrive but elsewhere .... #341 edge case handling - // This 'change' relates to an item that WAS in 'syncFolderChildPath' but is now - // stored elsewhere on OneDrive - outside the path we are syncing from - // Remove this item locally as it's local path is now obsolete - idsToDelete ~= [driveId, item["id"].str]; - } else { - // out of scope for some other reason - if (singleDirectoryScope){ - log.vlog("Remote change discarded - not in --single-directory sync scope (in DB)"); - } else { - log.vlog("Remote change discarded - not in sync scope"); - } - log.vdebug("Remote change discarded: ", item); - } - } else { - // item is not in the database - if (singleDirectoryScope){ - // We are syncing a single directory, so this is the reason why it is out of scope - log.vlog("Remote change discarded - not in --single-directory sync scope (not in DB)"); - log.vdebug("Remote change discarded: ", item); - } else { - // Not a single directory sync - if (syncBusinessFolders) { - // if we are syncing shared business folders, a 'change' may be out of scope as we are not syncing that 'folder' - // but we are sent all changes from the 'parent root' as we cannot query the 'delta' for this folder - // as that is a 501 error - not implemented - log.vlog("Remote change discarded - not in business shared folders sync scope"); - log.vdebug("Remote change discarded: ", item); - } else { - // out of scope for some other reason - log.vlog("Remote change discarded - not in sync scope"); - log.vdebug("Remote change discarded: ", item); - } - } - } - } - } - } - } else { - // No changes reported on OneDrive - log.vdebug("OneDrive Reported no delta changes - Local path and OneDrive in-sync"); - } - - // the response may contain either @odata.deltaLink or @odata.nextLink - if ("@odata.deltaLink" in changes) { - deltaLink = changes["@odata.deltaLink"].str; - log.vdebug("Setting next deltaLink to (@odata.deltaLink): ", deltaLink); - } - if (deltaLink != "") { - // we initialise deltaLink to a blank string - if it is blank, dont update the DB to be empty - log.vdebug("Updating completed deltaLink in DB to: ", deltaLink); - itemdb.setDeltaLink(driveId, id, deltaLink); - } - - // Processing Timing for this bundle - if (displayProcessingTime) { - endBundleProcessingTime = Clock.currTime(); - writeln("End 'change|item' API Response Bundle Processing Time: ", endBundleProcessingTime); - writeln("Elapsed Processing Time: ", (endBundleProcessingTime - startBundleProcessingTime)); - } - - if ("@odata.nextLink" in changes) { - // Update deltaLink to next changeSet bundle - deltaLink = changes["@odata.nextLink"].str; - // Update deltaLinkAvailable to next changeSet bundle to quantify how many changes we have to process - deltaLinkAvailable = changes["@odata.nextLink"].str; - log.vdebug("Setting next deltaLink & deltaLinkAvailable to (@odata.nextLink): ", deltaLink); - } - else break; - } else { - // Log that an invalid JSON object was returned - if ((driveId == defaultDriveId) || (!syncBusinessFolders)) { - log.vdebug("onedrive.viewChangesByItemId call returned an invalid JSON Object"); - } else { - log.vdebug("onedrive.viewChangesByDriveId call returned an invalid JSON Object"); - } - } - } - - // delete items in idsToDelete - if (idsToDelete.length > 0) deleteItems(); - // empty the skipped items - skippedItems.length = 0; - assumeSafeAppend(skippedItems); - - // Processing timing and metrics for everything that was processed - if (displayProcessingTime) { - endFunctionProcessingTime = Clock.currTime(); - // complete the bundle output - writeln("------------------------------------------------------------"); - writeln("Start Function Processing Time: ", startFunctionProcessingTime); - writeln("End Function Processing Time: ", endFunctionProcessingTime); - writeln("Elapsed Function Processing Time: ", (endFunctionProcessingTime - startFunctionProcessingTime)); - writeln("Total number of OneDrive items processed: ", cumulativeOneDriveItemCount); - writeln("============================================================"); - } - } - - // process the change of a single DriveItem - private void applyDifference(JSONValue driveItem, string driveId, bool isRoot) - { - // Format the OneDrive change into a consumable object for the database - Item item = makeItem(driveItem); - - // Reset the malwareDetected flag for this item - malwareDetected = false; - - // Reset the downloadFailed flag for this item - downloadFailed = false; - - // Path we will be using - string path = ""; - - if(isItemDeleted(driveItem)){ - // Change is to delete an item - log.vdebug("Remote deleted item"); - } else { - // Is the change from OneDrive a 'root' item - // The change should be considered a 'root' item if: - // 1. Contains a ["root"] element - // 2. Has no ["parentReference"]["id"] ... #323 & #324 highlighted that this is false as some 'root' shared objects now can have an 'id' element .. OneDrive API change - // 2. Has no ["parentReference"]["path"] - // 3. Was detected by an input flag as to be handled as a root item regardless of actual status - if (isItemRoot(driveItem) || !hasParentReferencePath(driveItem) || isRoot) { - log.vdebug("Handing a OneDrive 'root' change"); - item.parentId = null; // ensures that it has no parent - item.driveId = driveId; // HACK: makeItem() cannot set the driveId property of the root - log.vdebug("Update/Insert local database with item details"); - itemdb.upsert(item); - log.vdebug("item details: ", item); - return; - } - } - - bool unwanted; - // Check if the parent id is something we need to skip - if (skippedItems.find(item.parentId).length != 0) { - // Potentially need to flag as unwanted - log.vdebug("Flagging as unwanted: find(item.parentId).length != 0"); - unwanted = true; - - // Is this item id in the database? - if (itemdb.idInLocalDatabase(item.driveId, item.id)){ - // item exists in database, most likely moved out of scope for current client configuration - log.vdebug("This item was previously synced / seen by the client"); - if (("name" in driveItem["parentReference"]) != null) { - // How is this out of scope? - // is sync_list configured - if (syncListConfigured) { - // sync_list configured and in use - if (selectiveSync.isPathExcludedViaSyncList(driveItem["parentReference"]["name"].str)) { - // Previously synced item is now out of scope as it has been moved out of what is included in sync_list - log.vdebug("This previously synced item is now excluded from being synced due to sync_list exclusion"); - } - } - // flag to delete local file as it now is no longer in sync with OneDrive - log.vdebug("Flagging to delete item locally"); - idsToDelete ~= [item.driveId, item.id]; - } - } - } - - // Check if this is excluded by config option: skip_dir - if (!unwanted) { - // Only check path if config is != "" - if (cfg.getValueString("skip_dir") != "") { - // Is the item a folder and not a deleted item? - if ((isItemFolder(driveItem)) && (!isItemDeleted(driveItem))) { - // work out the 'snippet' path where this folder would be created - string simplePathToCheck = ""; - string complexPathToCheck = ""; - string matchDisplay = ""; - - if (hasParentReference(driveItem)) { - // we need to workout the FULL path for this item - string parentDriveId = driveItem["parentReference"]["driveId"].str; - string parentItem = driveItem["parentReference"]["id"].str; - // simple path - if (("name" in driveItem["parentReference"]) != null) { - simplePathToCheck = driveItem["parentReference"]["name"].str ~ "/" ~ driveItem["name"].str; - } else { - simplePathToCheck = driveItem["name"].str; - } - log.vdebug("skip_dir path to check (simple): ", simplePathToCheck); - // complex path - if (itemdb.idInLocalDatabase(parentDriveId, parentItem)){ - // build up complexPathToCheck - complexPathToCheck = computeItemPath(parentDriveId, parentItem) ~ "/" ~ driveItem["name"].str; - complexPathToCheck = buildNormalizedPath(complexPathToCheck); - } else { - log.vdebug("Parent details not in database - unable to compute complex path to check"); - } - log.vdebug("skip_dir path to check (complex): ", complexPathToCheck); - } else { - simplePathToCheck = driveItem["name"].str; - } - - // If 'simplePathToCheck' or 'complexPathToCheck' is of the following format: root:/folder - // then isDirNameExcluded matching will not work - // Clean up 'root:' if present - if (startsWith(simplePathToCheck, "root:")){ - log.vdebug("Updating simplePathToCheck to remove 'root:'"); - simplePathToCheck = strip(simplePathToCheck, "root:"); - } - if (startsWith(complexPathToCheck, "root:")){ - log.vdebug("Updating complexPathToCheck to remove 'root:'"); - complexPathToCheck = strip(complexPathToCheck, "root:"); - } - - // OK .. what checks are we doing? - if ((simplePathToCheck != "") && (complexPathToCheck == "")) { - // just a simple check - log.vdebug("Performing a simple check only"); - unwanted = selectiveSync.isDirNameExcluded(simplePathToCheck); - } else { - // simple and complex - log.vdebug("Performing a simple & complex path match if required"); - // simple first - unwanted = selectiveSync.isDirNameExcluded(simplePathToCheck); - matchDisplay = simplePathToCheck; - if (!unwanted) { - log.vdebug("Simple match was false, attempting complex match"); - // simple didnt match, perform a complex check - unwanted = selectiveSync.isDirNameExcluded(complexPathToCheck); - matchDisplay = complexPathToCheck; - } - } - - log.vdebug("Result: ", unwanted); - if (unwanted) log.vlog("Skipping item - excluded by skip_dir config: ", matchDisplay); - } - } - } - - // Check if this is excluded by config option: skip_file - if (!unwanted) { - // Is the item a file and not a deleted item? - if ((isItemFile(driveItem)) && (!isItemDeleted(driveItem))) { - // skip_file can contain 4 types of entries: - // - wildcard - *.txt - // - text + wildcard - name*.txt - // - full path + combination of any above two - /path/name*.txt - // - full path to file - /path/to/file.txt - - // is the parent id in the database? - if (itemdb.idInLocalDatabase(item.driveId, item.parentId)){ - // Compute this item path & need the full path for this file - path = computeItemPath(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 - // However, as 'path' used throughout, use a temp variable with this modification so that we use the temp variable for exclusion checks - string exclusionTestPath = ""; - if (!startsWith(path, "/")){ - // Add '/' to the path - exclusionTestPath = '/' ~ path; - } - - log.vdebug("skip_file item to check: ", exclusionTestPath); - unwanted = selectiveSync.isFileNameExcluded(exclusionTestPath); - 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"); - } - } - } - - // check the item type - if (!unwanted) { - if (isItemFile(driveItem)) { - log.vdebug("The item we are syncing is a file"); - } else if (isItemFolder(driveItem)) { - log.vdebug("The item we are syncing is a folder"); - } else if (isItemRemote(driveItem)) { - log.vdebug("The item we are syncing is a remote item"); - assert(isItemFolder(driveItem["remoteItem"]), "The remote item is not a folder"); - } else { - // Why was this unwanted? - if (path.empty) { - // Compute this item path & need the full path for this file - path = computeItemPath(item.driveId, item.parentId) ~ "/" ~ item.name; - } - // Microsoft OneNote container objects present as neither folder or file but has file size - if ((!isItemFile(driveItem)) && (!isItemFolder(driveItem)) && (hasFileSize(driveItem))) { - // Log that this was skipped as this was a Microsoft OneNote item and unsupported - log.vlog("The Microsoft OneNote Notebook '", path, "' is not supported by this client"); - } else { - // Log that this item was skipped as unsupported - log.vlog("The OneDrive item '", path, "' is not supported by this client"); - } - unwanted = true; - log.vdebug("Flagging as unwanted: item type is not supported"); - } - } - - // Check if this is included by use of sync_list - if (!unwanted) { - // Is the item parent in the local database? - if (itemdb.idInLocalDatabase(item.driveId, item.parentId)){ - // parent item is in the local database - // compute the item path if empty - if (path.empty) { - path = computeItemPath(item.driveId, item.parentId) ~ "/" ~ item.name; - } - // what path are we checking - log.vdebug("sync_list item to check: ", path); - - // Unfortunatly there is no avoiding this call to check if the path is excluded|included via sync_list - if (selectiveSync.isPathExcludedViaSyncList(path)) { - // selective sync advised to skip, however is this a file and are we configured to upload / download files in the root? - if ((isItemFile(driveItem)) && (cfg.getValueBool("sync_root_files")) && (rootName(path) == "") ) { - // This is a file - // We are configured to sync all files in the root - // This is a file in the logical root - unwanted = false; - } else { - // path is unwanted - unwanted = true; - log.vlog("Skipping item - excluded by sync_list config: ", path); - // flagging to skip this file now, but does this exist in the DB thus needs to be removed / deleted? - if (itemdb.idInLocalDatabase(item.driveId, item.id)){ - log.vlog("Flagging item for local delete as item exists in database: ", path); - // flag to delete - idsToDelete ~= [item.driveId, item.id]; - } - } - } - } else { - // Parent not in the database - // Is the parent a 'folder' from another user? ie - is this a 'shared folder' that has been shared with us? - if (defaultDriveId == item.driveId){ - // Flagging as unwanted - log.vdebug("Flagging as unwanted: item.driveId (", item.driveId,"), item.parentId (", item.parentId,") not in local database"); - unwanted = true; - } else { - // Edge case as the parent (from another users OneDrive account) will never be in the database - log.vdebug("The reported parentId is not in the database. This potentially is a shared folder as 'item.driveId' != 'defaultDriveId'. Relevant Details: item.driveId (", item.driveId,"), item.parentId (", item.parentId,")"); - // If we are syncing OneDrive Business Shared Folders, a 'folder' shared with us, has a 'parent' that is not shared with us hence the above message - // What we need to do is query the DB for this 'item.driveId' and use the response from the DB to set the 'item.parentId' for this new item we are trying to add to the database - if (syncBusinessFolders) { - foreach(dbItem; itemdb.selectByDriveId(item.driveId)) { - if (dbItem.name == "root") { - // Ensure that this item uses the root id as parent - log.vdebug("Falsifying item.parentId to be ", dbItem.id); - item.parentId = dbItem.id; - } - } - } else { - // Ensure that this item has no parent - log.vdebug("Setting item.parentId to be null"); - item.parentId = null; - } - log.vdebug("Update/Insert local database with item details"); - itemdb.upsert(item); - log.vdebug("item details: ", item); - return; - } - } - } - - // skip downloading dot files if configured - if (cfg.getValueBool("skip_dotfiles")) { - if (isDotFile(path)) { - log.vlog("Skipping item - .file or .folder: ", path); - unwanted = true; - } - } - - // skip unwanted items early - if (unwanted) { - log.vdebug("Skipping OneDrive change as this is determined to be unwanted"); - skippedItems ~= item.id; - return; - } - - // check if the item has been seen before - Item oldItem; - bool cached = itemdb.selectById(item.driveId, item.id, oldItem); - - // check if the item is going to be deleted - if (isItemDeleted(driveItem)) { - // item.name is not available, so we get a bunch of meaningless log output - // Item name we will attempt to delete will be printed out later - if (cached) { - // flag to delete - log.vdebug("Flagging item for deletion: ", item); - idsToDelete ~= [item.driveId, item.id]; - } else { - // flag to ignore - log.vdebug("Flagging item to skip: ", item); - skippedItems ~= item.id; - } - return; - } - - // rename the local item if it is unsynced and there is a new version of it on OneDrive - string oldPath; - 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"); - // Compute this item path - oldPath = computeItemPath(item.driveId, item.id); - // 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; - - // 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 - 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"); // correct message, remote item is newer - auto ext = extension(oldPath); - auto newPath = path.chomp(ext) ~ "-" ~ deviceName ~ ext; - - // has the user configured to IGNORE local data protection rules? - if (bypassDataPreservation) { - // The user has configured to ignore data safety checks and overwrite local data rather than preserve & rename - log.vlog("WARNING: Local Data Protection has been disabled. You may experience data loss on this file: ", oldPath); - } else { - // local data protection is configured, renaming local file - log.vlog("The local item is out-of-sync with OneDrive, renaming to preserve existing file and prevent data loss: ", oldPath, " -> ", newPath); - - // perform the rename action - if (!dryRun) { - safeRename(oldPath); - } else { - // Expectation here is that there is a new file locally (newPath) however as we don't create this, the "new file" will not be uploaded as it does not exist - log.vdebug("DRY-RUN: Skipping local file rename"); - } - } - } - } - cached = false; - } - } - } - - // update the item - if (cached) { - // the item is in the items.sqlite3 database - log.vdebug("OneDrive change is an update to an existing local item"); - applyChangedItem(oldItem, oldPath, item, path); - } else { - log.vdebug("OneDrive change is potentially a new local item"); - // Check if file should be skipped based on size limit - if (isItemFile(driveItem)) { - if (cfg.getValueLong("skip_size") != 0) { - if (driveItem["size"].integer >= this.newSizeLimit) { - log.vlog("Skipping item - excluded by skip_size config: ", item.name, " (", driveItem["size"].integer/2^^20, " MB)"); - return; - } - } - } - // apply this new item - applyNewItem(item, path); - } - - if ((malwareDetected == false) && (downloadFailed == false)){ - // save the item in the db - // if the file was detected as malware and NOT downloaded, we dont want to falsify the DB as downloading it as otherwise the next pass will think it was deleted, thus delete the remote item - // Likewise if the download failed, we dont want to falsify the DB as downloading it as otherwise the next pass will think it was deleted, thus delete the remote item - if (cached) { - // the item is in the items.sqlite3 database - // Do we need to update the database with the details that were provided by the OneDrive API? - // Is the last modified timestamp in the DB the same as the API data? - SysTime localModifiedTime = oldItem.mtime; - localModifiedTime.fracSecs = Duration.zero; - SysTime remoteModifiedTime = item.mtime; - remoteModifiedTime.fracSecs = Duration.zero; - - // If the timestamp is different, or we are running on a National Cloud Deployment that does not support /delta queries - we have to update the DB with the details from OneDrive - // Unfortunatly because of the consequence of Nataional Cloud Deployments not supporting /delta queries, the application uses the local database to flag what is out-of-date / track changes - // This means that the constant disk writing to the database fix implemented with https://github.com/abraunegg/onedrive/pull/2004 cannot be utilised when using Nataional Cloud Deployments - // as all records are touched / updated when performing the OneDrive sync operations. The only way to change this, is for Microsoft to support /delta queries for Nataional Cloud Deployments - if ((localModifiedTime != remoteModifiedTime) || (nationalCloudDeployment)) { - // Database update needed for this item because our local record is out-of-date - log.vdebug("Updating local database with item details from OneDrive as local record needs to be updated"); - itemdb.update(item); - } - } else { - // item is not in the items.sqlite3 database - log.vdebug("Inserting new item details to local database"); - itemdb.insert(item); - } - // 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"); - } - } - } - - // download an item that was not synced before - private void applyNewItem(const ref Item item, const(string) path) - { - // Test for the local path existence - if (exists(path)) { - // Issue #2209 fix - test if path is a bad symbolic link - if (isSymlink(path)) { - log.vdebug("Path on local disk is a symbolic link ........"); - if (!exists(readLink(path))) { - // reading the symbolic link failed - log.vdebug("Reading the symbolic link target failed ........ "); - log.logAndNotify("Skipping item - invalid symbolic link: ", path); - return; - } - } - - // path exists locally, is not a bad symbolic link - // 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; - } else { - // file is not in sync with the database - // is the local file technically 'newer' based on UTC timestamp? - SysTime localModifiedTime = timeLastModified(path).toUTC(); - SysTime itemModifiedTime = item.mtime; - // HACK: reduce time resolution to seconds before comparing - localModifiedTime.fracSecs = Duration.zero; - itemModifiedTime.fracSecs = Duration.zero; - - // is the local modified time greater than that from OneDrive? - if (localModifiedTime > itemModifiedTime) { - // local file is newer than item on OneDrive based on file modified time - // Is this item id in the database? - if (itemdb.idInLocalDatabase(item.driveId, item.id)){ - // item id is in the database - // no local rename - // no download needed - log.vlog("Local item modified time is newer based on UTC time conversion - keeping local item as this exists in the local database"); - log.vdebug("Skipping OneDrive change as this is determined to be unwanted due to local item modified time being newer than OneDrive item and present in the sqlite database"); - return; - } else { - // item id is not in the database .. maybe a --resync ? - // Should this 'download' be skipped? - // Do we need to check for .nosync? Only if --check-for-nosync was passed in - if (cfg.getValueBool("check_nosync")) { - // need the parent path for this object - string parentPath = dirName(path); - if (exists(parentPath ~ "/.nosync")) { - log.vlog("Skipping downloading item - .nosync found in parent folder & --check-for-nosync is enabled: ", path); - // flag that this download failed, otherwise the 'item' is added to the database - then, as not present on the local disk, would get deleted from OneDrive - downloadFailed = true; - // clean up this partial file, otherwise every sync we will get theis warning - log.vlog("Removing previous partial file download due to .nosync found in parent folder & --check-for-nosync is enabled"); - safeRemove(path); - return; - } - } - // file exists locally but is not in the sqlite database - maybe a failed download? - log.vlog("Local item does not exist in local database - replacing with file from OneDrive - failed download?"); - - - // in a --resync scenario or if items.sqlite3 was deleted before startup we have zero way of knowing IF the local file is meant to be the right file - // we have passed the following checks: - // 1. file exists locally - // 2. local modified time > remote modified time - // 3. id is not in the database - - auto ext = extension(path); - auto newPath = path.chomp(ext) ~ "-" ~ deviceName ~ ext; - // has the user configured to IGNORE local data protection rules? - if (bypassDataPreservation) { - // The user has configured to ignore data safety checks and overwrite local data rather than preserve & rename - log.vlog("WARNING: Local Data Protection has been disabled. You may experience data loss on this file: ", path); - } else { - // local data protection is configured, renaming local file - log.vlog("The local item is out-of-sync with OneDrive, renaming to preserve existing file and prevent local data loss: ", path, " -> ", newPath); - // perform the rename action of the local file - if (!dryRun) { - safeRename(path); - } else { - // Expectation here is that there is a new file locally (newPath) however as we don't create this, the "new file" will not be uploaded as it does not exist - log.vdebug("DRY-RUN: Skipping local file rename"); - } - } - - } - } else { - // remote file is newer than local item - 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; - - // has the user configured to IGNORE local data protection rules? - if (bypassDataPreservation) { - // The user has configured to ignore data safety checks and overwrite local data rather than preserve & rename - log.vlog("WARNING: Local Data Protection has been disabled. You may experience data loss on this file: ", path); - } else { - // local data protection is configured, renaming local file - log.vlog("The local item is out-of-sync with OneDrive, renaming to preserve existing file and prevent data loss: ", path, " -> ", newPath); - // perform the rename action of the local file - if (!dryRun) { - safeRename(path); - } else { - // Expectation here is that there is a new file locally (newPath) however as we don't create this, the "new file" will not be uploaded as it does not exist - log.vdebug("DRY-RUN: Skipping local file rename"); - } - } - } - } - } else { - // Path does not exist locally - this will be a new file download or folder creation - - // Should this 'download' be skipped due to 'skip_dir' directive - if (cfg.getValueString("skip_dir") != "") { - string pathToCheck; - // does the path start with '/'? - if (!startsWith(path, "/")){ - // path does not start with '/', but we need to check skip_dir entries with and without '/' - // so always make sure we are checking a path with '/' - // If this is a file, we need to check the parent path - if (item.type == ItemType.file) { - // use parent path and add '/' - pathToCheck = '/' ~ dirName(path); - } else { - // use path and add '/' - pathToCheck = '/' ~ path; - } - } - - // perform the check - if (selectiveSync.isDirNameExcluded(pathToCheck)) { - // this path should be skipped - if (item.type == ItemType.file) { - log.vlog("Skipping item - file path is excluded by skip_dir config: ", path); - } else { - log.vlog("Skipping item - excluded by skip_dir config: ", path); - } - // flag that this download failed, otherwise the 'item' is added to the database - then, as not present on the local disk, would get deleted from OneDrive - downloadFailed = true; - return; - } - } - - // Should this 'download' be skipped due to nosync directive? - // Do we need to check for .nosync? Only if --check-for-nosync was passed in - if (cfg.getValueBool("check_nosync")) { - // need the parent path for this object - string parentPath = dirName(path); - if (exists(parentPath ~ "/.nosync")) { - log.vlog("Skipping downloading item - .nosync found in parent folder & --check-for-nosync is enabled: ", path); - // flag that this download failed, otherwise the 'item' is added to the database - then, as not present on the local disk, would get deleted from OneDrive - downloadFailed = true; - return; - } - } - } - - // how to handle this item? - final switch (item.type) { - case ItemType.file: - downloadFileItem(item, path); - if (dryRun) { - // we dont download the file, but we need to track that we 'faked it' - idsFaked ~= [item.driveId, item.id]; - } - break; - case ItemType.dir: - case ItemType.remote: - log.log("Creating local directory: ", path); - - // Issue #658 handling - is sync_list in use? - if (syncListConfigured) { - // sync_list configured and in use - // path to create was previously checked if this should be included / excluded. No need to check again. - log.vdebug("Issue #658 handling"); - setOneDriveFullScanTrigger(); - } - - // Issue #865 handling - is skip_dir in use? - if (cfg.getValueString("skip_dir") != "") { - // we have some entries in skip_dir - // path to create was previously checked if this should be included / excluded. No need to check again. - log.vdebug("Issue #865 handling"); - setOneDriveFullScanTrigger(); - } - - if (!dryRun) { - 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()); - // Update the time of the folder to match the last modified time as is provided by OneDrive - // If there are any files then downloaded into this folder, the last modified time will get - // updated by the local Operating System with the latest timestamp - as this is normal operation - // as the directory has been modified - log.vdebug("Setting directory lastModifiedDateTime for: ", path , " to ", item.mtime); - setTimes(path, item.mtime, item.mtime); - } - } catch (FileException e) { - // display the error message - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - // 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]; - } - break; - } - } - - // update a local item - // the local item is assumed to be in sync with the local db - private void applyChangedItem(Item oldItem, string oldPath, Item newItem, string newPath) - { - assert(oldItem.driveId == newItem.driveId); - assert(oldItem.id == newItem.id); - assert(oldItem.type == newItem.type); - assert(oldItem.remoteDriveId == newItem.remoteDriveId); - assert(oldItem.remoteId == newItem.remoteId); - - if (oldItem.eTag != newItem.eTag) { - // handle changed name/path - if (oldPath != newPath) { - log.log("Moving ", oldPath, " to ", newPath); - if (exists(newPath)) { - Item localNewItem; - if (itemdb.selectByPath(newPath, defaultDriveId, localNewItem)) { - // 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 - log.vlog("The destination is occupied, renaming the conflicting file..."); - if (!dryRun) { - safeRename(newPath); - } - } - } else { - // to be overwritten item is not already in the itemdb, so it should - // be synced. Do a safe rename here, too. - // TODO: force remote sync by deleting local item - log.vlog("The destination is occupied by new file, renaming the conflicting file..."); - if (!dryRun) { - safeRename(newPath); - } - } - } - // try and rename path, catch exception - try { - log.vdebug("Calling rename(oldPath, newPath)"); - if (!dryRun) { - // rename physical path on disk - rename(oldPath, newPath); - } else { - // track this as a faked id item - idsFaked ~= [newItem.driveId, newItem.id]; - // we also need to track that we did not rename this path - pathsRenamed ~= [oldPath]; - } - } catch (FileException e) { - // display the error message - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - } - } - // handle changed content and mtime - // HACK: use mtime+hash instead of cTag because of https://github.com/OneDrive/onedrive-api-docs/issues/765 - if (newItem.type == ItemType.file && oldItem.mtime != newItem.mtime && !testFileHash(newPath, newItem)) { - downloadFileItem(newItem, newPath); - } - - // 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 - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - } - } - } - } - - // downloads a File resource - private void downloadFileItem(const ref Item item, const(string) path) - { - static import std.exception; - assert(item.type == ItemType.file); - write("Downloading file ", path, " ... "); - JSONValue fileDetails; - - try { - fileDetails = onedrive.getFileDetails(item.driveId, item.id); - } catch (OneDriveException e) { - log.error("ERROR: Query of OneDrive for file details failed"); - if (e.httpStatusCode >= 500) { - // OneDrive returned a 'HTTP 5xx Server Side Error' - gracefully handling error - error message already logged - downloadFailed = true; - return; - } - } - - // fileDetails has to be a valid JSON object - if (fileDetails.type() == JSONType.object){ - if (isMalware(fileDetails)){ - // OneDrive reports that this file is malware - log.error("ERROR: MALWARE DETECTED IN FILE - DOWNLOAD SKIPPED"); - // set global flag - malwareDetected = true; - return; - } - } else { - // Issue #550 handling - log.error("ERROR: Query of OneDrive for file details failed"); - log.vdebug("onedrive.getFileDetails call returned an invalid JSON Object"); - // We want to return, cant download - downloadFailed = true; - return; - } - - if (!dryRun) { - ulong onlineFileSize = 0; - string OneDriveFileHash; - - // fileDetails should be a valid JSON due to prior check - if (hasFileSize(fileDetails)) { - // Use the configured onlineFileSize as reported by OneDrive - onlineFileSize = fileDetails["size"].integer; - } else { - // filesize missing - log.vdebug("WARNING: fileDetails['size'] is missing"); - } - - if (hasHashes(fileDetails)) { - // File details returned hash details - // QuickXorHash - if (hasQuickXorHash(fileDetails)) { - // Use the configured quickXorHash as reported by OneDrive - if (fileDetails["file"]["hashes"]["quickXorHash"].str != "") { - OneDriveFileHash = fileDetails["file"]["hashes"]["quickXorHash"].str; - } - } else { - // Check for sha256Hash as quickXorHash did not exist - if (hasSHA256Hash(fileDetails)) { - // Use the configured sha256Hash as reported by OneDrive - if (fileDetails["file"]["hashes"]["sha256Hash"].str != "") { - OneDriveFileHash = fileDetails["file"]["hashes"]["sha256Hash"].str; - } - } - } - } else { - // file hash data missing - log.vdebug("WARNING: fileDetails['file']['hashes'] is missing - unable to compare file hash after download"); - } - - // Is there enough free space locally to download the file - // - We can use '.' here as we change the current working directory to the configured 'sync_dir' - ulong localActualFreeSpace = to!ulong(getAvailableDiskSpace(".")); - // So that we are not responsible in making the disk 100% full if we can download the file, compare the current available space against the reservation set and file size - // The reservation value is user configurable in the config file, 50MB by default - ulong freeSpaceReservation = cfg.getValueLong("space_reservation"); - // debug output - log.vdebug("Local Disk Space Actual: ", localActualFreeSpace); - log.vdebug("Free Space Reservation: ", freeSpaceReservation); - log.vdebug("File Size to Download: ", onlineFileSize); - - // calculate if we can download file - if ((localActualFreeSpace < freeSpaceReservation) || (onlineFileSize > localActualFreeSpace)) { - // localActualFreeSpace is less than freeSpaceReservation .. insufficient free space - // onlineFileSize is greater than localActualFreeSpace .. insufficient free space - writeln("failed!"); - log.log("Insufficient local disk space to download file"); - downloadFailed = true; - return; - } - - // Attempt to download the file - try { - onedrive.downloadById(item.driveId, item.id, path, onlineFileSize); - } catch (OneDriveException e) { - log.vdebug("onedrive.downloadById(item.driveId, item.id, path, onlineFileSize); 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 - // Back off & retry with incremental delay - int retryCount = 10; - int retryAttempts = 1; - int backoffInterval = 2; - while (retryAttempts < retryCount){ - // retry in 2,4,8,16,32,64,128,256,512,1024 seconds - Thread.sleep(dur!"seconds"(retryAttempts*backoffInterval)); - try { - onedrive.downloadById(item.driveId, item.id, path, onlineFileSize); - // successful download - retryAttempts = retryCount; - } catch (OneDriveException e) { - log.vdebug("onedrive.downloadById(item.driveId, item.id, path, onlineFileSize); generated a OneDriveException"); - if ((e.httpStatusCode == 429) || (e.httpStatusCode == 408)) { - // If another 408 .. - if (e.httpStatusCode == 408) { - // Increment & loop around - log.vdebug("HTTP 408 generated - incrementing retryAttempts"); - retryAttempts++; - } - // If a 429 .. - if (e.httpStatusCode == 429) { - // Increment & loop around - handleOneDriveThrottleRequest(); - log.vdebug("HTTP 429 generated - incrementing retryAttempts"); - retryAttempts++; - } - } else { - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - } - } - } - } - - if (e.httpStatusCode == 429) { - // HTTP request returned status code 429 (Too Many Requests) - // https://github.com/abraunegg/onedrive/issues/133 - int retryCount = 10; - int retryAttempts = 1; - while (retryAttempts < retryCount){ - // retry after waiting the timeout value from the 429 HTTP response header Retry-After - handleOneDriveThrottleRequest(); - try { - onedrive.downloadById(item.driveId, item.id, path, onlineFileSize); - // successful download - retryAttempts = retryCount; - } catch (OneDriveException e) { - log.vdebug("onedrive.downloadById(item.driveId, item.id, path, onlineFileSize); generated a OneDriveException"); - if ((e.httpStatusCode == 429) || (e.httpStatusCode == 408)) { - // If another 408 .. - if (e.httpStatusCode == 408) { - // Increment & loop around - log.vdebug("HTTP 408 generated - incrementing retryAttempts"); - retryAttempts++; - } - // If a 429 .. - if (e.httpStatusCode == 429) { - // Increment & loop around - handleOneDriveThrottleRequest(); - log.vdebug("HTTP 429 generated - incrementing retryAttempts"); - retryAttempts++; - } - } else { - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - } - } - } - } - } catch (FileException e) { - // There was a file system error - // display the error message - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - downloadFailed = true; - return; - } catch (std.exception.ErrnoException e) { - // There was a file system error - // display the error message - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - downloadFailed = true; - return; - } - // file has to have downloaded in order to set the times / data for the file - if (exists(path)) { - // When downloading some files from SharePoint, the OneDrive API reports one file size, but the SharePoint HTTP Server sends a totally different byte count - // for the same file - // we have implemented --disable-download-validation to disable these checks - - if (!disableDownloadValidation) { - // A 'file' was downloaded - does what we downloaded = reported onlineFileSize or if there is some sort of funky local disk compression going on - // does the file hash OneDrive reports match what we have locally? - string quickXorHash = computeQuickXorHash(path); - // Compute the local file size - ulong localFileSize = getSize(path); - - if ((localFileSize == onlineFileSize) || (OneDriveFileHash == quickXorHash)) { - // 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 - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - } - } else { - // size error? - if (localFileSize != onlineFileSize) { - // downloaded file size does not match - log.vdebug("Actual file size on disk: ", localFileSize); - log.vdebug("OneDrive API reported size: ", onlineFileSize); - log.error("ERROR: File download size mis-match. Increase logging verbosity to determine why."); - } - // hash error? - if (OneDriveFileHash != quickXorHash) { - // downloaded file hash does not match - log.vdebug("Actual local file hash: ", quickXorHash); - log.vdebug("OneDrive API reported hash: ", OneDriveFileHash); - log.error("ERROR: File download hash mis-match. Increase logging verbosity to determine why."); - } - // add some workaround messaging - if (accountType == "documentLibrary"){ - // It has been seen where SharePoint / OneDrive API reports one size via the JSON - // but the content length and file size written to disk is totally different - example: - // From JSON: "size": 17133 - // From HTTPS Server: < Content-Length: 19340 - // with no logical reason for the difference, except for a 302 redirect before file download - log.error("INFO: It is most likely that a SharePoint OneDrive API issue is the root cause. Add --disable-download-validation to work around this issue but downloaded data integrity cannot be guaranteed."); - } else { - // other account types - log.error("INFO: Potentially add --disable-download-validation to work around this issue but downloaded data integrity cannot be guaranteed."); - } - - // we do not want this local file to remain on the local file system - safeRemove(path); - downloadFailed = true; - return; - } - } else { - // download checks have been disabled - log.vdebug("Downloaded file validation disabled due to --disable-download-validation "); - } - } else { - log.error("ERROR: File failed to download. Increase logging verbosity to determine why."); - downloadFailed = true; - return; - } - } - - if (!downloadFailed) { - writeln("done."); - log.fileOnly("Downloading file ", path, " ... done."); - } else { - writeln("failed!"); - log.fileOnly("Downloading file ", path, " ... failed!"); - } - } - - // returns true if the given item corresponds to the local one - private bool isItemSynced(const ref Item item, const(string) path, string itemSource) - { - if (!exists(path)) return false; - final switch (item.type) { - case ItemType.file: - if (isFile(path)) { - // can we actually read the local file? - if (readLocalFile(path)){ - // local file is readable - SysTime localModifiedTime = timeLastModified(path).toUTC(); - SysTime itemModifiedTime = item.mtime; - // HACK: reduce time resolution to seconds before comparing - 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, " when compared to ", itemSource, " modified time ", itemModifiedTime); - // The file has been modified ... is the hash the same? - // Test the file hash as the date / time stamp is different - // Generating a hash is computationally expensive - only generate the hash if timestamp was modified - if (testFileHash(path, item)) { - return true; - } else { - log.vlog("The local item has a different hash when compared to ", itemSource, " item hash"); - } - } - } else { - // Unable to read local file - log.log("Unable to determine the sync state of this file as it cannot be read (file permissions or file corruption): ", path); - return false; - } - } else { - log.vlog("The local item is a directory but should be a file"); - } - break; - case ItemType.dir: - case ItemType.remote: - if (isDir(path)) { - return true; - } else { - log.vlog("The local item is a file but should be a directory"); - } - break; - } - return false; - } - - private void deleteItems() - { - foreach_reverse (i; idsToDelete) { - Item item; - string path; - if (!itemdb.selectById(i[0], i[1], item)) continue; // check if the item is in the db - // Compute this item path - path = computeItemPath(i[0], i[1]); - // Try to delete item object - log.log("Trying to delete item ", path); - if (!dryRun) { - // Actually process the database entry removal - itemdb.deleteById(item.driveId, item.id); - if (item.remoteDriveId != null) { - // delete the linked remote folder - itemdb.deleteById(item.remoteDriveId, item.remoteId); - } - } - bool needsRemoval = false; - if (exists(path)) { - // path exists on the local system - // make sure that the path refers to the correct item - Item pathItem; - if (itemdb.selectByPath(path, item.driveId, pathItem)) { - if (pathItem.id == item.id) { - needsRemoval = true; - } else { - log.log("Skipped due to id difference!"); - } - } else { - // item has disappeared completely - needsRemoval = true; - } - } - if (needsRemoval) { - log.log("Deleting item ", path); - if (!dryRun) { - if (isFile(path)) { - remove(path); - } else { - try { - // Remove any children of this path if they still exist - // Resolve 'Directory not empty' error when deleting local files - foreach (DirEntry child; dirEntries(path, SpanMode.depth, false)) { - attrIsDir(child.linkAttributes) ? rmdir(child.name) : remove(child.name); - } - // Remove the path now that it is empty of children - rmdirRecurse(path); - } catch (FileException e) { - // display the error message - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - } - } - } - } - } - - if (!dryRun) { - // clean up idsToDelete - idsToDelete.length = 0; - assumeSafeAppend(idsToDelete); - } - } - - // scan the given directory for differences and new items - for use with --synchronize - void scanForDifferences(const(string) path) - { - // To improve logging output for this function, what is the 'logical path' we are scanning for file & folder differences? - string logPath; - if (path == ".") { - // get the configured sync_dir - logPath = buildNormalizedPath(cfg.getValueString("sync_dir")); - } else { - // use what was passed in - 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 - if (nationalCloudDeployment) { - // Select items that have a out-of-sync flag set - flagNationalCloudDeploymentOutOfSyncItems(); - } - - // scan for changes in the path provided - if (isDir(path)) { - // if this path is a directory, output this message. - // if a file, potentially leads to confusion as to what the client is actually doing - log.log("Uploading differences of ", logPath); - } - - Item item; - // For each unique OneDrive driveID we know about - foreach (driveId; driveIDsArray) { - log.vdebug("Processing DB entries for this driveId: ", driveId); - // Database scan of every item in DB for the given driveId based on the root parent for that drive - if ((syncBusinessFolders) && (driveId != defaultDriveId)) { - // There could be multiple shared folders all from this same driveId - are we doing a single directory sync? - if (cfg.getValueString("single_directory") != ""){ - // Limit the local filesystem check to just the requested directory - if (itemdb.selectByPath(path, driveId, item)) { - // Does it still exist on disk in the location the DB thinks it is - log.vdebug("Calling uploadDifferences(dbItem) as item is present in local cache DB"); - uploadDifferences(item); - } - } else { - // check everything associated with each driveId we know about - foreach(dbItem; itemdb.selectByDriveId(driveId)) { - // Does it still exist on disk in the location the DB thinks it is - log.vdebug("Calling uploadDifferences(dbItem) as item is present in local cache DB"); - uploadDifferences(dbItem); - } - } - } else { - if (itemdb.selectByPath(path, driveId, item)) { - // Does it still exist on disk in the location the DB thinks it is - log.vdebug("Calling uploadDifferences(dbItem) as item is present in local cache DB"); - uploadDifferences(item); - } - } - } - - // scan for changes in the path provided - if (isDir(path)) { - // if this path is a directory, output this message. - // if a file, potentially leads to confusion as to what the client is actually doing - log.log("Uploading new items of ", logPath); - } - - // Filesystem walk to find new files not uploaded - uploadNewItems(path); - // clean up idsToDelete only if --dry-run is set - if (dryRun) { - idsToDelete.length = 0; - assumeSafeAppend(idsToDelete); - } - } - - // scan the given directory for differences only - for use with --monitor - void scanForDifferencesDatabaseScan(const(string) path) - { - // To improve logging output for this function, what is the 'logical path' we are scanning for file & folder differences? - string logPath; - if (path == ".") { - // get the configured sync_dir - logPath = buildNormalizedPath(cfg.getValueString("sync_dir")); - } else { - // use what was passed in - 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 - if (nationalCloudDeployment) { - // Select items that have a out-of-sync flag set - flagNationalCloudDeploymentOutOfSyncItems(); - } - - // scan for changes in the path provided - if (isDir(path)) { - // if this path is a directory, output this message. - // if a file, potentially leads to confusion as to what the client is actually doing - log.vlog("Uploading differences of ", logPath); - } - Item item; - // For each unique OneDrive driveID we know about - foreach (driveId; driveIDsArray) { - log.vdebug("Processing DB entries for this driveId: ", driveId); - // Database scan of every item in DB for the given driveId based on the root parent for that drive - if ((syncBusinessFolders) && (driveId != defaultDriveId)) { - // There could be multiple shared folders all from this same driveId - are we doing a single directory sync? - if (cfg.getValueString("single_directory") != ""){ - // Limit the local filesystem check to just the requested directory - if (itemdb.selectByPath(path, driveId, item)) { - // Does it still exist on disk in the location the DB thinks it is - log.vdebug("Calling uploadDifferences(dbItem) as item is present in local cache DB"); - uploadDifferences(item); - } - } else { - // check everything associated with each driveId we know about - foreach(dbItem; itemdb.selectByDriveId(driveId)) { - // Does it still exist on disk in the location the DB thinks it is - log.vdebug("Calling uploadDifferences(dbItem) as item is present in local cache DB"); - uploadDifferences(dbItem); - } - } - } else { - if (itemdb.selectByPath(path, driveId, item)) { - // Does it still exist on disk in the location the DB thinks it is - log.vdebug("Calling uploadDifferences(dbItem) as item is present in local cache DB"); - uploadDifferences(item); - } - } - } - } - - void flagNationalCloudDeploymentOutOfSyncItems() { - // 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, however National Cloud Deployments do not support /delta as a query - // https://docs.microsoft.com/en-us/graph/deployments#supported-features - // Select items that have a out-of-sync flag set - foreach (driveId; driveIDsArray) { - // For each unique OneDrive driveID we know about - Item[] outOfSyncItems = itemdb.selectOutOfSyncItems(driveId); - foreach (item; outOfSyncItems) { - if (!dryRun) { - // clean up idsToDelete - idsToDelete.length = 0; - assumeSafeAppend(idsToDelete); - // flag to delete local file as it now is no longer in sync with OneDrive - log.vdebug("Flagging to delete local item as it now is no longer in sync with OneDrive"); - log.vdebug("item: ", item); - idsToDelete ~= [item.driveId, item.id]; - // delete items in idsToDelete - if (idsToDelete.length > 0) deleteItems(); - } - } - } - } - - 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 or --cleanup-local-files - void scanForDifferencesFilesystemScan(const(string) path) - { - // To improve logging output for this function, what is the 'logical path' we are scanning for file & folder differences? - string logPath; - if (path == ".") { - // get the configured sync_dir - logPath = buildNormalizedPath(cfg.getValueString("sync_dir")); - } else { - // use what was passed in - logPath = path; - } - - // scan for changes in the path provided - if (isDir(path)) { - // if this path is a directory, output this message. - // if a file, potentially leads to confusion as to what the client is actually doing - if (!cleanupLocalFiles) { - // if --cleanup-local-files was set, we will not be uploading data - log.vlog("Uploading new items of ", logPath); - } - } - - // Filesystem walk to find extra files that reside locally. - // If --cleanup-local-files is not used, these will be uploaded (normal operation) - // If --download-only --cleanup-local-files is being used, extra files found locally will be deleted from the local filesystem - uploadNewItems(path); - } - - private void uploadDifferences(const ref Item item) - { - // see if this item.id we were supposed to have deleted - // match early and return - if (dryRun) { - foreach (i; idsToDelete) { - if (i[1] == item.id) { - return; - } - } - } - - bool unwanted = false; - string path; - - // Compute this item path early as we we use this path often - path = computeItemPath(item.driveId, item.id); - - // item.id was in the database associated with the item.driveId specified - log.vlog("Processing ", buildNormalizedPath(path)); - - // What type of DB item are we processing - // 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? - unwanted = selectiveSync.isFileNameExcluded(item.name); - } - - // If path or filename does not exclude, is this excluded due to use of selective sync? - if (!unwanted) { - // is sync_list configured - if (syncListConfigured) { - // sync_list configured and in use - // Is the path excluded via sync_list? - unwanted = selectiveSync.isPathExcludedViaSyncList(path); - } - } - - // skip unwanted items - if (unwanted) { - //log.vlog("Filtered out"); - return; - } - - // Check against Microsoft OneDrive restriction and limitations about Windows naming files - if (!isValidName(path)) { - log.logAndNotify("Skipping item - invalid name (Microsoft Naming Convention): ", path); - return; - } - - // Check for bad whitespace items - if (!containsBadWhiteSpace(path)) { - log.logAndNotify("Skipping item - invalid name (Contains an invalid whitespace item): ", path); - return; - } - - // Check for HTML ASCII Codes as part of file name - if (!containsASCIIHTMLCodes(path)) { - log.logAndNotify("Skipping item - invalid name (Contains HTML ASCII Code): ", path); - return; - } - - final switch (item.type) { - case ItemType.dir: - uploadDirDifferences(item, path); - break; - case ItemType.file: - uploadFileDifferences(item, path); - break; - case ItemType.remote: - uploadRemoteDirDifferences(item, path); - break; - } - } - - private void uploadDirDifferences(const ref Item item, const(string) path) - { - assert(item.type == ItemType.dir); - if (exists(path)) { - // Fix https://github.com/abraunegg/onedrive/issues/1915 - try { - if (!isDir(path)) { - log.vlog("The item was a directory but now it is a file"); - uploadDeleteItem(item, path); - uploadNewFile(path); - } else { - log.vlog("The directory has not changed"); - // loop through the children - foreach (Item child; itemdb.selectChildren(item.driveId, item.id)) { - uploadDifferences(child); - } - } - } catch (FileException e) { - // display the error message - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - return; - } - } else { - // Directory does not exist locally - // If we are in a --dry-run situation - this directory may never have existed as we never downloaded it - if (!dryRun) { - // Not --dry-run situation - if (!cfg.getValueBool("monitor")) { - // Not in --monitor mode - log.vlog("The directory has been deleted locally"); - } else { - // Appropriate message as we are in --monitor mode - log.vlog("The directory appears to have been deleted locally .. but we are running in --monitor mode. This may have been 'moved' on the local filesystem rather than being 'deleted'"); - log.vdebug("Most likely cause - 'inotify' event was missing for whatever action was taken locally or action taken when application was stopped"); - } - // A moved file will be uploaded as 'new', delete the old file and reference - if (noRemoteDelete) { - // do not process remote directory delete - log.vlog("Skipping remote directory delete as --upload-only & --no-remote-delete configured"); - } else { - uploadDeleteItem(item, path); - } - } else { - // we are in a --dry-run situation, directory appears to have deleted locally - this directory may never have existed as we never downloaded it .. - // Check if path does not exist in database - Item databaseItem; - if (!itemdb.selectByPath(path, defaultDriveId, databaseItem)) { - // Path not found in database - log.vlog("The directory has been deleted locally"); - if (noRemoteDelete) { - // do not process remote directory delete - log.vlog("Skipping remote directory delete as --upload-only & --no-remote-delete configured"); - } else { - uploadDeleteItem(item, path); - } - } else { - // Path was found in the database - // Did we 'fake create it' as part of --dry-run ? - foreach (i; idsFaked) { - if (i[1] == item.id) { - log.vdebug("Matched faked dir which is 'supposed' to exist but not created due to --dry-run use"); - log.vlog("The directory has not changed"); - return; - } - } - // item.id did not match a 'faked' download new directory creation - log.vlog("The directory has been deleted locally"); - uploadDeleteItem(item, path); - } - } - } - } - - private void uploadRemoteDirDifferences(const ref Item item, const(string) path) - { - assert(item.type == ItemType.remote); - if (exists(path)) { - if (!isDir(path)) { - log.vlog("The item was a directory but now it is a file"); - uploadDeleteItem(item, path); - uploadNewFile(path); - } else { - log.vlog("The directory has not changed"); - // continue through the linked folder - assert(item.remoteDriveId && item.remoteId); - Item remoteItem; - bool found = itemdb.selectById(item.remoteDriveId, item.remoteId, remoteItem); - if(found){ - // item was found in the database - uploadDifferences(remoteItem); - } - } - } else { - // are we in a dry-run scenario - if (!dryRun) { - // no dry-run - log.vlog("The directory has been deleted locally"); - if (noRemoteDelete) { - // do not process remote directory delete - log.vlog("Skipping remote directory delete as --upload-only & --no-remote-delete configured"); - } else { - uploadDeleteItem(item, path); - } - } else { - // we are in a --dry-run situation, directory appears to have deleted locally - this directory may never have existed as we never downloaded it .. - // Check if path does not exist in database - Item databaseItem; - if (!itemdb.selectByPathWithoutRemote(path, defaultDriveId, databaseItem)) { - // Path not found in database - log.vlog("The directory has been deleted locally"); - if (noRemoteDelete) { - // do not process remote directory delete - log.vlog("Skipping remote directory delete as --upload-only & --no-remote-delete configured"); - } else { - uploadDeleteItem(item, path); - } - } else { - // Path was found in the database - // Did we 'fake create it' as part of --dry-run ? - foreach (i; idsFaked) { - if (i[1] == item.id) { - log.vdebug("Matched faked dir which is 'supposed' to exist but not created due to --dry-run use"); - log.vlog("The directory has not changed"); - return; - } - } - // item.id did not match a 'faked' download new directory creation - log.vlog("The directory has been deleted locally"); - uploadDeleteItem(item, path); - } - } - } - } - - // upload local file system differences to OneDrive - private void uploadFileDifferences(const ref Item item, const(string) path) - { - // Reset upload failure - OneDrive or filesystem issue (reading data) - uploadFailed = false; - - // uploadFileDifferences is called when processing DB entries to compare against actual files on disk - string itemSource = "database"; - - assert(item.type == ItemType.file); - if (exists(path)) { - if (isFile(path)) { - // can we actually read the local file? - if (readLocalFile(path)){ - // file is readable - 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; - - if (localModifiedTime != itemModifiedTime) { - log.vlog("The file last modified time has changed"); - log.vdebug("The local item has a different modified time ", localModifiedTime, " when compared to ", itemSource, " modified time ", itemModifiedTime); - string eTag = item.eTag; - - // perform file hash tests - has the content of the file changed? - if (!testFileHash(path, item)) { - log.vlog("The file content has changed"); - log.vdebug("The local item has a different hash when compared to ", itemSource, " item hash"); - write("Uploading modified file ", path, " ... "); - JSONValue response; - - if (!dryRun) { - // Get the file size - long thisFileSize = getSize(path); - // Are we using OneDrive Personal or OneDrive Business? - // To solve 'Multiple versions of file shown on website after single upload' (https://github.com/abraunegg/onedrive/issues/2) - // check what 'account type' this is as this issue only affects OneDrive Business so we need some extra logic here - if (accountType == "personal"){ - // Original file upload logic - if (thisFileSize <= thresholdFileSize) { - try { - response = onedrive.simpleUploadReplace(path, item.driveId, item.id, item.eTag); - } catch (OneDriveException e) { - if (e.httpStatusCode == 401) { - // OneDrive returned a 'HTTP/1.1 401 Unauthorized Error' - file failed to be uploaded - writeln("skipped."); - log.vlog("OneDrive returned a 'HTTP 401 - Unauthorized' - gracefully handling error"); - uploadFailed = true; - return; - } - if (e.httpStatusCode == 404) { - // HTTP request returned status code 404 - the eTag provided does not exist - // Delete record from the local database - file will be uploaded as a new file - writeln("skipped."); - log.vlog("OneDrive returned a 'HTTP 404 - eTag Issue' - gracefully handling error"); - itemdb.deleteById(item.driveId, item.id); - uploadFailed = true; - return; - } - // Resolve https://github.com/abraunegg/onedrive/issues/36 - if ((e.httpStatusCode == 409) || (e.httpStatusCode == 423)) { - // The file is currently checked out or locked for editing by another user - // We cant upload this file at this time - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - write("", path, " is currently checked out or locked for editing by another user."); - log.fileOnly(path, " is currently checked out or locked for editing by another user."); - uploadFailed = true; - return; - } - if (e.httpStatusCode == 412) { - // HTTP request returned status code 412 - ETag does not match current item's value - // Delete record from the local database - file will be uploaded as a new file - writeln("skipped."); - log.vdebug("Simple Upload Replace Failed - OneDrive eTag / cTag match issue (Personal Account)"); - log.vlog("OneDrive returned a 'HTTP 412 - Precondition Failed' - gracefully handling error. Will upload as new file."); - itemdb.deleteById(item.driveId, item.id); - uploadFailed = true; - return; - } - if (e.httpStatusCode == 504) { - // HTTP request returned status code 504 (Gateway Timeout) - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' - retrying upload request as a session"); - // Try upload as a session - response = session.upload(path, item.driveId, item.parentId, baseName(path), item.eTag); - } else { - // display what the error is - writeln("skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } catch (FileException e) { - // display the error message - writeln("skipped."); - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - // upload done without error - writeln("done."); - } else { - writeln(""); - try { - response = session.upload(path, item.driveId, item.parentId, baseName(path), item.eTag); - } catch (OneDriveException e) { - if (e.httpStatusCode == 401) { - // OneDrive returned a 'HTTP/1.1 401 Unauthorized Error' - file failed to be uploaded - writeln("skipped."); - log.vlog("OneDrive returned a 'HTTP 401 - Unauthorized' - gracefully handling error"); - uploadFailed = true; - return; - } - if (e.httpStatusCode == 412) { - // HTTP request returned status code 412 - ETag does not match current item's value - // Delete record from the local database - file will be uploaded as a new file - writeln("skipped."); - log.vdebug("Session Upload Replace Failed - OneDrive eTag / cTag match issue (Personal Account)"); - log.vlog("OneDrive returned a 'HTTP 412 - Precondition Failed' - gracefully handling error. Will upload as new file."); - itemdb.deleteById(item.driveId, item.id); - uploadFailed = true; - return; - } else { - // display what the error is - writeln("skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } catch (FileException e) { - // display the error message - writeln("skipped."); - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - // upload done without error - writeln("done."); - } - } else { - // OneDrive Business Account - // We need to always use a session to upload, but handle the changed file correctly - if (accountType == "business"){ - try { - // is this a zero-byte file? - if (thisFileSize == 0) { - // the file we are trying to upload as a session is a zero byte file - we cant use a session to upload or replace the file - // as OneDrive technically does not support zero byte files - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - log.vlog("Skip Reason: Microsoft OneDrive does not support 'zero-byte' files as a modified upload. Will upload as new file."); - // delete file on OneDrive - onedrive.deleteById(item.driveId, item.id, item.eTag); - // delete file from local database - itemdb.deleteById(item.driveId, item.id); - return; - } else { - if ((!syncBusinessFolders) || (item.driveId == defaultDriveId)) { - // For logging consistency - writeln(""); - // If we are not syncing Shared Business Folders, or this change is going to the 'users' default drive, handle normally - // Perform a normal session upload - response = session.upload(path, item.driveId, item.parentId, baseName(path), item.eTag); - } else { - // If we are uploading to a shared business folder, there are a couple of corner cases here: - // 1. Shared Folder is a 'users' folder - // 2. Shared Folder is a 'SharePoint Library' folder, meaning we get hit by this stupidity: https://github.com/OneDrive/onedrive-api-docs/issues/935 - response = handleSharePointMetadataAdditionBug(item, path); - } - } - } catch (OneDriveException e) { - if (e.httpStatusCode == 401) { - // OneDrive returned a 'HTTP/1.1 401 Unauthorized Error' - file failed to be uploaded - writeln("skipped."); - log.vlog("OneDrive returned a 'HTTP 401 - Unauthorized' - gracefully handling error"); - uploadFailed = true; - return; - } - // Resolve https://github.com/abraunegg/onedrive/issues/36 - if ((e.httpStatusCode == 409) || (e.httpStatusCode == 423)) { - // The file is currently checked out or locked for editing by another user - // We cant upload this file at this time - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - writeln("", path, " is currently checked out or locked for editing by another user."); - log.fileOnly(path, " is currently checked out or locked for editing by another user."); - uploadFailed = true; - return; - } - if (e.httpStatusCode == 412) { - // HTTP request returned status code 412 - ETag does not match current item's value - // Delete record from the local database - file will be uploaded as a new file - writeln("skipped."); - log.vdebug("Session Upload Replace Failed - OneDrive eTag / cTag match issue (Business Account)"); - log.vlog("OneDrive returned a 'HTTP 412 - Precondition Failed' - gracefully handling error. Will upload as new file."); - itemdb.deleteById(item.driveId, item.id); - uploadFailed = true; - return; - } else { - // display what the error is - writeln("skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } catch (FileException e) { - // display the error message - writeln("skipped."); - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - // Did the upload fail? - if (!uploadFailed){ - // upload done without error or failure - writeln("done."); - // As the session.upload includes the last modified time, save the response - // Is the response a valid JSON object - validation checking done in saveItem - saveItem(response); - } else { - // uploadFailed, return - return; - } - } - - // OneDrive documentLibrary - if (accountType == "documentLibrary"){ - // is this a zero-byte file? - if (thisFileSize == 0) { - // the file we are trying to upload as a session is a zero byte file - we cant use a session to upload or replace the file - // as OneDrive technically does not support zero byte files - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - log.vlog("Skip Reason: Microsoft OneDrive does not support 'zero-byte' files as a modified upload. Will upload as new file."); - // delete file on OneDrive - onedrive.deleteById(item.driveId, item.id, item.eTag); - // delete file from local database - itemdb.deleteById(item.driveId, item.id); - return; - } else { - // Due to https://github.com/OneDrive/onedrive-api-docs/issues/935 Microsoft modifies all PDF, MS Office & HTML files with added XML content. It is a 'feature' of SharePoint. - // This means, as a session upload, on 'completion' the file is 'moved' and generates a 404 ...... - response = handleSharePointMetadataAdditionBug(item, path); - - // Did the upload fail? - if (!uploadFailed){ - // upload done without error or failure - writeln("done."); - // As the session.upload includes the last modified time, save the response - // Is the response a valid JSON object - validation checking done in saveItem - saveItem(response); - } else { - // uploadFailed, return - return; - } - } - } - } - - // Update etag with ctag from response - if ("cTag" in response) { - // use the cTag instead of the eTag because OneDrive may update the metadata of files AFTER they have been uploaded via simple upload - eTag = response["cTag"].str; - } else { - // Is there an eTag in the response? - if ("eTag" in response) { - // use the eTag from the response as there was no cTag - eTag = response["eTag"].str; - } else { - // no tag available - set to nothing - eTag = ""; - } - } - - // log that the modified file was uploaded successfully - log.fileOnly("Uploading modified file ", path, " ... done."); - - // update free space tracking if this is our drive id - if (item.driveId == defaultDriveId) { - // how much space is left on OneDrive after upload? - remainingFreeSpace = (remainingFreeSpace - thisFileSize); - log.vlog("Remaining free space on OneDrive: ", remainingFreeSpace); - } - } else { - // we are --dry-run - simulate the file upload - writeln("done."); - response = createFakeResponse(path); - // Log action to log file - log.fileOnly("Uploading modified file ", path, " ... done."); - // Is the response a valid JSON object - validation checking done in saveItem - saveItem(response); - return; - } - } - if (accountType == "personal"){ - // If Personal, call to update the modified time as stored on OneDrive - if (!dryRun) { - uploadLastModifiedTime(item.driveId, item.id, eTag, localModifiedTime.toUTC()); - } - } - } else { - log.vlog("The file has not changed"); - } - } else { - //The file is not readable - skipped - log.log("Skipping processing this file as it cannot be read (file permissions or file corruption): ", path); - uploadFailed = true; - } - } else { - log.vlog("The item was a file but now is a directory"); - uploadDeleteItem(item, path); - uploadCreateDir(path); - } - } else { - // File does not exist locally - // If we are in a --dry-run situation - this file may never have existed as we never downloaded it - if (!dryRun) { - // Not --dry-run situation - if (!cfg.getValueBool("monitor")) { - log.vlog("The file has been deleted locally"); - } else { - // Appropriate message as we are in --monitor mode - log.vlog("The file appears to have been deleted locally .. but we are running in --monitor mode. This may have been 'moved' on the local filesystem rather than being 'deleted'"); - log.vdebug("Most likely cause - 'inotify' event was missing for whatever action was taken locally or action taken when application was stopped"); - } - // A moved file will be uploaded as 'new', delete the old file and reference - if (noRemoteDelete) { - // do not process remote file delete - log.vlog("Skipping remote file delete as --upload-only & --no-remote-delete configured"); - } else { - uploadDeleteItem(item, path); - } - } else { - // We are in a --dry-run situation, file appears to have deleted locally - this file may never have existed as we never downloaded it .. - // Check if path does not exist in database - Item databaseItem; - if (!itemdb.selectByPath(path, defaultDriveId, databaseItem)) { - // file not found in database - log.vlog("The file has been deleted locally"); - if (noRemoteDelete) { - // do not process remote file delete - log.vlog("Skipping remote file delete as --upload-only & --no-remote-delete configured"); - } else { - uploadDeleteItem(item, path); - } - } else { - // file was found in the database - // Did we 'fake create it' as part of --dry-run ? - foreach (i; idsFaked) { - if (i[1] == item.id) { - log.vdebug("Matched faked file which is 'supposed' to exist but not created due to --dry-run use"); - log.vlog("The file has not changed"); - return; - } - } - // item.id did not match a 'faked' download new file creation - log.vlog("The file has been deleted locally"); - if (noRemoteDelete) { - // do not process remote file delete - log.vlog("Skipping remote file delete as --upload-only & --no-remote-delete configured"); - } else { - uploadDeleteItem(item, path); - } - } - } - } - } - - private JSONValue handleSharePointMetadataAdditionBug(const ref Item item, const(string) path) - { - // Explicit function for handling https://github.com/OneDrive/onedrive-api-docs/issues/935 - JSONValue response; - // Handle certain file types differently - if ((extension(path) == ".txt") || (extension(path) == ".csv")) { - // .txt and .csv are unaffected by https://github.com/OneDrive/onedrive-api-docs/issues/935 - // For logging consistency - writeln(""); - try { - response = session.upload(path, item.driveId, item.parentId, baseName(path), item.eTag); - } catch (OneDriveException e) { - if (e.httpStatusCode == 401) { - // OneDrive returned a 'HTTP/1.1 401 Unauthorized Error' - file failed to be uploaded - writeln("skipped."); - log.vlog("OneDrive returned a 'HTTP 401 - Unauthorized' - gracefully handling error"); - uploadFailed = true; - return response; - } - // Resolve https://github.com/abraunegg/onedrive/issues/36 - if ((e.httpStatusCode == 409) || (e.httpStatusCode == 423)) { - // The file is currently checked out or locked for editing by another user - // We cant upload this file at this time - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - writeln("", path, " is currently checked out or locked for editing by another user."); - log.fileOnly(path, " is currently checked out or locked for editing by another user."); - uploadFailed = true; - return response; - } - if (e.httpStatusCode == 412) { - // HTTP request returned status code 412 - ETag does not match current item's value - // Delete record from the local database - file will be uploaded as a new file - writeln("skipped."); - log.vdebug("Session Upload Replace Failed - OneDrive eTag / cTag match issue (Sharepoint Library)"); - log.vlog("OneDrive returned a 'HTTP 412 - Precondition Failed' - gracefully handling error. Will upload as new file."); - itemdb.deleteById(item.driveId, item.id); - uploadFailed = true; - return response; - } else { - // display what the error is - writeln("skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return response; - } - } catch (FileException e) { - // display the error message - writeln("skipped."); - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return response; - } - // upload done without error - writeln("done."); - } else { - // Due to https://github.com/OneDrive/onedrive-api-docs/issues/935 Microsoft modifies all PDF, MS Office & HTML files with added XML content. It is a 'feature' of SharePoint. - // This means, as a session upload, on 'completion' the file is 'moved' and generates a 404 ...... - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - log.vlog("Skip Reason: Microsoft Sharepoint 'enrichment' after upload issue"); - log.vlog("See: https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details"); - // Delete record from the local database - file will be uploaded as a new file - itemdb.deleteById(item.driveId, item.id); - uploadFailed = true; - return response; - } - - // return a JSON response so that it can be used and saved - return response; - } - - // upload new items to OneDrive - private void uploadNewItems(const(string) path) - { - static import std.utf; - import std.range : walkLength; - import std.uni : byGrapheme; - // https://support.microsoft.com/en-us/help/3125202/restrictions-and-limitations-when-you-sync-files-and-folders - // If the path is greater than allowed characters, then one drive will return a '400 - Bad Request' - // Need to ensure that the URI is encoded before the check is made: - // - 400 Character Limit for OneDrive Business / Office 365 - // - 430 Character Limit for OneDrive Personal - long maxPathLength = 0; - long pathWalkLength = 0; - - // Configure maxPathLength based on account type - if (accountType == "personal"){ - // Personal Account - maxPathLength = 430; - } else { - // Business Account / Office365 - maxPathLength = 400; - } - - // A short lived file that has disappeared will cause an error - is the path valid? - if (!exists(path)) { - log.log("Skipping item - path has disappeared: ", path); - return; - } - - // Calculate the path length by walking the path, catch any UTF-8 character errors - // https://github.com/abraunegg/onedrive/issues/487 - // https://github.com/abraunegg/onedrive/issues/1192 - try { - pathWalkLength = path.byGrapheme.walkLength; - } catch (std.utf.UTFException e) { - // path contains characters which generate a UTF exception - log.vlog("Skipping item - invalid UTF sequence: ", path); - log.vdebug(" Error Reason:", e.msg); - return; - } - - // check the std.encoding of the path - // https://github.com/skilion/onedrive/issues/57 - // https://github.com/abraunegg/onedrive/issues/487 - if(!isValid(path)) { - // Path is not valid according to https://dlang.org/phobos/std_encoding.html - log.vlog("Skipping item - invalid character encoding sequence: ", path); - return; - } - - // Is the path length is less than maxPathLength - if(pathWalkLength < maxPathLength){ - // skip dot files if configured - if (cfg.getValueBool("skip_dotfiles")) { - if (isDotFile(path)) { - log.vlog("Skipping item - .file or .folder: ", path); - return; - } - } - - // 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 a symbolic link - if (isSymlink(path)) { - // if config says so we skip all symlinked items - if (cfg.getValueBool("skip_symlinks")) { - log.vlog("Skipping item - skip symbolic links configured: ", path); - return; - - } - // skip unexisting symbolic links - else if (!exists(readLink(path))) { - // reading the symbolic link failed - is the link a relative symbolic link - // drwxrwxr-x. 2 alex alex 46 May 30 09:16 . - // drwxrwxr-x. 3 alex alex 35 May 30 09:14 .. - // lrwxrwxrwx. 1 alex alex 61 May 30 09:16 absolute.txt -> /home/alex/OneDrivePersonal/link_tests/intercambio/prueba.txt - // lrwxrwxrwx. 1 alex alex 13 May 30 09:16 relative.txt -> ../prueba.txt - // - // absolute links will be able to be read, but 'relative' links will fail, because they cannot be read based on the current working directory 'sync_dir' - string currentSyncDir = getcwd(); - string fullLinkPath = buildNormalizedPath(absolutePath(path)); - string fileName = baseName(fullLinkPath); - string parentLinkPath = dirName(fullLinkPath); - // test if this is a 'relative' symbolic link - chdir(parentLinkPath); - auto relativeLink = readLink(fileName); - auto relativeLinkTest = exists(readLink(fileName)); - // reset back to our 'sync_dir' - chdir(currentSyncDir); - // results - if (relativeLinkTest) { - log.vdebug("Not skipping item - symbolic link is a 'relative link' to target ('", relativeLink, "') which can be supported: ", path); - } else { - log.logAndNotify("Skipping item - invalid symbolic link: ", path); - return; - } - } - } - - // Check for bad whitespace items - if (!containsBadWhiteSpace(path)) { - log.logAndNotify("Skipping item - invalid name (Contains an invalid whitespace item): ", path); - return; - } - - // Check for HTML ASCII Codes as part of file name - if (!containsASCIIHTMLCodes(path)) { - log.logAndNotify("Skipping item - invalid name (Contains HTML ASCII Code): ", path); - return; - } - - // Is this item excluded by user configuration of skip_dir or skip_file? - if (path != ".") { - if (isDir(path)) { - log.vdebug("Checking local path: ", path); - // Only check path if config is != "" - if (cfg.getValueString("skip_dir") != "") { - // The path that needs to be checked needs to include the '/' - // This due to if the user has specified in skip_dir an exclusive path: '/path' - that is what must be matched - if (selectiveSync.isDirNameExcluded(path.strip('.'))) { - log.vlog("Skipping item - excluded by skip_dir config: ", path); - return; - } - } - - // In the event that this 'new item' is actually a OneDrive Business Shared Folder - // however the user may have omitted --sync-shared-folders, thus 'technically' this is a new item - // for this account OneDrive root, however this then would cause issues if --sync-shared-folders - // is added again after this sync - if ((exists(cfg.businessSharedFolderFilePath)) && (!syncBusinessFolders)){ - // business_shared_folders file exists, but we are not using / syncing them - // The file contents can only contain 'folder' names, so we need to strip './' from any path we are checking - if(selectiveSync.isSharedFolderMatched(strip(path,"./"))){ - // path detected as a 'new item' is matched as a path in business_shared_folders - log.vlog("Skipping item - excluded as included in business_shared_folders config: ", path); - log.vlog("To sync this directory to your OneDrive Account update your business_shared_folders config"); - return; - } - } - } - - if (isFile(path)) { - log.vdebug("Checking file: ", path); - // 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 (selectiveSync.isFileNameExcluded(path.strip('.'))) { - log.vlog("Skipping item - excluded by skip_file config: ", path); - return; - } - } - - // is sync_list configured - if (syncListConfigured) { - // sync_list configured and in use - if (selectiveSync.isPathExcludedViaSyncList(path)) { - if ((isFile(path)) && (cfg.getValueBool("sync_root_files")) && (rootName(path.strip('.').strip('/')) == "")) { - log.vdebug("Not skipping path due to sync_root_files inclusion: ", path); - } else { - string userSyncList = cfg.configDirName ~ "/sync_list"; - if (exists(userSyncList)){ - // skipped most likely due to inclusion in sync_list - log.vlog("Skipping item - excluded by sync_list config: ", path); - return; - } else { - // skipped for some other reason - log.vlog("Skipping item - path excluded by user config: ", path); - return; - } - } - } - } - } - - // Check against Microsoft OneDrive restriction and limitations about Windows naming files - if (!isValidName(path)) { - log.logAndNotify("Skipping item - invalid name (Microsoft Naming Convention): ", path); - return; - } - - // If we are in a --dry-run scenario, we may have renamed a folder - but it is technically not renamed locally - // Thus, that entire path may be attemtped to be uploaded as new data to OneDrive - if (dryRun) { - // check the pathsRenamed array for this path - // if any match - we need to exclude this path - foreach (thisRenamedPath; pathsRenamed) { - log.vdebug("Renamed Path to evaluate: ", thisRenamedPath); - // Can we find 'thisRenamedPath' in the given 'path' - if (canFind(path, thisRenamedPath)) { - log.vdebug("Renamed Path MATCH - DONT UPLOAD AS NEW"); - return; - } - } - } - - // We want to upload this new local data - if (isDir(path)) { - Item item; - bool pathFoundInDB = false; - foreach (driveId; driveIDsArray) { - if (itemdb.selectByPath(path, driveId, item)) { - pathFoundInDB = true; - } - } - - // Was the path found in the database? - if (!pathFoundInDB) { - // Path not found in database when searching all drive id's - if (!cleanupLocalFiles) { - // --download-only --cleanup-local-files not used - uploadCreateDir(path); - } else { - // we need to clean up this directory - log.log("Removing local directory as --download-only & --cleanup-local-files configured"); - // Remove any children of this path if they still exist - // Resolve 'Directory not empty' error when deleting local files - try { - foreach (DirEntry child; dirEntries(path, SpanMode.depth, false)) { - // what sort of child is this? - if (isDir(child.name)) { - log.log("Removing local directory: ", child.name); - } else { - log.log("Removing local file: ", child.name); - } - // are we in a --dry-run scenario? - if (!dryRun) { - // No --dry-run ... process local delete - try { - attrIsDir(child.linkAttributes) ? rmdir(child.name) : remove(child.name); - } catch (FileException e) { - // display the error message - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - } - } - } - // Remove the path now that it is empty of children - log.log("Removing local directory: ", path); - // are we in a --dry-run scenario? - if (!dryRun) { - // No --dry-run ... process local delete - try { - rmdirRecurse(path); - } catch (FileException e) { - // display the error message - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - } - } - } catch (FileException e) { - // display the error message - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - return; - } - } - } - - // recursively traverse children - // the above operation takes time and the directory might have - // disappeared in the meantime - if (!exists(path)) { - if (!cleanupLocalFiles) { - // --download-only --cleanup-local-files not used - log.vlog("Directory disappeared during upload: ", path); - } - return; - } - - // Try and access the directory and any path below - try { - auto entries = dirEntries(path, SpanMode.shallow, false); - foreach (DirEntry entry; entries) { - string thisPath = entry.name; - uploadNewItems(thisPath); - } - } catch (FileException e) { - // display the error message - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - return; - } - } else { - // path is not a directory, is it a valid file? - // pipes - whilst technically valid files, are not valid for this client - // prw-rw-r--. 1 user user 0 Jul 7 05:55 my_pipe - if (isFile(path)) { - // Path is a valid file - bool fileFoundInDB = false; - Item item; - - // Search the database for this file - foreach (driveId; driveIDsArray) { - if (itemdb.selectByPath(path, driveId, item)) { - fileFoundInDB = true; - } - } - - // Was the file found in the database? - if (!fileFoundInDB) { - // File not found in database when searching all drive id's - // Do we upload the file or clean up the file? - if (!cleanupLocalFiles) { - // --download-only --cleanup-local-files not used - uploadNewFile(path); - // Did the upload fail? - if (!uploadFailed) { - // Upload did not fail - // Issue #763 - Delete local files after sync handling - // are we in an --upload-only & --remove-source-files scenario? - if ((uploadOnly) && (localDeleteAfterUpload)) { - // Log that we are deleting a local item - log.log("Removing local file as --upload-only & --remove-source-files configured"); - // are we in a --dry-run scenario? - log.vdebug("Removing local file: ", path); - if (!dryRun) { - // No --dry-run ... process local file delete - safeRemove(path); - } - } - } - } else { - // we need to clean up this file - log.log("Removing local file as --download-only & --cleanup-local-files configured"); - // are we in a --dry-run scenario? - log.log("Removing local file: ", path); - if (!dryRun) { - // No --dry-run ... process local file delete - safeRemove(path); - } - } - } - } else { - // path is not a valid file - log.log("Skipping item - item is not a valid file: ", path); - } - } - } else { - // This path was skipped - why? - log.log("Skipping item '", path, "' due to the full path exceeding ", maxPathLength, " characters (Microsoft OneDrive limitation)"); - } - } - - // create new directory on OneDrive - private void uploadCreateDir(const(string) path) - { - log.vlog("OneDrive Client requested to create remote path: ", path); - - JSONValue onedrivePathDetails; - Item parent; - - // Was the path entered the root path? - if (path != "."){ - // What parent path to use? - string parentPath = dirName(path); // will be either . or something else - if (parentPath == "."){ - // Assume this is a new 'local' folder in the users configured sync_dir - // Use client defaults - parent.id = defaultRootId; // Should give something like 12345ABCDE1234A1!101 - parent.driveId = defaultDriveId; // Should give something like 12345abcde1234a1 - } else { - // Query the database using each of the driveId's we are using - foreach (driveId; driveIDsArray) { - // Query the database for this parent path using each driveId - Item dbResponse; - if(itemdb.selectByPathWithoutRemote(parentPath, driveId, dbResponse)){ - // parent path was found in the database - parent = dbResponse; - } - } - } - - // If this is still null or empty - we cant query the database properly later on - // Query OneDrive API for parent details - if ((parent.driveId == "") && (parent.id == "")){ - try { - log.vdebug("Attempting to query OneDrive for this parent path: ", parentPath); - onedrivePathDetails = onedrive.getPathDetails(parentPath); - } catch (OneDriveException e) { - log.vdebug("onedrivePathDetails = onedrive.getPathDetails(parentPath); generated a OneDriveException"); - // exception - set onedriveParentRootDetails to a blank valid JSON - onedrivePathDetails = parseJSON("{}"); - if (e.httpStatusCode == 404) { - // Parent does not exist ... need to create parent - log.vdebug("Parent path does not exist: ", parentPath); - uploadCreateDir(parentPath); - } - - 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 uploadCreateDir(path);"); - uploadCreateDir(path); - // return back to original call - return; - } - - if (e.httpStatusCode >= 500) { - // OneDrive returned a 'HTTP 5xx Server Side Error' - gracefully handling error - error message already logged - return; - } - } - - // configure the parent item data - if (hasId(onedrivePathDetails) && hasParentReference(onedrivePathDetails)){ - log.vdebug("Parent path found, configuring parent item"); - parent.id = onedrivePathDetails["id"].str; // This item's ID. Should give something like 12345ABCDE1234A1!101 - parent.driveId = onedrivePathDetails["parentReference"]["driveId"].str; // Should give something like 12345abcde1234a1 - } else { - // OneDrive API query failed - // Assume client defaults - log.vdebug("Parent path could not be queried, using OneDrive account defaults"); - parent.id = defaultRootId; // Should give something like 12345ABCDE1234A1!101 - parent.driveId = defaultDriveId; // Should give something like 12345abcde1234a1 - } - } - - JSONValue response; - // test if the path we are going to create already exists on OneDrive - try { - log.vdebug("Attempting to query OneDrive for this path: ", path); - response = onedrive.getPathDetailsByDriveId(parent.driveId, path); - } catch (OneDriveException e) { - log.vdebug("response = onedrive.getPathDetails(path); generated a OneDriveException"); - if (e.httpStatusCode == 404) { - // The directory was not found on the drive id we queried - log.vlog("The requested directory to create was not found on OneDrive - creating remote directory: ", path); - - if (!dryRun) { - // Perform the database lookup - is the parent in the database? - if (!itemdb.selectByPath(dirName(path), parent.driveId, parent)) { - // parent is not in the database - log.vdebug("Parent path is not in the database - need to add it: ", dirName(path)); - uploadCreateDir(dirName(path)); - } - - // Is the parent a 'folder' from another user? ie - is this a 'shared folder' that has been shared with us? - if (defaultDriveId == parent.driveId){ - // enforce check of parent path. if the above was triggered, the below will generate a sync retry and will now be sucessful - enforce(itemdb.selectByPath(dirName(path), parent.driveId, parent), "The parent item id is not in the database"); - } else { - log.vdebug("Parent drive ID is not our drive ID - parent most likely a shared folder"); - } - - JSONValue driveItem = [ - "name": JSONValue(baseName(path)), - "folder": parseJSON("{}") - ]; - - // Submit the creation request - // Fix for https://github.com/skilion/onedrive/issues/356 - try { - // Attempt to create a new folder on the configured parent driveId & parent id - response = onedrive.createById(parent.driveId, parent.id, driveItem); - } catch (OneDriveException e) { - if (e.httpStatusCode == 409) { - // OneDrive API returned a 404 (above) to say the directory did not exist - // but when we attempted to create it, OneDrive responded that it now already exists - log.vlog("OneDrive reported that ", path, " already exists .. OneDrive API race condition"); - return; - } else { - // some other error from OneDrive was returned - display what it is - log.error("OneDrive generated an error when creating this path: ", path); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } - // Is the response a valid JSON object - validation checking done in saveItem - saveItem(response); - } else { - // Simulate a successful 'directory create' & save it to the dryRun database copy - // The simulated response has to pass 'makeItem' as part of saveItem - auto fakeResponse = createFakeResponse(path); - saveItem(fakeResponse); - } - - log.vlog("Successfully created the remote directory ", path, " 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 uploadCreateDir(path);"); - uploadCreateDir(path); - // return back to original call - return; - } - - if (e.httpStatusCode >= 500) { - // OneDrive returned a 'HTTP 5xx Server Side Error' - gracefully handling error - error message already logged - return; - } - } - - // response from OneDrive has to be a valid JSON object - if (response.type() == JSONType.object){ - // https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file - // Do not assume case sensitivity. For example, consider the names OSCAR, Oscar, and oscar to be the same, - // even though some file systems (such as a POSIX-compliant file system) may consider them as different. - // Note that NTFS supports POSIX semantics for case sensitivity but this is not the default behavior. - - if (response["name"].str == baseName(path)){ - // OneDrive 'name' matches local path name - log.vlog("The requested directory to create was found on OneDrive - skipping creating the directory: ", path ); - // Check that this path is in the database - if (!itemdb.selectById(parent.driveId, parent.id, parent)){ - // parent for 'path' is NOT in the database - log.vlog("The parent for this path is not in the local database - need to add parent to local database"); - parentPath = dirName(path); - // add the parent into the database - uploadCreateDir(parentPath); - // save this child item into the database - log.vlog("The parent for this path has been added to the local database - adding requested path (", path ,") to database"); - if (!dryRun) { - // save the live data - saveItem(response); - } else { - // need to fake this data - auto fakeResponse = createFakeResponse(path); - saveItem(fakeResponse); - } - } 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 - 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(pathDetails); - - // OneDrive Personal Shared Folder edgecase handling - // In a: - // --resync --upload-only --single-directory 'dir' scenario, and where the root 'dir' for --single-directory is a 'shared folder' - // OR - // --resync --upload-only scenario, and where the root 'dir' to upload is a 'shared folder' - // - // We will not have the 'tie' DB entry created because of --upload-only because we do not download the folder structure from OneDrive - // to know what the remoteDriveId actually is - if (accountType == "personal"){ - // are we in a --resync --upload-only scenario ? - if ((cfg.getValueBool("resync")) && (cfg.getValueBool("upload_only"))) { - // Create a temp item - // Takes a JSON input and formats to an item which can be used by the database - Item tempItem = makeItem(pathDetails); - // New DB Tie item due to edge case - Item tieDBItem; - // Set the name - tieDBItem.name = tempItem.name; - // Set the correct item type - tieDBItem.type = ItemType.dir; - //parent.type = ItemType.remote; - if ((tempItem.type == ItemType.remote) && (!tempItem.remoteDriveId.empty)) { - // set the right elements - tieDBItem.driveId = tempItem.remoteDriveId; - tieDBItem.id = tempItem.remoteId; - // Set the correct mtime - tieDBItem.mtime = tempItem.mtime; - // Add tie DB record to the local database - log.vdebug("Adding tie DB record to database: ", tieDBItem); - itemdb.upsert(tieDBItem); - } - } - } - } else { - // need to fake this data - auto fakeResponse = createFakeResponse(path); - saveItem(fakeResponse); - } - } - } else { - // They are the "same" name wise but different in case sensitivity - log.error("ERROR: Current directory has a 'case-insensitive match' to an existing directory on OneDrive"); - log.error("ERROR: To resolve, rename this local directory: ", buildNormalizedPath(absolutePath(path))); - log.error("ERROR: Remote OneDrive directory: ", response["name"].str); - log.log("Skipping: ", buildNormalizedPath(absolutePath(path))); - return; - } - } else { - // response is not valid JSON, an error was returned from OneDrive - log.error("ERROR: There was an error performing this operation on OneDrive"); - log.error("ERROR: Increase logging verbosity to assist determining why."); - log.log("Skipping: ", buildNormalizedPath(absolutePath(path))); - return; - } - } - } - - // upload a new file to OneDrive - private void uploadNewFile(const(string) path) - { - // Reset upload failure - OneDrive or filesystem issue (reading data) - uploadFailed = false; - Item parent; - bool parentPathFoundInDB = false; - // Check the database for the parent path - // What parent path to use? - string parentPath = dirName(path); // will be either . or something else - if (parentPath == "."){ - // Assume this is a new file in the users configured sync_dir root - // Use client defaults - parent.id = defaultRootId; // Should give something like 12345ABCDE1234A1!101 - parent.driveId = defaultDriveId; // Should give something like 12345abcde1234a1 - parentPathFoundInDB = true; - } else { - // Query the database using each of the driveId's we are using - foreach (driveId; driveIDsArray) { - // Query the database for this parent path using each driveId - Item dbResponse; - if(itemdb.selectByPath(parentPath, driveId, dbResponse)){ - // parent path was found in the database - parent = dbResponse; - parentPathFoundInDB = true; - } - } - } - - // Get the file size - long thisFileSize = getSize(path); - // Can we upload this file - is there enough free space? - https://github.com/skilion/onedrive/issues/73 - // We can only use 'remainingFreeSpace' if we are uploading to our driveId ... if this is a shared folder, we have no visibility of space available, as quota details are not provided by the OneDrive API - if (parent.driveId == defaultDriveId) { - // the file will be uploaded to my driveId - log.vdebug("File upload destination is users default driveId .."); - // are quota details being restricted? - if (!quotaRestricted) { - // quota is not being restricted - we can track drive space allocation to determine if it is possible to upload the file - if ((remainingFreeSpace - thisFileSize) < 0) { - // no space to upload file, based on tracking of quota values - quotaAvailable = false; - } else { - // there is free space to upload file, based on tracking of quota values - quotaAvailable = true; - } - } else { - // set quotaAvailable as true, even though we have zero way to validate that this is correct or not - quotaAvailable = true; - } - } else { - // the file will be uploaded to a shared folder - // we can't track if there is enough free space to upload the file - log.vdebug("File upload destination is a shared folder - the upload may fail if not enough space on OneDrive .."); - // set quotaAvailable as true, even though we have zero way to validate that this is correct or not - quotaAvailable = true; - } - - // If performing a dry-run or parentPath is found in the database & there is quota available to upload file - if ((dryRun) || (parentPathFoundInDB && quotaAvailable)) { - // Maximum file size upload - // https://support.microsoft.com/en-us/office/invalid-file-names-and-file-types-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa?ui=en-us&rs=en-us&ad=us - // July 2020, maximum file size for all accounts is 100GB - // January 2021, maximum file size for all accounts is 250GB - auto maxUploadFileSize = 268435456000; // 250GB - - // Can we read the file - as a permissions issue or file corruption will cause a failure - // https://github.com/abraunegg/onedrive/issues/113 - if (readLocalFile(path)){ - // we are able to read the file - // To avoid a 409 Conflict error - does the file actually exist on OneDrive already? - JSONValue fileDetailsFromOneDrive; - if (thisFileSize <= maxUploadFileSize){ - // Resolves: https://github.com/skilion/onedrive/issues/121, https://github.com/skilion/onedrive/issues/294, https://github.com/skilion/onedrive/issues/329 - // Does this 'file' already exist on OneDrive? - try { - // test if the local path exists on OneDrive - // if parent.driveId is invalid, then API call will generate a 'HTTP 400 - Bad Request' - make sure we at least have a valid parent.driveId - if (!parent.driveId.empty) { - // use configured value for parent.driveId - fileDetailsFromOneDrive = onedrive.getPathDetailsByDriveId(parent.driveId, path); - } else { - // switch to using defaultDriveId - log.vdebug("parent.driveId is empty - using defaultDriveId for API call"); - fileDetailsFromOneDrive = onedrive.getPathDetailsByDriveId(defaultDriveId, path); - } - } catch (OneDriveException e) { - // log that we generated an exception - log.vdebug("fileDetailsFromOneDrive = onedrive.getPathDetailsByDriveId(parent.driveId, path); generated a OneDriveException"); - // OneDrive returned a 'HTTP/1.1 400 Bad Request' - // If the 'path', when encoded, cannot be interpreted by the OneDrive API, the API will generate a 400 error - if (e.httpStatusCode == 400) { - log.log("Skipping uploading this new file: ", buildNormalizedPath(absolutePath(path))); - log.vlog("Skipping item - OneDrive returned a 'HTTP 400 - Bad Request' when attempting to query if file exists"); - log.error("ERROR: To resolve, rename this local file: ", buildNormalizedPath(absolutePath(path))); - uploadFailed = true; - return; - } - // OneDrive returned a 'HTTP/1.1 401 Unauthorized Error' - if (e.httpStatusCode == 401) { - log.vlog("Skipping item - OneDrive returned a 'HTTP 401 - Unauthorized' when attempting to query if file exists"); - uploadFailed = true; - return; - } - // A 404 is the expected response if the file was not present - if (e.httpStatusCode == 404) { - // The file was not found on OneDrive, need to upload it - // Check if file should be skipped based on skip_size config - if (thisFileSize >= this.newSizeLimit) { - log.vlog("Skipping item - excluded by skip_size config: ", path, " (", thisFileSize/2^^20," MB)"); - return; - } - - // start of upload file - write("Uploading new file ", path, " ... "); - JSONValue response; - - // Calculate upload speed - auto uploadStartTime = Clock.currTime(); - - if (!dryRun) { - // Resolve https://github.com/abraunegg/onedrive/issues/37 - if (thisFileSize == 0){ - // We can only upload zero size files via simpleFileUpload regardless of account type - // https://github.com/OneDrive/onedrive-api-docs/issues/53 - try { - response = onedrive.simpleUpload(path, parent.driveId, parent.id, baseName(path)); - } catch (OneDriveException e) { - // error uploading file - if (e.httpStatusCode == 401) { - // OneDrive returned a 'HTTP/1.1 401 Unauthorized Error' - file failed to be uploaded - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - log.vlog("OneDrive returned a 'HTTP 401 - Unauthorized' - gracefully handling error"); - uploadFailed = true; - 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 - uploadNewFile(path); - // return back to original call - return; - } - if (e.httpStatusCode == 504) { - // HTTP request returned status code 504 (Gateway Timeout) - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' - retrying upload request"); - // Retry original request by calling function again to avoid replicating any further error handling - uploadNewFile(path); - // return back to original call - return; - } else { - // display what the error is - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } catch (FileException e) { - // display the error message - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } else { - // File is not a zero byte file - // Are we using OneDrive Personal or OneDrive Business? - // To solve 'Multiple versions of file shown on website after single upload' (https://github.com/abraunegg/onedrive/issues/2) - // check what 'account type' this is as this issue only affects OneDrive Business so we need some extra logic here - if (accountType == "personal"){ - // Original file upload logic - if (thisFileSize <= thresholdFileSize) { - try { - response = onedrive.simpleUpload(path, parent.driveId, parent.id, baseName(path)); - } catch (OneDriveException e) { - if (e.httpStatusCode == 401) { - // OneDrive returned a 'HTTP/1.1 401 Unauthorized Error' - file failed to be uploaded - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - log.vlog("OneDrive returned a 'HTTP 401 - Unauthorized' - gracefully handling error"); - uploadFailed = true; - 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 - uploadNewFile(path); - // return back to original call - return; - } - - if (e.httpStatusCode == 504) { - // HTTP request returned status code 504 (Gateway Timeout) - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' - retrying upload request as a session"); - // Try upload as a session - try { - response = session.upload(path, parent.driveId, parent.id, baseName(path)); - } catch (OneDriveException e) { - // error uploading file - 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 - uploadNewFile(path); - // return back to original call - return; - } else { - // display what the error is - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } - } else { - // display what the error is - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } catch (FileException e) { - // display the error message - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } else { - // File larger than threshold - use a session to upload - writeln(""); - try { - response = session.upload(path, parent.driveId, parent.id, baseName(path)); - } catch (OneDriveException e) { - if (e.httpStatusCode == 401) { - // OneDrive returned a 'HTTP/1.1 401 Unauthorized Error' - file failed to be uploaded - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - log.vlog("OneDrive returned a 'HTTP 401 - Unauthorized' - gracefully handling error"); - uploadFailed = true; - 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 - uploadNewFile(path); - // return back to original call - return; - } - if (e.httpStatusCode == 504) { - // HTTP request returned status code 504 (Gateway Timeout) - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' - retrying upload request"); - // Retry original request by calling function again to avoid replicating any further error handling - uploadNewFile(path); - // return back to original call - return; - } else { - // display what the error is - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } catch (FileException e) { - // display the error message - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } - } else { - // OneDrive Business Account - always use a session to upload - writeln(""); - try { - response = session.upload(path, parent.driveId, parent.id, baseName(path)); - } catch (OneDriveException e) { - if (e.httpStatusCode == 401) { - // OneDrive returned a 'HTTP/1.1 401 Unauthorized Error' - file failed to be uploaded - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - log.vlog("OneDrive returned a 'HTTP 401 - Unauthorized' - gracefully handling error"); - uploadFailed = true; - 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 - uploadNewFile(path); - // return back to original call - return; - } - if (e.httpStatusCode == 504) { - // HTTP request returned status code 504 (Gateway Timeout) - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' - retrying upload request"); - // Retry original request by calling function again to avoid replicating any further error handling - uploadNewFile(path); - // return back to original call - return; - } else { - // display what the error is - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } catch (FileException e) { - // display the error message - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } - } - - // response from OneDrive has to be a valid JSON object - if (response.type() == JSONType.object){ - // upload done without error - writeln("done."); - - // upload finished - auto uploadFinishTime = Clock.currTime(); - auto uploadDuration = uploadFinishTime - uploadStartTime; - log.vdebug("File Size: ", thisFileSize, " Bytes"); - log.vdebug("Upload Duration: ", (uploadDuration.total!"msecs"/1e3), " Seconds"); - auto uploadSpeed = (thisFileSize / (uploadDuration.total!"msecs"/1e3)/ 1024 / 1024); - log.vdebug("Upload Speed: ", uploadSpeed, " Mbps (approx)"); - - // Log upload action to log file - log.fileOnly("Uploading new file ", path, " ... done."); - // The file was uploaded, or a 4xx / 5xx error was generated - if ("size" in response){ - // The response JSON contains size, high likelihood valid response returned - ulong uploadFileSize = response["size"].integer; - - // In some cases the file that was uploaded was not complete, but 'completed' without errors on OneDrive - // This has been seen with PNG / JPG files mainly, which then contributes to generating a 412 error when we attempt to update the metadata - // Validate here that the file uploaded, at least in size, matches in the response to what the size is on disk - if (thisFileSize != uploadFileSize){ - // Upload size did not match local size - // There are 2 scenarios where this happens: - // 1. Failed Transfer - // 2. Upload file is going to a SharePoint Site, where Microsoft enriches the file with additional metadata with no way to disable - // For this client: - // - If a SharePoint Library, disableUploadValidation gets flagged as True - // - If we are syncing a business shared folder, this folder could reside on a Users Path (there should be no upload issue) or SharePoint (upload issue) - if ((disableUploadValidation)|| (syncBusinessFolders && (parent.driveId != defaultDriveId))){ - // Print a warning message - should only be triggered if: - // - disableUploadValidation gets flagged (documentLibrary account type) - // - syncBusinessFolders is being used & parent.driveId != defaultDriveId - log.log("WARNING: Uploaded file size does not match local file - skipping upload validation"); - log.vlog("WARNING: Due to Microsoft Sharepoint 'enrichment' of files, this file is now technically different to your local copy"); - log.vlog("See: https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details"); - } else { - // OK .. the uploaded file does not match and we did not disable this validation - log.log("Uploaded file size does not match local file - upload failure - retrying"); - // Delete uploaded bad file - onedrive.deleteById(response["parentReference"]["driveId"].str, response["id"].str, response["eTag"].str); - // Re-upload - uploadNewFile(path); - return; - } - } - - // File validation is OK - if ((accountType == "personal") || (thisFileSize == 0)){ - // Update the item's metadata on OneDrive - string id = response["id"].str; - string cTag; - - // Is there a valid cTag in the response? - if ("cTag" in response) { - // use the cTag instead of the eTag because OneDrive may update the metadata of files AFTER they have been uploaded - cTag = response["cTag"].str; - } else { - // Is there an eTag in the response? - if ("eTag" in response) { - // use the eTag from the response as there was no cTag - cTag = response["eTag"].str; - } else { - // no tag available - set to nothing - cTag = ""; - } - } - // check if the path exists locally before we try to set the file times - if (exists(path)) { - SysTime mtime = timeLastModified(path).toUTC(); - // update the file modified time on OneDrive and save item details to database - uploadLastModifiedTime(parent.driveId, id, cTag, mtime); - } else { - // will be removed in different event! - log.log("File disappeared after upload: ", path); - } - } else { - // OneDrive Business Account - always use a session to upload - // The session includes a Request Body element containing lastModifiedDateTime - // which negates the need for a modify event against OneDrive - // Is the response a valid JSON object - validation checking done in saveItem - saveItem(response); - } - } - - // update free space tracking if this is our drive id - if (parent.driveId == defaultDriveId) { - // how much space is left on OneDrive after upload? - remainingFreeSpace = (remainingFreeSpace - thisFileSize); - log.vlog("Remaining free space on OneDrive: ", remainingFreeSpace); - } - // File uploaded successfully, space details updated if required - return; - } else { - // response is not valid JSON, an error was returned from OneDrive - log.fileOnly("Uploading new file ", path, " ... error"); - uploadFailed = true; - return; - } - } else { - // we are --dry-run - simulate the file upload - writeln("done."); - response = createFakeResponse(path); - // Log action to log file - log.fileOnly("Uploading new file ", path, " ... done."); - // Is the response a valid JSON object - validation checking done in saveItem - saveItem(response); - return; - } - } - // OneDrive returned a '429 - Too Many Requests' - 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 uploadNewFile(path);"); - uploadNewFile(path); - // return back to original call - return; - } - // OneDrive returned a 'HTTP 5xx Server Side Error' - gracefully handling error - error message already logged - if (e.httpStatusCode >= 500) { - uploadFailed = true; - return; - } - } - - // Check that the filename that is returned is actually the file we wish to upload - // https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file - // Do not assume case sensitivity. For example, consider the names OSCAR, Oscar, and oscar to be the same, - // even though some file systems (such as a POSIX-compliant file system) may consider them as different. - // Note that NTFS supports POSIX semantics for case sensitivity but this is not the default behavior. - - // fileDetailsFromOneDrive has to be a valid object - if (fileDetailsFromOneDrive.type() == JSONType.object){ - // fileDetailsFromOneDrive = onedrive.getPathDetails(path) returned a valid JSON, meaning the file exists on OneDrive - // Check that 'name' is in the JSON response (validates data) and that 'name' == the path we are looking for - if (("name" in fileDetailsFromOneDrive) && (fileDetailsFromOneDrive["name"].str == baseName(path))) { - // OneDrive 'name' matches local path name - log.vlog("Requested file to upload exists on OneDrive - local database is out of sync for this file: ", path); - - // Is the local file newer than the uploaded file? - SysTime localFileModifiedTime = timeLastModified(path).toUTC(); - SysTime remoteFileModifiedTime = SysTime.fromISOExtString(fileDetailsFromOneDrive["fileSystemInfo"]["lastModifiedDateTime"].str); - localFileModifiedTime.fracSecs = Duration.zero; - - if (localFileModifiedTime > remoteFileModifiedTime){ - // local file is newer - log.vlog("Requested file to upload is newer than existing file on OneDrive"); - write("Uploading modified file ", path, " ... "); - JSONValue response; - - if (!dryRun) { - if (accountType == "personal"){ - // OneDrive Personal account upload handling - if (thisFileSize <= thresholdFileSize) { - try { - response = onedrive.simpleUpload(path, parent.driveId, parent.id, baseName(path)); - writeln("done."); - } catch (OneDriveException e) { - log.vdebug("response = onedrive.simpleUpload(path, parent.driveId, parent.id, baseName(path)); generated a OneDriveException"); - if (e.httpStatusCode == 401) { - // OneDrive returned a 'HTTP/1.1 401 Unauthorized Error' - file failed to be uploaded - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - log.vlog("OneDrive returned a 'HTTP 401 - Unauthorized' - gracefully handling error"); - uploadFailed = true; - 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 uploadNewFile(path);"); - uploadNewFile(path); - // return back to original call - return; - } - - if (e.httpStatusCode == 504) { - // HTTP request returned status code 504 (Gateway Timeout) - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' - retrying upload request as a session"); - // Try upload as a session - try { - response = session.upload(path, parent.driveId, parent.id, baseName(path)); - writeln("done."); - } catch (OneDriveException e) { - 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 - uploadNewFile(path); - // return back to original call - return; - } else { - // error uploading file - // display what the error is - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } - } else { - // display what the error is - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } catch (FileException e) { - // display the error message - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } else { - // File larger than threshold - use a session to upload - writeln(""); - try { - response = session.upload(path, parent.driveId, parent.id, baseName(path)); - writeln("done."); - } catch (OneDriveException e) { - log.vdebug("response = session.upload(path, parent.driveId, parent.id, baseName(path)); generated a OneDriveException"); - if (e.httpStatusCode == 401) { - // OneDrive returned a 'HTTP/1.1 401 Unauthorized Error' - file failed to be uploaded - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - log.vlog("OneDrive returned a 'HTTP 401 - Unauthorized' - gracefully handling error"); - uploadFailed = true; - 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 uploadNewFile(path);"); - uploadNewFile(path); - // return back to original call - return; - } - if (e.httpStatusCode == 504) { - // HTTP request returned status code 504 (Gateway Timeout) - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' - retrying upload request"); - // Retry original request by calling function again to avoid replicating any further error handling - uploadNewFile(path); - // return back to original call - return; - } else { - // error uploading file - // display what the error is - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } catch (FileException e) { - // display the error message - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } - - // response from OneDrive has to be a valid JSON object - if (response.type() == JSONType.object){ - // response is a valid JSON object - string id = response["id"].str; - string cTag; - - // Is there a valid cTag in the response? - if ("cTag" in response) { - // use the cTag instead of the eTag because Onedrive may update the metadata of files AFTER they have been uploaded - cTag = response["cTag"].str; - } else { - // Is there an eTag in the response? - if ("eTag" in response) { - // use the eTag from the response as there was no cTag - cTag = response["eTag"].str; - } else { - // no tag available - set to nothing - cTag = ""; - } - } - // validate if path exists so mtime can be calculated - if (exists(path)) { - SysTime mtime = timeLastModified(path).toUTC(); - uploadLastModifiedTime(parent.driveId, id, cTag, mtime); - } else { - // will be removed in different event! - log.log("File disappeared after upload: ", path); - } - } else { - // Log that an invalid JSON object was returned - log.vdebug("onedrive.simpleUpload or session.upload call returned an invalid JSON Object"); - return; - } - } else { - // OneDrive Business account modified file upload handling - if (accountType == "business"){ - // OneDrive Business Account - if ((!syncBusinessFolders) || (parent.driveId == defaultDriveId)) { - // If we are not syncing Shared Business Folders, or this change is going to the 'users' default drive, handle normally - // For logging consistency - writeln(""); - try { - response = session.upload(path, parent.driveId, parent.id, baseName(path), fileDetailsFromOneDrive["eTag"].str); - } catch (OneDriveException e) { - log.vdebug("response = session.upload(path, parent.driveId, parent.id, baseName(path), fileDetailsFromOneDrive['eTag'].str); generated a OneDriveException"); - if (e.httpStatusCode == 401) { - // OneDrive returned a 'HTTP/1.1 401 Unauthorized Error' - file failed to be uploaded - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - log.vlog("OneDrive returned a 'HTTP 401 - Unauthorized' - gracefully handling error"); - uploadFailed = true; - 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 uploadNewFile(path);"); - uploadNewFile(path); - // return back to original call - return; - } - if (e.httpStatusCode == 504) { - // HTTP request returned status code 504 (Gateway Timeout) - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' - retrying upload request"); - // Retry original request by calling function again to avoid replicating any further error handling - uploadNewFile(path); - // return back to original call - return; - } else { - // error uploading file - // display what the error is - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - } catch (FileException e) { - // display the error message - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return; - } - // upload complete - writeln("done."); - saveItem(response); - } else { - // If we are uploading to a shared business folder, there are a couple of corner cases here: - // 1. Shared Folder is a 'users' folder - // 2. Shared Folder is a 'SharePoint Library' folder, meaning we get hit by this stupidity: https://github.com/OneDrive/onedrive-api-docs/issues/935 - - // Need try{} & catch (OneDriveException e) { & catch (FileException e) { handler for this query - response = handleSharePointMetadataAdditionBugReplaceFile(fileDetailsFromOneDrive, parent, path); - if (!uploadFailed){ - // Is the response a valid JSON object - validation checking done in saveItem - saveItem(response); - } else { - // uploadFailed, return - return; - } - } - } - - // OneDrive SharePoint account modified file upload handling - if (accountType == "documentLibrary"){ - // Depending on the file size, this will depend on how best to handle the modified local file - // as if too large, the following error will be generated by OneDrive: - // HTTP request returned status code 413 (Request Entity Too Large) - // We also cant use a session to upload the file, we have to use simpleUploadReplace - - // Need try{} & catch (OneDriveException e) { & catch (FileException e) { handler for this query - response = handleSharePointMetadataAdditionBugReplaceFile(fileDetailsFromOneDrive, parent, path); - if (!uploadFailed){ - // Is the response a valid JSON object - validation checking done in saveItem - saveItem(response); - } else { - // uploadFailed, return - return; - } - } - } - - // Log action to log file - log.fileOnly("Uploading modified file ", path, " ... done."); - - // update free space tracking if this is our drive id - if (parent.driveId == defaultDriveId) { - // how much space is left on OneDrive after upload? - remainingFreeSpace = (remainingFreeSpace - thisFileSize); - log.vlog("Remaining free space on OneDrive: ", remainingFreeSpace); - } - } else { - // we are --dry-run - simulate the file upload - writeln("done."); - response = createFakeResponse(path); - // Log action to log file - log.fileOnly("Uploading modified file ", path, " ... done."); - // Is the response a valid JSON object - validation checking done in saveItem - saveItem(response); - return; - } - } else { - // Save the details of the file that we got from OneDrive - // --dry-run safe - log.vlog("Updating the local database with details for this file: ", path); - if (!dryRun) { - // use the live data - saveItem(fileDetailsFromOneDrive); - } else { - // need to fake this data - auto fakeResponse = createFakeResponse(path); - saveItem(fakeResponse); - } - } - } else { - // The files are the "same" name wise but different in case sensitivity - log.error("ERROR: A local file has the same name as another local file."); - log.error("ERROR: To resolve, rename this local file: ", buildNormalizedPath(absolutePath(path))); - log.log("Skipping uploading this new file: ", buildNormalizedPath(absolutePath(path))); - } - } else { - // fileDetailsFromOneDrive is not valid JSON, an error was returned from OneDrive - log.error("ERROR: An error was returned from OneDrive and the resulting response is not a valid JSON object"); - log.error("ERROR: Increase logging verbosity to assist determining why."); - uploadFailed = true; - return; - } - } else { - // Skip file - too large - log.log("Skipping uploading this new file as it exceeds the maximum size allowed by OneDrive: ", path); - uploadFailed = true; - return; - } - } else { - // unable to read local file - log.log("Skipping uploading this file as it cannot be read (file permissions or file corruption): ", path); - } - } else { - // Upload of the new file did not occur .. why? - if (!parentPathFoundInDB) { - // Parent path was not found - log.log("Skipping uploading this new file as parent path is not in the database: ", path); - uploadFailed = true; - return; - } - if (!quotaAvailable) { - // Not enough free space - log.log("Skipping item '", path, "' due to insufficient free space available on OneDrive"); - uploadFailed = true; - return; - } - } - } - - private JSONValue handleSharePointMetadataAdditionBugReplaceFile(JSONValue fileDetailsFromOneDrive, const ref Item parent, const(string) path) - { - // Explicit function for handling https://github.com/OneDrive/onedrive-api-docs/issues/935 - // Replace existing file - JSONValue response; - - // Depending on the file size, this will depend on how best to handle the modified local file - // as if too large, the following error will be generated by OneDrive: - // HTTP request returned status code 413 (Request Entity Too Large) - // We also cant use a session to upload the file, we have to use simpleUploadReplace - - // Calculate existing hash for this file - string existingFileHash = computeQuickXorHash(path); - - if (getSize(path) <= thresholdFileSize) { - // Upload file via simpleUploadReplace as below threshold size - try { - response = onedrive.simpleUploadReplace(path, fileDetailsFromOneDrive["parentReference"]["driveId"].str, fileDetailsFromOneDrive["id"].str, fileDetailsFromOneDrive["eTag"].str); - } catch (OneDriveException e) { - if (e.httpStatusCode == 401) { - // OneDrive returned a 'HTTP/1.1 401 Unauthorized Error' - file failed to be uploaded - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - log.vlog("OneDrive returned a 'HTTP 401 - Unauthorized' - gracefully handling error"); - uploadFailed = true; - return response; - } else { - // display what the error is - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return response; - } - } catch (FileException e) { - // display the error message - writeln("skipped."); - log.fileOnly("Uploading modified file ", path, " ... skipped."); - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return response; - } - } else { - // Have to upload via a session, however we have to delete the file first otherwise this will generate a 404 error post session upload - // Remove the existing file - onedrive.deleteById(fileDetailsFromOneDrive["parentReference"]["driveId"].str, fileDetailsFromOneDrive["id"].str, fileDetailsFromOneDrive["eTag"].str); - // Upload as a session, as a new file - writeln(""); - try { - response = session.upload(path, parent.driveId, parent.id, baseName(path)); - } catch (OneDriveException e) { - if (e.httpStatusCode == 401) { - // OneDrive returned a 'HTTP/1.1 401 Unauthorized Error' - file failed to be uploaded - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - log.vlog("OneDrive returned a 'HTTP 401 - Unauthorized' - gracefully handling error"); - uploadFailed = true; - return response; - } else { - // display what the error is - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return response; - } - } catch (FileException e) { - // display the error message - writeln("skipped."); - log.fileOnly("Uploading new file ", path, " ... skipped."); - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - uploadFailed = true; - return response; - } - } - writeln("done."); - // Due to https://github.com/OneDrive/onedrive-api-docs/issues/935 Microsoft modifies all PDF, MS Office & HTML files with added XML content. It is a 'feature' of SharePoint. - // So - now the 'local' and 'remote' file is technically DIFFERENT ... thanks Microsoft .. NO way to disable this stupidity - string uploadNewFileHash; - if (hasQuickXorHash(response)) { - // use the response json hash detail to compare - uploadNewFileHash = response["file"]["hashes"]["quickXorHash"].str; - } - - if (existingFileHash != uploadNewFileHash) { - // file was modified by Microsoft post upload to SharePoint site - log.vdebug("Existing Local File Hash: ", existingFileHash); - log.vdebug("New Remote File Hash: ", uploadNewFileHash); - - if(!uploadOnly){ - // Download the Microsoft 'modified' file so 'local' is now in sync - log.vlog("Due to Microsoft Sharepoint 'enrichment' of files, downloading 'enriched' file to ensure local file is in-sync"); - log.vlog("See: https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details"); - auto fileSize = response["size"].integer; - onedrive.downloadById(response["parentReference"]["driveId"].str, response["id"].str, path, fileSize); - } else { - // we are not downloading a file, warn that file differences will exist - log.vlog("WARNING: Due to Microsoft Sharepoint 'enrichment' of files, this file is now technically different to your local copy"); - log.vlog("See: https://github.com/OneDrive/onedrive-api-docs/issues/935 for further details"); - } - } - - // return a JSON response so that it can be used and saved - return response; - } - - // delete an item on OneDrive - private void uploadDeleteItem(Item item, const(string) path) - { - log.log("Deleting item from OneDrive: ", path); - bool flagAsBigDelete = false; - - // query the database - how many objects will this remove? - auto children = getChildren(item.driveId, item.id); - long itemsToDelete = count(children); - log.vdebug("Number of items to delete: ", itemsToDelete); - - // Are we running in monitor mode? A local delete of a file will issue a inotify event, which will trigger the local & remote data immediately - if (!cfg.getValueBool("monitor")) { - // not running in monitor mode - if (itemsToDelete > cfg.getValueLong("classify_as_big_delete")) { - // A big delete detected - flagAsBigDelete = true; - if (!cfg.getValueBool("force")) { - log.error("ERROR: An attempt to remove a large volume of data from OneDrive has been detected. Exiting client to preserve data on OneDrive"); - log.error("ERROR: To delete a large volume of data use --force or increase the config value 'classify_as_big_delete' to a larger value"); - // Must exit here to preserve data on OneDrive - onedrive.shutdown(); - exit(-1); - } - } - } - - if (!dryRun) { - // we are not in a --dry-run situation, process deletion to OneDrive - if ((item.driveId == "") && (item.id == "") && (item.eTag == "")){ - // These are empty ... we cannot delete if this is empty .... - log.vdebug("item.driveId, item.id & item.eTag are empty ... need to query OneDrive for values"); - log.vdebug("Checking OneDrive for path: ", path); - JSONValue onedrivePathDetails = onedrive.getPathDetails(path); // Returns a JSON String for the OneDrive Path - log.vdebug("OneDrive path details: ", onedrivePathDetails); - item.driveId = onedrivePathDetails["parentReference"]["driveId"].str; // Should give something like 12345abcde1234a1 - item.id = onedrivePathDetails["id"].str; // This item's ID. Should give something like 12345ABCDE1234A1!101 - item.eTag = onedrivePathDetails["eTag"].str; // Should be something like aNjM2NjJFRUVGQjY2NjJFMSE5MzUuMA - } - - // do the delete - try { - // what item are we trying to delete? - log.vdebug("Attempting to delete item from drive: ", item.driveId); - log.vdebug("Attempting to delete this item id: ", item.id); - // perform the delete via the API - onedrive.deleteById(item.driveId, item.id, item.eTag); - } catch (OneDriveException e) { - if (e.httpStatusCode == 404) { - // item.id, item.eTag could not be found on driveId - log.vlog("OneDrive reported: The resource could not be found."); - } else { - // Not a 404 response .. is this a 401 response due to some sort of OneDrive Business security policy? - if ((e.httpStatusCode == 401) && (accountType != "personal")) { - log.vdebug("onedrive.deleteById generated a 401 error response when attempting to delete object by item id"); - auto errorArray = splitLines(e.msg); - JSONValue errorMessage = parseJSON(replace(e.msg, errorArray[0], "")); - if (errorMessage["error"]["message"].str == "Access denied. You do not have permission to perform this action or access this resource.") { - // Issue #1041 - Unable to delete OneDrive content when permissions prevent deletion - try { - log.vdebug("Attempting a reverse delete of all child objects from OneDrive"); - foreach_reverse (Item child; children) { - log.vdebug("Delete child item from drive: ", child.driveId); - log.vdebug("Delete this child item id: ", child.id); - onedrive.deleteById(child.driveId, child.id, child.eTag); - // delete the child reference in the local database - itemdb.deleteById(child.driveId, child.id); - } - log.vdebug("Delete parent item from drive: ", item.driveId); - log.vdebug("Delete this parent item id: ", item.id); - onedrive.deleteById(item.driveId, item.id, item.eTag); - } catch (OneDriveException e) { - // display what the error is - log.vdebug("A further error was generated when attempting a reverse delete of objects from OneDrive"); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } - } - - // Not a 404 response .. is this a 403 response due to OneDrive Business Retention Policy being enabled? - if ((e.httpStatusCode == 403) && (accountType != "personal")) { - log.vdebug("onedrive.deleteById generated a 403 error response when attempting to delete object by item id"); - auto errorArray = splitLines(e.msg); - JSONValue errorMessage = parseJSON(replace(e.msg, errorArray[0], "")); - if (errorMessage["error"]["message"].str == "Request was cancelled by event received. If attempting to delete a non-empty folder, it's possible that it's on hold") { - // Issue #338 - Unable to delete OneDrive content when OneDrive Business Retention Policy is enabled - try { - log.vdebug("Attempting a reverse delete of all child objects from OneDrive"); - foreach_reverse (Item child; children) { - log.vdebug("Delete child item from drive: ", child.driveId); - log.vdebug("Delete this child item id: ", child.id); - onedrive.deleteById(child.driveId, child.id, child.eTag); - // delete the child reference in the local database - itemdb.deleteById(child.driveId, child.id); - } - log.vdebug("Delete parent item from drive: ", item.driveId); - log.vdebug("Delete this parent item id: ", item.id); - onedrive.deleteById(item.driveId, item.id, item.eTag); - } catch (OneDriveException e) { - // display what the error is - log.vdebug("A further error was generated when attempting a reverse delete of objects from OneDrive"); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } - } else { - // Not a 403 response & OneDrive Business Account / O365 Shared Folder / Library - log.vdebug("onedrive.deleteById generated an error response when attempting to delete object by item id"); - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } - } - - // delete the reference in the local database - itemdb.deleteById(item.driveId, item.id); - if (item.remoteId != null) { - // If the item is a remote item, delete the reference in the local database - itemdb.deleteById(item.remoteDriveId, item.remoteId); - } - } - } - - // get the children of an item id from the database - private Item[] getChildren(string driveId, string id) - { - Item[] children; - children ~= itemdb.selectChildren(driveId, id); - foreach (Item child; children) { - if (child.type != ItemType.file) { - // recursively get the children of this child - children ~= getChildren(child.driveId, child.id); - } - } - return children; - } - - // update the item's last modified time - private void uploadLastModifiedTime(const(char)[] driveId, const(char)[] id, const(char)[] eTag, SysTime mtime) - { - string itemModifiedTime; - itemModifiedTime = mtime.toISOExtString(); - JSONValue data = [ - "fileSystemInfo": JSONValue([ - "lastModifiedDateTime": itemModifiedTime - ]) - ]; - - JSONValue response; - try { - response = onedrive.updateById(driveId, id, data, eTag); - } catch (OneDriveException e) { - if (e.httpStatusCode == 412) { - // OneDrive threw a 412 error, most likely: ETag does not match current item's value - // Retry without eTag - log.vdebug("File Metadata Update Failed - OneDrive eTag / cTag match issue"); - log.vlog("OneDrive returned a 'HTTP 412 - Precondition Failed' when attempting file time stamp update - gracefully handling error"); - string nullTag = null; - response = onedrive.updateById(driveId, id, data, nullTag); - } - } - // save the updated response from OneDrive in the database - // Is the response a valid JSON object - validation checking done in saveItem - saveItem(response); - } - - // save item details into database - private void saveItem(JSONValue jsonItem) - { - // jsonItem has to be a valid object - if (jsonItem.type() == JSONType.object){ - // Check if the response JSON has an 'id', otherwise makeItem() fails with 'Key not found: id' - if (hasId(jsonItem)) { - // Are we in a --upload-only & --remove-source-files scenario? - // We do not want to add the item to the database in this situation as there is no local reference to the file post file deletion - // If the item is a directory, we need to add this to the DB, if this is a file, we dont add this, the parent path is not in DB, thus any new files in this directory are not added - if ((uploadOnly) && (localDeleteAfterUpload) && (isItemFile(jsonItem))) { - // Log that we skipping adding item to the local DB and the reason why - log.vdebug("Skipping adding to database as --upload-only & --remove-source-files configured"); - } else { - // What is the JSON item we are trying to create a DB record with? - log.vdebug("Creating DB item from this JSON: ", jsonItem); - // Takes a JSON input and formats to an item which can be used by the database - Item item = makeItem(jsonItem); - // Add to the local database - log.vdebug("Adding to database: ", item); - itemdb.upsert(item); - - // If we have a remote drive ID, add this to our list of known drive id's - if (!item.remoteDriveId.empty) { - // Keep the driveIDsArray with unique entries only - if (!canFind(driveIDsArray, item.remoteDriveId)) { - // Add this drive id to the array to search with - driveIDsArray ~= item.remoteDriveId; - } - } - } - } else { - // log error - log.error("ERROR: OneDrive response missing required 'id' element"); - log.error("ERROR: ", jsonItem); - } - } else { - // log error - log.error("ERROR: An error was returned from OneDrive and the resulting response is not a valid JSON object"); - log.error("ERROR: Increase logging verbosity to assist determining why."); - } - } - - // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_move - // This function is only called in monitor mode when an move event is coming from - // inotify and we try to move the item. - void uploadMoveItem(string from, string to) - { - log.log("Moving ", from, " to ", to); - - // 'to' file validation .. is the 'to' file valid for upload? - if (isSymlink(to)) { - // if config says so we skip all symlinked items - if (cfg.getValueBool("skip_symlinks")) { - log.vlog("Skipping item - skip symbolic links configured: ", to); - return; - - } - // skip unexisting symbolic links - else if (!exists(readLink(to))) { - log.logAndNotify("Skipping item - invalid symbolic link: ", to); - return; - } - } - - // Check against Microsoft OneDrive restriction and limitations about Windows naming files - if (!isValidName(to)) { - log.logAndNotify("Skipping item - invalid name (Microsoft Naming Convention): ", to); - return; - } - - // Check for bad whitespace items - if (!containsBadWhiteSpace(to)) { - log.logAndNotify("Skipping item - invalid name (Contains an invalid whitespace item): ", to); - return; - } - - // Check for HTML ASCII Codes as part of file name - if (!containsASCIIHTMLCodes(to)) { - log.logAndNotify("Skipping item - invalid name (Contains HTML ASCII Code): ", to); - return; - } - - // 'to' file has passed file validation - Item fromItem, toItem, parentItem; - if (!itemdb.selectByPath(from, defaultDriveId, fromItem)) { - if (cfg.getValueBool("skip_dotfiles") && isDotFile(to)){ - log.log("Skipping upload due to skip_dotfile = true"); - return; - } else { - uploadNewFile(to); - return; - } - } - if (fromItem.parentId == null) { - // the item is a remote folder, need to do the operation on the parent - enforce(itemdb.selectByPathWithoutRemote(from, defaultDriveId, fromItem)); - } - if (itemdb.selectByPath(to, defaultDriveId, toItem)) { - // the destination has been overwritten - uploadDeleteItem(toItem, to); - } - if (!itemdb.selectByPath(dirName(to), defaultDriveId, parentItem)) { - // the parent item is not in the database - - // is the destination a .folder that is being skipped? - if (cfg.getValueBool("skip_dotfiles")) { - if (isDotFile(dirName(to))) { - // target location is a .folder - log.vdebug("Target location is excluded from sync due to skip_dotfiles = true"); - // item will have been moved locally, but as this is now to a location that is not synced, needs to be removed from OneDrive - log.log("Item has been moved to a location that is excluded from sync operations. Removing item from OneDrive"); - uploadDeleteItem(fromItem, from); - return; - } - } - - // some other error - throw new SyncException("Can't move an item to an unsynced directory"); - } - if (cfg.getValueBool("skip_dotfiles") && isDotFile(to)){ - log.log("Removing item from OneDrive due to skip_dotfiles = true"); - uploadDeleteItem(fromItem, from); - return; - } - if (fromItem.driveId != parentItem.driveId) { - // items cannot be moved between drives - uploadDeleteItem(fromItem, from); - uploadNewFile(to); - } else { - if (!exists(to)) { - log.vlog("uploadMoveItem target has disappeared: ", to); - return; - } - SysTime mtime = timeLastModified(to).toUTC(); - JSONValue diff = [ - "name": JSONValue(baseName(to)), - "parentReference": JSONValue([ - "id": parentItem.id - ]), - "fileSystemInfo": JSONValue([ - "lastModifiedDateTime": mtime.toISOExtString() - ]) - ]; - - // Perform the move operation on OneDrive - JSONValue response; - try { - response = onedrive.updateById(fromItem.driveId, fromItem.id, diff, fromItem.eTag); - } catch (OneDriveException e) { - if (e.httpStatusCode == 412) { - // OneDrive threw a 412 error, most likely: ETag does not match current item's value - // Retry without eTag - log.vdebug("File Move Failed - OneDrive eTag / cTag match issue"); - log.vlog("OneDrive returned a 'HTTP 412 - Precondition Failed' when attempting to move the file - gracefully handling error"); - string nullTag = null; - // move the file but without the eTag - response = onedrive.updateById(fromItem.driveId, fromItem.id, diff, nullTag); - } - } - // save the move response from OneDrive in the database - // Is the response a valid JSON object - validation checking done in saveItem - saveItem(response); - } - } - - // delete an item by it's path - void deleteByPath(const(string) path) - { - Item item; - // Need to check all driveid's we know about, not just the defaultDriveId - bool itemInDB = false; - foreach (searchDriveId; driveIDsArray) { - if (itemdb.selectByPath(path, searchDriveId, item)) { - // item was found in the DB - itemInDB = true; - break; - } - } - if (!itemInDB) { - throw new SyncException("The item to delete is not in the local database"); - } - - if (item.parentId == null) { - // the item is a remote folder, need to do the operation on the parent - enforce(itemdb.selectByPathWithoutRemote(path, defaultDriveId, item)); - } - try { - if (noRemoteDelete) { - // do not process remote delete - log.vlog("Skipping remote delete as --upload-only & --no-remote-delete configured"); - } else { - uploadDeleteItem(item, path); - } - } catch (OneDriveException e) { - if (e.httpStatusCode == 404) { - log.log(e.msg); - } else { - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - } - } - } - - // move a OneDrive folder from one name to another - void moveByPath(const(string) source, const(string) destination) - { - log.vlog("Moving remote folder: ", source, " -> ", destination); - - // Source and Destination are relative to ~/OneDrive - string sourcePath = source; - string destinationBasePath = dirName(destination).idup; - - // if destinationBasePath == '.' then destinationBasePath needs to be "" - if (destinationBasePath == ".") { - destinationBasePath = ""; - } - - string newFolderName = baseName(destination).idup; - string destinationPathString = "/drive/root:/" ~ destinationBasePath; - - // Build up the JSON changes - JSONValue moveData = ["name": newFolderName]; - JSONValue destinationPath = ["path": destinationPathString]; - moveData["parentReference"] = destinationPath; - - // Make the change on OneDrive - auto res = onedrive.moveByPath(sourcePath, moveData); - } - - // Query Office 365 SharePoint Shared Library site to obtain it's Drive ID - void querySiteCollectionForDriveID(string o365SharedLibraryName) - { - // Steps to get the ID: - // 1. Query https://graph.microsoft.com/v1.0/sites?search= with the name entered - // 2. Evaluate the response. A valid response will contain the description and the id. If the response comes back with nothing, the site name cannot be found or no access - // 3. If valid, use the returned ID and query the site drives - // https://graph.microsoft.com/v1.0/sites//drives - // 4. Display Shared Library Name & Drive ID - - string site_id; - string drive_id; - bool found = false; - JSONValue siteQuery; - string nextLink; - string[] siteSearchResults; - - // The account type must not be a personal account type - if (accountType == "personal"){ - log.error("ERROR: A OneDrive Personal Account cannot be used with --get-O365-drive-id. Please re-authenticate your client using a OneDrive Business Account."); - return; - } - - // What query are we performing? - log.log("Office 365 Library Name Query: ", o365SharedLibraryName); - - for (;;) { - try { - siteQuery = onedrive.o365SiteSearch(nextLink); - } catch (OneDriveException e) { - log.error("ERROR: Query of OneDrive for Office 365 Library Name failed"); - // Forbidden - most likely authentication scope needs to be updated - if (e.httpStatusCode == 403) { - log.error("ERROR: Authentication scope needs to be updated. Use --reauth and re-authenticate client."); - return; - } - // Requested resource cannot be found - if (e.httpStatusCode == 404) { - string siteSearchUrl; - if (nextLink.empty) { - siteSearchUrl = onedrive.getSiteSearchUrl(); - } else { - siteSearchUrl = nextLink; - } - // log the error - log.error("ERROR: Your OneDrive Account and Authentication Scope cannot access this OneDrive API: ", siteSearchUrl); - log.error("ERROR: To resolve, please discuss this issue with whomever supports your OneDrive and SharePoint environment."); - return; - } - // HTTP request returned status code 429 (Too Many Requests) - 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(); - log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to query OneDrive drive children"); - } - // HTTP request returned status code 504 (Gateway Timeout) or 429 retry - if ((e.httpStatusCode == 429) || (e.httpStatusCode == 504)) { - // re-try the specific changes queries - if (e.httpStatusCode == 504) { - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' when attempting to query Sharepoint Sites - retrying applicable request"); - log.vdebug("siteQuery = onedrive.o365SiteSearch(nextLink) previously threw an error - retrying"); - // The server, while acting as a proxy, did not receive a timely response from the upstream server it needed to access in attempting to complete the request. - log.vdebug("Thread sleeping for 30 seconds as the server did not receive a timely response from the upstream server it needed to access in attempting to complete the request"); - Thread.sleep(dur!"seconds"(30)); - } - // re-try original request - retried for 429 and 504 - try { - log.vdebug("Retrying Query: siteQuery = onedrive.o365SiteSearch(nextLink)"); - siteQuery = onedrive.o365SiteSearch(nextLink); - log.vdebug("Query 'siteQuery = onedrive.o365SiteSearch(nextLink)' performed successfully on re-try"); - } catch (OneDriveException e) { - // display what the error is - log.vdebug("Query Error: siteQuery = onedrive.o365SiteSearch(nextLink) on re-try after delay"); - // error was not a 504 this time - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } else { - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } - - // is siteQuery a valid JSON object & contain data we can use? - if ((siteQuery.type() == JSONType.object) && ("value" in siteQuery)) { - // valid JSON object - log.vdebug("O365 Query Response: ", siteQuery); - - foreach (searchResult; siteQuery["value"].array) { - // Need an 'exclusive' match here with o365SharedLibraryName as entered - log.vdebug("Found O365 Site: ", searchResult); - - // 'displayName' and 'id' have to be present in the search result record in order to query the site - if (("displayName" in searchResult) && ("id" in searchResult)) { - if (o365SharedLibraryName == searchResult["displayName"].str){ - // 'displayName' matches search request - site_id = searchResult["id"].str; - JSONValue siteDriveQuery; - - try { - siteDriveQuery = onedrive.o365SiteDrives(site_id); - } catch (OneDriveException e) { - log.error("ERROR: Query of OneDrive for Office Site ID failed"); - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - - // is siteDriveQuery a valid JSON object & contain data we can use? - if ((siteDriveQuery.type() == JSONType.object) && ("value" in siteDriveQuery)) { - // valid JSON object - foreach (driveResult; siteDriveQuery["value"].array) { - // Display results - writeln("-----------------------------------------------"); - log.vdebug("Site Details: ", driveResult); - found = true; - writeln("Site Name: ", searchResult["displayName"].str); - writeln("Library Name: ", driveResult["name"].str); - writeln("drive_id: ", driveResult["id"].str); - writeln("Library URL: ", driveResult["webUrl"].str); - } - // closeout - writeln("-----------------------------------------------"); - } else { - // not a valid JSON object - log.error("ERROR: There was an error performing this operation on OneDrive"); - log.error("ERROR: Increase logging verbosity to assist determining why."); - return; - } - } - } else { - // 'displayName', 'id' or ''webUrl' not present in JSON results for a specific site - string siteNameAvailable = "Site 'name' was restricted by OneDrive API permissions"; - bool displayNameAvailable = false; - bool idAvailable = false; - if ("name" in searchResult) siteNameAvailable = searchResult["name"].str; - if ("displayName" in searchResult) displayNameAvailable = true; - if ("id" in searchResult) idAvailable = true; - - // Display error details for this site data - writeln(); - log.error("ERROR: SharePoint Site details not provided for: ", siteNameAvailable); - log.error("ERROR: The SharePoint Site results returned from OneDrive API do not contain the required items to match. Please check your permissions with your site administrator."); - log.error("ERROR: Your site security settings is preventing the following details from being accessed: 'displayName' or 'id'"); - log.vlog(" - Is 'displayName' available = ", displayNameAvailable); - log.vlog(" - Is 'id' available = ", idAvailable); - log.error("ERROR: To debug this further, please increase verbosity (--verbose or --verbose --verbose) to provide further insight as to what details are actually being returned."); - } - } - - if(!found) { - // The SharePoint site we are searching for was not found in this bundle set - // Add to siteSearchResults so we can display what we did find - string siteSearchResultsEntry; - foreach (searchResult; siteQuery["value"].array) { - // We can only add the displayName if it is available - if ("displayName" in searchResult) { - // Use the displayName - siteSearchResultsEntry = " * " ~ searchResult["displayName"].str; - siteSearchResults ~= siteSearchResultsEntry; - } else { - // Add, but indicate displayName unavailable, use id - if ("id" in searchResult) { - siteSearchResultsEntry = " * " ~ "Unknown displayName (Data not provided by API), Site ID: " ~ searchResult["id"].str; - siteSearchResults ~= siteSearchResultsEntry; - } else { - // displayName and id unavailable, display in debug log the entry - log.vdebug("Bad SharePoint Data for site: ", searchResult); - } - } - } - } - } else { - // not a valid JSON object - log.error("ERROR: There was an error performing this operation on OneDrive"); - log.error("ERROR: Increase logging verbosity to assist determining why."); - return; - } - - // If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response - // to indicate more items are available and provide the request URL for the next page of items. - if ("@odata.nextLink" in siteQuery) { - // Update nextLink to next set of SharePoint library names - nextLink = siteQuery["@odata.nextLink"].str; - log.vdebug("Setting nextLink to (@odata.nextLink): ", nextLink); - } else break; - } - - // Was the intended target found? - if(!found) { - writeln(); - log.error("ERROR: The requested SharePoint site could not be found. Please check it's name and your permissions to access the site."); - // List all sites returned to assist user - writeln(); - log.log("The following SharePoint site names were returned:"); - foreach (searchResultEntry; siteSearchResults) { - // list the display name that we use to match against the user query - log.log(searchResultEntry); - } - } - } - - // Create an anonymous read-only shareable link for an existing file on OneDrive - void createShareableLinkForFile(string filePath, bool writeablePermissions) - { - JSONValue onedrivePathDetails; - JSONValue createShareableLinkResponse; - string driveId; - string itemId; - string fileShareLink; - - // Get the path details from OneDrive - try { - onedrivePathDetails = onedrive.getPathDetails(filePath); // Returns a JSON String for the OneDrive Path - } catch (OneDriveException e) { - log.vdebug("onedrivePathDetails = onedrive.getPathDetails(filePath); generated a OneDriveException"); - if (e.httpStatusCode == 404) { - // Requested path could not be found - log.error("ERROR: The requested path to query was not found on OneDrive"); - log.error("ERROR: Cannot create a shareable link for a file that does not exist 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 queryDriveForChanges(path);"); - createShareableLinkForFile(filePath, writeablePermissions); - // return back to original call - return; - } - - if (e.httpStatusCode == 504) { - // HTTP request returned status code 504 (Gateway Timeout) - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' - retrying request"); - // Retry original request by calling function again to avoid replicating any further error handling - createShareableLinkForFile(filePath, writeablePermissions); - // return back to original call - return; - } else { - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } - - // Was a valid JSON response received? - if (onedrivePathDetails.type() == JSONType.object) { - // valid JSON response for the file was received - // Configure the required variables - driveId = onedrivePathDetails["parentReference"]["driveId"].str; - itemId = onedrivePathDetails["id"].str; - - // What sort of shareable link is required? - JSONValue accessScope; - if (writeablePermissions) { - // configure the read-write access scope - accessScope = [ - "type": "edit", - "scope": "anonymous" - ]; - } else { - // configure the read-only access scope (default) - accessScope = [ - "type": "view", - "scope": "anonymous" - ]; - } - - // Create the shareable file link - createShareableLinkResponse = onedrive.createShareableLink(driveId, itemId, accessScope); - if ((createShareableLinkResponse.type() == JSONType.object) && ("link" in createShareableLinkResponse)) { - // Extract the file share link from the JSON response - fileShareLink = createShareableLinkResponse["link"]["webUrl"].str; - writeln("File Shareable Link: ", fileShareLink); - if (writeablePermissions) { - writeln("Shareable Link has read-write permissions - use and provide with caution"); - } - - } else { - // not a valid JSON object - log.error("ERROR: There was an error performing this operation on OneDrive"); - log.error("ERROR: Increase logging verbosity to assist determining why."); - return; - } - } else { - // not a valid JSON object - log.error("ERROR: There was an error performing this operation on OneDrive"); - log.error("ERROR: Increase logging verbosity to assist determining why."); - return; - } - } - - // Query OneDrive for file details of a given path - void queryOneDriveForFileDetails(string localFilePath, string syncDir, string outputType) - { - // Query if file is valid locally - if (exists(localFilePath)) { - // File exists locally, does it exist in the database - // Path needs to be relative to sync_dir path - Item item; - string[] distinctDriveIds = itemdb.selectDistinctDriveIds(); - string relativePath = relativePath(localFilePath, syncDir); - bool fileInDB = false; - foreach (searchDriveId; distinctDriveIds) { - if (itemdb.selectByPath(relativePath, searchDriveId, item)) { - // File is in the local database cache - fileInDB = true; - JSONValue fileDetails; - try { - fileDetails = onedrive.getFileDetails(item.driveId, item.id); - } catch (OneDriveException e) { - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - - // debug output of response - log.vdebug("API Response: ", fileDetails); - - // What sort of response to we generate - // --get-file-link response - if (outputType == "URL") { - if ((fileDetails.type() == JSONType.object) && ("webUrl" in fileDetails)) { - // Valid JSON object - writeln(fileDetails["webUrl"].str); - } - } - - // --modified-by response - if (outputType == "ModifiedBy") { - if ((fileDetails.type() == JSONType.object) && ("lastModifiedBy" in fileDetails)) { - // Valid JSON object - writeln("Last modified: ", fileDetails["lastModifiedDateTime"].str); - writeln("Last modified by: ", fileDetails["lastModifiedBy"]["user"]["displayName"].str); - // if 'email' provided, add this to the output - if ("email" in fileDetails["lastModifiedBy"]["user"]) { - writeln("Email Address: ", fileDetails["lastModifiedBy"]["user"]["email"].str); - } - } - } - } - } - // was path found? - if (!fileInDB) { - // File has not been synced with OneDrive - log.error("Path has not been synced with OneDrive: ", localFilePath); - } - } else { - // File does not exist locally - log.error("Path not found on local system: ", localFilePath); - } - } - - // Query the OneDrive 'drive' to determine if we are 'in sync' or if there are pending changes - void queryDriveForChanges(const(string) path) - { - - // Function variables - int validChanges = 0; - long downloadSize = 0; - string driveId; - string folderId; - string deltaLink; - string thisItemId; - string thisItemParentPath; - string syncFolderName; - string syncFolderPath; - string syncFolderChildPath; - JSONValue changes; - JSONValue onedrivePathDetails; - - // Get the path details from OneDrive - try { - onedrivePathDetails = onedrive.getPathDetails(path); // Returns a JSON String for the OneDrive Path - } catch (OneDriveException e) { - log.vdebug("onedrivePathDetails = onedrive.getPathDetails(path); generated a OneDriveException"); - if (e.httpStatusCode == 404) { - // Requested path could not be found - log.error("ERROR: The requested path to query 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 queryDriveForChanges(path);"); - queryDriveForChanges(path); - // return back to original call - return; - } - - if (e.httpStatusCode == 504) { - // HTTP request returned status code 504 (Gateway Timeout) - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' - retrying request"); - // Retry original request by calling function again to avoid replicating any further error handling - queryDriveForChanges(path); - // return back to original call - return; - } else { - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } - - if(isItemRemote(onedrivePathDetails)){ - // remote changes - driveId = onedrivePathDetails["remoteItem"]["parentReference"]["driveId"].str; // Should give something like 66d53be8a5056eca - folderId = onedrivePathDetails["remoteItem"]["id"].str; // Should give something like BC7D88EC1F539DCF!107 - syncFolderName = onedrivePathDetails["name"].str; - // A remote drive item will not have ["parentReference"]["path"] - syncFolderPath = ""; - syncFolderChildPath = ""; - } else { - driveId = defaultDriveId; - folderId = onedrivePathDetails["id"].str; // Should give something like 12345ABCDE1234A1!101 - syncFolderName = onedrivePathDetails["name"].str; - if (hasParentReferencePath(onedrivePathDetails)) { - syncFolderPath = onedrivePathDetails["parentReference"]["path"].str; - syncFolderChildPath = syncFolderPath ~ "/" ~ syncFolderName ~ "/"; - } else { - // root drive item will not have ["parentReference"]["path"] - syncFolderPath = ""; - syncFolderChildPath = ""; - } - } - - // Query Database for the deltaLink - deltaLink = itemdb.getDeltaLink(driveId, folderId); - - const(char)[] idToQuery; - if (driveId == defaultDriveId) { - // The drive id matches our users default drive id - idToQuery = defaultRootId.dup; - } else { - // The drive id does not match our users default drive id - // Potentially the 'path id' we are requesting the details of is a Shared Folder (remote item) - // Use folderId - idToQuery = folderId; - } - - // Query OneDrive changes - try { - changes = onedrive.viewChangesByItemId(driveId, idToQuery, deltaLink); - } catch (OneDriveException e) { - 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 queryDriveForChanges(path);"); - queryDriveForChanges(path); - // return back to original call - return; - } else { - // OneDrive threw an error - log.vdebug("Error query: changes = onedrive.viewChangesById(driveId, idToQuery, deltaLink)"); - log.vdebug("OneDrive threw an error when querying for these changes:"); - log.vdebug("driveId: ", driveId); - log.vdebug("idToQuery: ", idToQuery); - log.vdebug("Previous deltaLink: ", deltaLink); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return; - } - } - - // Are there any changes on OneDrive? - if (count(changes["value"].array) != 0) { - // Were we given a remote path to check if we are in sync for, or the root? - if (path != "/") { - // we were given a directory to check, we need to validate the list of changes against this path only - foreach (item; changes["value"].array) { - // Is this change valid for the 'path' we are checking? - if (hasParentReferencePath(item)) { - thisItemId = item["parentReference"]["id"].str; - thisItemParentPath = item["parentReference"]["path"].str; - } else { - thisItemId = item["id"].str; - // Is the defaultDriveId == driveId - if (driveId == defaultDriveId){ - // 'root' items will not have ["parentReference"]["path"] - if (isItemRoot(item)){ - thisItemParentPath = ""; - } else { - thisItemParentPath = item["parentReference"]["path"].str; - } - } else { - // A remote drive item will not have ["parentReference"]["path"] - thisItemParentPath = ""; - } - } - - if ( (thisItemId == folderId) || (canFind(thisItemParentPath, syncFolderChildPath)) || (canFind(thisItemParentPath, folderId)) ){ - // This is a change we want count - validChanges++; - if ((isItemFile(item)) && (hasFileSize(item))) { - downloadSize = downloadSize + item["size"].integer; - } - } - } - // Are there any valid changes? - if (validChanges != 0){ - writeln("Selected directory is out of sync with OneDrive"); - if (downloadSize > 0){ - downloadSize = downloadSize / 1000; - writeln("Approximate data to download from OneDrive: ", downloadSize, " KB"); - } - } else { - writeln("No pending remote changes - selected directory is in sync"); - } - } else { - writeln("Local directory is out of sync with OneDrive"); - foreach (item; changes["value"].array) { - if ((isItemFile(item)) && (hasFileSize(item))) { - downloadSize = downloadSize + item["size"].integer; - } - } - if (downloadSize > 0){ - downloadSize = downloadSize / 1000; - writeln("Approximate data to download from OneDrive: ", downloadSize, " KB"); - } - } - } else { - writeln("No pending remote changes - in sync"); - } - } - - // Create a fake OneDrive response suitable for use with saveItem - JSONValue createFakeResponse(const(string) path) - { - import std.digest.sha; - // Generate a simulated JSON response which can be used - // At a minimum we need: - // 1. eTag - // 2. cTag - // 3. fileSystemInfo - // 4. file or folder. if file, hash of file - // 5. id - // 6. name - // 7. parent reference - - string fakeDriveId = defaultDriveId; - string fakeRootId = defaultRootId; - SysTime mtime = timeLastModified(path).toUTC(); - - // Need to update the 'fakeDriveId' & 'fakeRootId' with elements from the --dry-run database - // Otherwise some calls to validate objects will fail as the actual driveId being used is invalid - 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[] fakedOneDriveItemValues = sha1.digest(path); - - JSONValue fakeResponse; - - if (isDir(path)) { - // path is a directory - fakeResponse = [ - "id": JSONValue(toHexString(fakedOneDriveItemValues)), - "cTag": JSONValue(toHexString(fakedOneDriveItemValues)), - "eTag": JSONValue(toHexString(fakedOneDriveItemValues)), - "fileSystemInfo": JSONValue([ - "createdDateTime": mtime.toISOExtString(), - "lastModifiedDateTime": mtime.toISOExtString() - ]), - "name": JSONValue(baseName(path)), - "parentReference": JSONValue([ - "driveId": JSONValue(fakeDriveId), - "driveType": JSONValue(accountType), - "id": JSONValue(fakeRootId) - ]), - "folder": JSONValue("") - ]; - } else { - // path is a file - // compute file hash - both business and personal responses use quickXorHash - string quickXorHash = computeQuickXorHash(path); - - fakeResponse = [ - "id": JSONValue(toHexString(fakedOneDriveItemValues)), - "cTag": JSONValue(toHexString(fakedOneDriveItemValues)), - "eTag": JSONValue(toHexString(fakedOneDriveItemValues)), - "fileSystemInfo": JSONValue([ - "createdDateTime": mtime.toISOExtString(), - "lastModifiedDateTime": mtime.toISOExtString() - ]), - "name": JSONValue(baseName(path)), - "parentReference": JSONValue([ - "driveId": JSONValue(fakeDriveId), - "driveType": JSONValue(accountType), - "id": JSONValue(fakeRootId) - ]), - "file": JSONValue([ - "hashes":JSONValue([ - "quickXorHash": JSONValue(quickXorHash) - ]) - - ]) - ]; - } - - log.vdebug("Generated Fake OneDrive Response: ", fakeResponse); - return fakeResponse; - } - - void handleOneDriveThrottleRequest() - { - // If OneDrive sends a status code 429 then this function will be used to process the Retry-After response header which contains the value by which we need to wait - log.vdebug("Handling a OneDrive HTTP 429 Response Code (Too Many Requests)"); - // Read in the Retry-After HTTP header as set and delay as per this value before retrying the request - auto retryAfterValue = onedrive.getRetryAfterValue(); - log.vdebug("Using Retry-After Value = ", retryAfterValue); - - // HTTP request returned status code 429 (Too Many Requests) - // https://github.com/abraunegg/onedrive/issues/133 - // https://github.com/abraunegg/onedrive/issues/815 - - ulong delayBeforeRetry = 0; - if (retryAfterValue != 0) { - // Use the HTTP Response Header Value - delayBeforeRetry = retryAfterValue; - } else { - // Use a 120 second delay as a default given header value was zero - // This value is based on log files and data when determining correct process for 429 response handling - delayBeforeRetry = 120; - // Update that we are over-riding the provided value with a default - log.vdebug("HTTP Response Header retry-after value was 0 - Using a preconfigured default of: ", delayBeforeRetry); - } - - // Sleep thread as per request - log.log("Thread sleeping due to 'HTTP request returned status code 429' - The request has been throttled"); - log.log("Sleeping for ", delayBeforeRetry, " seconds"); - Thread.sleep(dur!"seconds"(delayBeforeRetry)); - - // Reset retry-after value to zero as we have used this value now and it may be changed in the future to a different value - onedrive.resetRetryAfterValue(); - } - - // Generage a /delta compatible response when using National Azure AD deployments that do not support /delta queries - // see: https://docs.microsoft.com/en-us/graph/deployments#supported-features - JSONValue generateDeltaResponse(const(char)[] driveId, const(char)[] idToQuery) - { - // JSON value which will be responded with - JSONValue deltaResponse; - // initial data - JSONValue rootData; - JSONValue driveData; - JSONValue topLevelChildren; - JSONValue[] childrenData; - string nextLink; - - // Get drive details for the provided driveId - try { - driveData = onedrive.getPathDetailsById(driveId, idToQuery); - } catch (OneDriveException e) { - log.vdebug("driveData = onedrive.getPathDetailsById(driveId, idToQuery) generated a OneDriveException"); - // HTTP request returned status code 504 (Gateway Timeout) or 429 retry - if ((e.httpStatusCode == 429) || (e.httpStatusCode == 504)) { - // 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. - if (e.httpStatusCode == 429) { - log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - retrying applicable request"); - handleOneDriveThrottleRequest(); - } - if (e.httpStatusCode == 504) { - log.vdebug("Retrying original request that generated the HTTP 504 (Gateway Timeout) - retrying applicable request"); - Thread.sleep(dur!"seconds"(30)); - } - // Retry original request by calling function again to avoid replicating any further error handling - driveData = onedrive.getPathDetailsById(driveId, idToQuery); - } else { - // There was a HTTP 5xx Server Side Error - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - // Must exit here - onedrive.shutdown(); - exit(-1); - } - } - - if (!isItemRoot(driveData)) { - // Get root details for the provided driveId - try { - rootData = onedrive.getDriveIdRoot(driveId); - } catch (OneDriveException e) { - log.vdebug("rootData = onedrive.getDriveIdRoot(driveId) generated a OneDriveException"); - // HTTP request returned status code 504 (Gateway Timeout) or 429 retry - if ((e.httpStatusCode == 429) || (e.httpStatusCode == 504)) { - // 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. - if (e.httpStatusCode == 429) { - log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - retrying applicable request"); - handleOneDriveThrottleRequest(); - } - if (e.httpStatusCode == 504) { - log.vdebug("Retrying original request that generated the HTTP 504 (Gateway Timeout) - retrying applicable request"); - Thread.sleep(dur!"seconds"(30)); - } - // Retry original request by calling function again to avoid replicating any further error handling - rootData = onedrive.getDriveIdRoot(driveId); - - } else { - // There was a HTTP 5xx Server Side Error - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - // Must exit here - onedrive.shutdown(); - exit(-1); - } - } - // Add driveData JSON data to array - log.vlog("Adding OneDrive root details for processing"); - childrenData ~= rootData; - } - - // Add driveData JSON data to array - log.vlog("Adding OneDrive folder details for processing"); - childrenData ~= driveData; - - for (;;) { - // query top level children - try { - topLevelChildren = onedrive.listChildren(driveId, idToQuery, nextLink); - } catch (OneDriveException e) { - // OneDrive threw an error - log.vdebug("------------------------------------------------------------------"); - log.vdebug("Query Error: topLevelChildren = onedrive.listChildren(driveId, idToQuery, nextLink)"); - log.vdebug("driveId: ", driveId); - log.vdebug("idToQuery: ", idToQuery); - log.vdebug("nextLink: ", nextLink); - - // HTTP request returned status code 404 (Not Found) - if (e.httpStatusCode == 404) { - // Stop application - log.log("\n\nOneDrive returned a 'HTTP 404 - Item not found'"); - log.log("The item id to query was not found on OneDrive"); - log.log("\nRemove your '", cfg.databaseFilePath, "' file and try to sync again\n"); - } - - // HTTP request returned status code 429 (Too Many Requests) - 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(); - log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to query OneDrive drive children"); - } - - // HTTP request returned status code 500 (Internal Server Error) - if (e.httpStatusCode == 500) { - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - } - - // HTTP request returned status code 504 (Gateway Timeout) or 429 retry - if ((e.httpStatusCode == 429) || (e.httpStatusCode == 504)) { - // re-try the specific changes queries - if (e.httpStatusCode == 504) { - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' when attempting to query OneDrive drive children - retrying applicable request"); - log.vdebug("topLevelChildren = onedrive.listChildren(driveId, idToQuery, nextLink) previously threw an error - retrying"); - // The server, while acting as a proxy, did not receive a timely response from the upstream server it needed to access in attempting to complete the request. - log.vdebug("Thread sleeping for 30 seconds as the server did not receive a timely response from the upstream server it needed to access in attempting to complete the request"); - Thread.sleep(dur!"seconds"(30)); - } - // re-try original request - retried for 429 and 504 - try { - log.vdebug("Retrying Query: topLevelChildren = onedrive.listChildren(driveId, idToQuery, nextLink)"); - topLevelChildren = onedrive.listChildren(driveId, idToQuery, nextLink); - log.vdebug("Query 'topLevelChildren = onedrive.listChildren(driveId, idToQuery, nextLink)' performed successfully on re-try"); - } catch (OneDriveException e) { - // display what the error is - log.vdebug("Query Error: topLevelChildren = onedrive.listChildren(driveId, idToQuery, nextLink) on re-try after delay"); - // error was not a 504 this time - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - } - } else { - // Default operation if not 404, 410, 429, 500 or 504 errors - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - } - } - - // process top level children - log.vlog("Adding ", count(topLevelChildren["value"].array), " OneDrive items for processing from OneDrive folder"); - foreach (child; topLevelChildren["value"].array) { - // add this child to the array of objects - childrenData ~= child; - // is this child a folder? - if (isItemFolder(child)){ - // We have to query this folders children if childCount > 0 - if (child["folder"]["childCount"].integer > 0){ - // This child folder has children - string childIdToQuery = child["id"].str; - string childDriveToQuery = child["parentReference"]["driveId"].str; - auto childParentPath = child["parentReference"]["path"].str.split(":"); - string folderPathToScan = childParentPath[1] ~ "/" ~ child["name"].str; - string pathForLogging = "/" ~ driveData["name"].str ~ "/" ~ child["name"].str; - JSONValue[] grandChildrenData = queryForChildren(childDriveToQuery, childIdToQuery, folderPathToScan, pathForLogging); - foreach (grandChild; grandChildrenData.array) { - // add the grandchild to the array - childrenData ~= grandChild; - } - } - } - } - // If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response - // to indicate more items are available and provide the request URL for the next page of items. - if ("@odata.nextLink" in topLevelChildren) { - // Update nextLink to next changeSet bundle - log.vdebug("Setting nextLink to (@odata.nextLink): ", nextLink); - nextLink = topLevelChildren["@odata.nextLink"].str; - } else break; - } - - // craft response from all returned elements - deltaResponse = [ - "@odata.context": JSONValue("https://graph.microsoft.com/v1.0/$metadata#Collection(driveItem)"), - "value": JSONValue(childrenData.array) - ]; - - // return the generated JSON response - return deltaResponse; - } - - // query child for children - JSONValue[] queryForChildren(const(char)[] driveId, const(char)[] idToQuery, const(char)[] childParentPath, string pathForLogging) - { - // function variables - JSONValue thisLevelChildren; - JSONValue[] thisLevelChildrenData; - string nextLink; - - for (;;) { - // query children - thisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink); - - // process this level children - if (!childParentPath.empty) { - // We dont use childParentPath to log, as this poses an information leak risk. - // The full parent path of the child, as per the JSON might be: - // /Level 1/Level 2/Level 3/Child Shared Folder/some folder/another folder - // But 'Child Shared Folder' is what is shared, thus '/Level 1/Level 2/Level 3/' is a potential information leak if logged. - // Plus, the application output now shows accuratly what is being shared - so that is a good thing. - log.vlog("Adding ", count(thisLevelChildren["value"].array), " OneDrive items for processing from ", pathForLogging); - } - foreach (child; thisLevelChildren["value"].array) { - // add this child to the array of objects - thisLevelChildrenData ~= child; - // is this child a folder? - if (isItemFolder(child)){ - // We have to query this folders children if childCount > 0 - if (child["folder"]["childCount"].integer > 0){ - // This child folder has children - string childIdToQuery = child["id"].str; - string childDriveToQuery = child["parentReference"]["driveId"].str; - auto grandchildParentPath = child["parentReference"]["path"].str.split(":"); - string folderPathToScan = grandchildParentPath[1] ~ "/" ~ child["name"].str; - string newLoggingPath = pathForLogging ~ "/" ~ child["name"].str; - JSONValue[] grandChildrenData = queryForChildren(childDriveToQuery, childIdToQuery, folderPathToScan, newLoggingPath); - foreach (grandChild; grandChildrenData.array) { - // add the grandchild to the array - thisLevelChildrenData ~= grandChild; - } - } - } - } - // If a collection exceeds the default page size (200 items), the @odata.nextLink property is returned in the response - // to indicate more items are available and provide the request URL for the next page of items. - if ("@odata.nextLink" in thisLevelChildren) { - // Update nextLink to next changeSet bundle - nextLink = thisLevelChildren["@odata.nextLink"].str; - log.vdebug("Setting nextLink to (@odata.nextLink): ", nextLink); - } else break; - } - - // return response - return thisLevelChildrenData; - } - - // Query from OneDrive the child objects for this element - JSONValue queryThisLevelChildren(const(char)[] driveId, const(char)[] idToQuery, string nextLink) - { - JSONValue thisLevelChildren; - - // query children - try { - // attempt API call - log.vdebug("Attempting Query: thisLevelChildren = onedrive.listChildren(driveId, idToQuery, nextLink)"); - thisLevelChildren = onedrive.listChildren(driveId, idToQuery, nextLink); - log.vdebug("Query 'thisLevelChildren = onedrive.listChildren(driveId, idToQuery, nextLink)' performed successfully"); - } catch (OneDriveException e) { - // OneDrive threw an error - log.vdebug("------------------------------------------------------------------"); - log.vdebug("Query Error: thisLevelChildren = onedrive.listChildren(driveId, idToQuery, nextLink)"); - log.vdebug("driveId: ", driveId); - log.vdebug("idToQuery: ", idToQuery); - log.vdebug("nextLink: ", nextLink); - - // HTTP request returned status code 404 (Not Found) - if (e.httpStatusCode == 404) { - // Stop application - log.log("\n\nOneDrive returned a 'HTTP 404 - Item not found'"); - log.log("The item id to query was not found on OneDrive"); - log.log("\nRemove your '", cfg.databaseFilePath, "' file and try to sync again\n"); - } - - // HTTP request returned status code 429 (Too Many Requests) - 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(); - log.vdebug("Retrying original request that generated the OneDrive HTTP 429 Response Code (Too Many Requests) - attempting to query OneDrive drive children"); - } - - // HTTP request returned status code 504 (Gateway Timeout) or 429 retry - if ((e.httpStatusCode == 429) || (e.httpStatusCode == 504)) { - // re-try the specific changes queries - if (e.httpStatusCode == 504) { - // transient error - try again in 30 seconds - log.log("OneDrive returned a 'HTTP 504 - Gateway Timeout' when attempting to query OneDrive drive children - retrying applicable request"); - log.vdebug("thisLevelChildren = onedrive.listChildren(driveId, idToQuery, nextLink) previously threw an error - retrying"); - // The server, while acting as a proxy, did not receive a timely response from the upstream server it needed to access in attempting to complete the request. - log.vdebug("Thread sleeping for 30 seconds as the server did not receive a timely response from the upstream server it needed to access in attempting to complete the request"); - Thread.sleep(dur!"seconds"(30)); - } - // re-try original request - retried for 429 and 504 - but loop back calling this function - log.vdebug("Retrying Query: thisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink)"); - thisLevelChildren = queryThisLevelChildren(driveId, idToQuery, nextLink); - } else { - // Default operation if not 404, 429 or 504 errors - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - } - } - // return response - return thisLevelChildren; - } - - // OneDrive Business Shared Folder support - void listOneDriveBusinessSharedFolders() - { - // List OneDrive Business Shared Folders - log.log("\nListing available OneDrive Business Shared Folders:"); - // Query the GET /me/drive/sharedWithMe API - JSONValue graphQuery; - try { - graphQuery = onedrive.getSharedWithMe(); - } catch (OneDriveException e) { - if (e.httpStatusCode == 401) { - // HTTP request returned status code 401 (Unauthorized) - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - handleClientUnauthorised(); - } - 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) - graphQuery = onedrive.getSharedWithMe();"); - graphQuery = onedrive.getSharedWithMe(); - } - if (e.httpStatusCode >= 500) { - // There was a HTTP 5xx Server Side Error - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - // Must exit here - onedrive.shutdown(); - exit(-1); - } - } - - if (graphQuery.type() == JSONType.object) { - if (count(graphQuery["value"].array) == 0) { - // no shared folders returned - write("\nNo OneDrive Business Shared Folders were returned\n"); - } else { - // shared folders were returned - log.vdebug("onedrive.getSharedWithMe API Response: ", graphQuery); - foreach (searchResult; graphQuery["value"].array) { - // loop variables - string sharedFolderName; - string sharedByName; - string sharedByEmail; - // 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; - } - } - // Output query result - log.log("---------------------------------------"); - log.log("Shared Folder: ", sharedFolderName); - if ((sharedByName != "") && (sharedByEmail != "")) { - log.log("Shared By: ", sharedByName, " (", sharedByEmail, ")"); - } else { - if (sharedByName != "") { - log.log("Shared By: ", sharedByName); - } - } - 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); - } - } - } - } - write("\n"); - } else { - // Log that an invalid JSON object was returned - log.error("ERROR: onedrive.getSharedWithMe call returned an invalid JSON Object"); - } - } - - // Query itemdb.computePath() and catch potential assert when DB consistency issue occurs - string computeItemPath(string thisDriveId, string thisItemId) - { - static import core.exception; - string calculatedPath; - log.vdebug("Attempting to calculate local filesystem path for ", thisDriveId, " and ", thisItemId); - try { - calculatedPath = itemdb.computePath(thisDriveId, thisItemId); - } catch (core.exception.AssertError) { - // broken tree in the database, we cant compute the path for this item id, exit - log.error("ERROR: A database consistency issue has been caught. A --resync is needed to rebuild the database."); - // Must exit here to preserve data - onedrive.shutdown(); - exit(-1); - } - - // return calculated path as string - return calculatedPath; - } - - void handleClientUnauthorised() - { - // common code for handling when a client is unauthorised - writeln(); - log.errorAndNotify("ERROR: Check your configuration as your refresh_token may be empty or invalid. You may need to issue a --reauth and re-authorise this client."); - writeln(); - // Must exit here - onedrive.shutdown(); - exit(-1); - } - - // Wrapper function for makeDatabaseItem so we can check if the item, if a file, has any hashes - private Item makeItem(JSONValue onedriveJSONItem) - { - Item newDatabaseItem = makeDatabaseItem(onedriveJSONItem); - - // Check for hashes in this DB item - if (newDatabaseItem.type == ItemType.file) { - // Does this file have a size greater than 0 - zero size files will potentially not have a hash - if (hasFileSize(onedriveJSONItem)) { - if (onedriveJSONItem["size"].integer > 0) { - // Does the item have any hashes? - if ((newDatabaseItem.quickXorHash.empty) && (newDatabaseItem.sha256Hash.empty)) { - // Odd .. no hash ...... - string apiMessage = "WARNING: OneDrive API inconsistency - this file does not have any hash: "; - // This is computationally expensive .. but we are only doing this if there are no hashses provided: - bool parentInDatabase = itemdb.idInLocalDatabase(newDatabaseItem.driveId, newDatabaseItem.parentId); - if (parentInDatabase) { - // Calculate this item path - string newItemPath = computeItemPath(newDatabaseItem.driveId, newDatabaseItem.parentId) ~ "/" ~ newDatabaseItem.name; - log.log(apiMessage, newItemPath); - } else { - // Use the item ID - log.log(apiMessage, newDatabaseItem.id); - } - } - } - } - } - return newDatabaseItem; - } - -} diff --git a/src/upload.d b/src/upload.d deleted file mode 100644 index 012598a0..00000000 --- a/src/upload.d +++ /dev/null @@ -1,302 +0,0 @@ -import std.algorithm, std.conv, std.datetime, std.file, std.json; -import std.stdio, core.thread, std.string; -import progress, onedrive, util; -static import log; - -private long fragmentSize = 10 * 2^^20; // 10 MiB - -struct UploadSession -{ - private OneDriveApi onedrive; - private bool verbose; - // https://dev.onedrive.com/resources/uploadSession.htm - private JSONValue session; - // path where to save the session - private string sessionFilePath; - - this(OneDriveApi onedrive, string sessionFilePath) - { - assert(onedrive); - this.onedrive = onedrive; - this.sessionFilePath = sessionFilePath; - this.verbose = verbose; - } - - JSONValue upload(string localPath, const(char)[] parentDriveId, const(char)[] parentId, const(char)[] filename, const(char)[] eTag = null) - { - // Fix https://github.com/abraunegg/onedrive/issues/2 - // More Details https://github.com/OneDrive/onedrive-api-docs/issues/778 - - SysTime localFileLastModifiedTime = timeLastModified(localPath).toUTC(); - localFileLastModifiedTime.fracSecs = Duration.zero; - - JSONValue fileSystemInfo = [ - "item": JSONValue([ - "@name.conflictBehavior": JSONValue("replace"), - "fileSystemInfo": JSONValue([ - "lastModifiedDateTime": localFileLastModifiedTime.toISOExtString() - ]) - ]) - ]; - - // Try to create the upload session for this file - session = onedrive.createUploadSession(parentDriveId, parentId, filename, eTag, fileSystemInfo); - - if ("uploadUrl" in session){ - session["localPath"] = localPath; - save(); - return upload(); - } else { - // there was an error - log.vlog("Create file upload session failed ... skipping file upload"); - // return upload() will return a JSONValue response, create an empty JSONValue response to return - JSONValue response; - return response; - } - } - - /* Restore the previous upload session. - * Returns true if the session is valid. Call upload() to resume it. - * Returns false if there is no session or the session is expired. */ - bool restore() - { - if (exists(sessionFilePath)) { - log.vlog("Trying to restore the upload session ..."); - // We cant use JSONType.object check, as this is currently a string - // We cant use a try & catch block, as it does not catch std.json.JSONException - auto sessionFileText = readText(sessionFilePath); - if(canFind(sessionFileText,"@odata.context")) { - session = readText(sessionFilePath).parseJSON(); - } else { - log.vlog("Upload session resume data is invalid"); - remove(sessionFilePath); - return false; - } - - // Check the session resume file for expirationDateTime - if ("expirationDateTime" in session){ - // expirationDateTime in the file - auto expiration = SysTime.fromISOExtString(session["expirationDateTime"].str); - if (expiration < Clock.currTime()) { - log.vlog("The upload session is expired"); - return false; - } - if (!exists(session["localPath"].str)) { - log.vlog("The file does not exist anymore"); - return false; - } - // Can we read the file - as a permissions issue or file corruption will cause a failure on resume - // https://github.com/abraunegg/onedrive/issues/113 - if (readLocalFile(session["localPath"].str)){ - // able to read the file - // request the session status - JSONValue response; - try { - response = onedrive.requestUploadStatus(session["uploadUrl"].str); - } catch (OneDriveException e) { - // handle any onedrive error response - if (e.httpStatusCode == 400) { - log.vlog("Upload session not found"); - return false; - } - } - - // do we have a valid response from OneDrive? - if (response.type() == JSONType.object){ - // JSON object - if (("expirationDateTime" in response) && ("nextExpectedRanges" in response)){ - // has the elements we need - session["expirationDateTime"] = response["expirationDateTime"]; - session["nextExpectedRanges"] = response["nextExpectedRanges"]; - if (session["nextExpectedRanges"].array.length == 0) { - log.vlog("The upload session is completed"); - return false; - } - } else { - // bad data - log.vlog("Restore file upload session failed - invalid data response from OneDrive"); - if (exists(sessionFilePath)) { - remove(sessionFilePath); - } - return false; - } - } else { - // not a JSON object - log.vlog("Restore file upload session failed - invalid response from OneDrive"); - if (exists(sessionFilePath)) { - remove(sessionFilePath); - } - return false; - } - return true; - } else { - // unable to read the local file - log.vlog("Restore file upload session failed - unable to read the local file"); - if (exists(sessionFilePath)) { - remove(sessionFilePath); - } - return false; - } - } else { - // session file contains an error - cant resume - log.vlog("Restore file upload session failed - cleaning up session resume"); - if (exists(sessionFilePath)) { - remove(sessionFilePath); - } - return false; - } - } - return false; - } - - JSONValue upload() - { - // Response for upload - JSONValue response; - - // session JSON needs to contain valid elements - long offset; - long fileSize; - - if ("nextExpectedRanges" in session){ - offset = session["nextExpectedRanges"][0].str.splitter('-').front.to!long; - } - - if ("localPath" in session){ - fileSize = getSize(session["localPath"].str); - } - - if ("uploadUrl" in session){ - // Upload file via session created - // Upload Progress Bar - size_t iteration = (roundTo!int(double(fileSize)/double(fragmentSize)))+1; - Progress p = new Progress(iteration); - p.title = "Uploading"; - long fragmentCount = 0; - long fragSize = 0; - - // Initialise the download bar at 0% - p.next(); - - while (true) { - fragmentCount++; - log.vdebugNewLine("Fragment: ", fragmentCount, " of ", iteration); - p.next(); - log.vdebugNewLine("fragmentSize: ", fragmentSize, "offset: ", offset, " fileSize: ", fileSize ); - fragSize = fragmentSize < fileSize - offset ? fragmentSize : fileSize - offset; - log.vdebugNewLine("Using fragSize: ", fragSize); - - // fragSize must not be a negative value - if (fragSize < 0) { - // Session upload will fail - // not a JSON object - fragment upload failed - log.vlog("File upload session failed - invalid calculation of fragment size"); - if (exists(sessionFilePath)) { - remove(sessionFilePath); - } - // set response to null as error - response = null; - return response; - } - - // If the resume upload fails, we need to check for a return code here - try { - response = onedrive.uploadFragment( - session["uploadUrl"].str, - session["localPath"].str, - offset, - fragSize, - fileSize - ); - } catch (OneDriveException e) { - // if a 100 response is generated, continue - if (e.httpStatusCode == 100) { - continue; - } - // there was an error response from OneDrive when uploading the file fragment - // handle 'HTTP request returned status code 429 (Too Many Requests)' first - if (e.httpStatusCode == 429) { - auto retryAfterValue = onedrive.getRetryAfterValue(); - log.vdebug("Fragment upload failed - received throttle request response from OneDrive"); - log.vdebug("Using Retry-After Value = ", retryAfterValue); - // Sleep thread as per request - log.log("\nThread sleeping due to 'HTTP request returned status code 429' - The request has been throttled"); - log.log("Sleeping for ", retryAfterValue, " seconds"); - Thread.sleep(dur!"seconds"(retryAfterValue)); - log.log("Retrying fragment upload"); - } else { - // insert a new line as well, so that the below error is inserted on the console in the right location - log.vlog("\nFragment upload failed - received an exception response from OneDrive"); - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - // retry fragment upload in case error is transient - log.vlog("Retrying fragment upload"); - } - - try { - response = onedrive.uploadFragment( - session["uploadUrl"].str, - session["localPath"].str, - offset, - fragSize, - fileSize - ); - } catch (OneDriveException e) { - // OneDrive threw another error on retry - log.vlog("Retry to upload fragment failed"); - // display what the error is - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - // set response to null as the fragment upload was in error twice - response = null; - } - } - // was the fragment uploaded without issue? - if (response.type() == JSONType.object){ - offset += fragmentSize; - if (offset >= fileSize) break; - // update the session details - session["expirationDateTime"] = response["expirationDateTime"]; - session["nextExpectedRanges"] = response["nextExpectedRanges"]; - save(); - } else { - // not a JSON object - fragment upload failed - log.vlog("File upload session failed - invalid response from OneDrive"); - if (exists(sessionFilePath)) { - remove(sessionFilePath); - } - // set response to null as error - response = null; - return response; - } - } - // upload complete - p.next(); - writeln(); - if (exists(sessionFilePath)) { - remove(sessionFilePath); - } - return response; - } else { - // session elements were not present - log.vlog("Session has no valid upload URL ... skipping this file upload"); - // return an empty JSON response - response = null; - return response; - } - } - - string getUploadSessionLocalFilePath() { - // return the session file path - string localPath = ""; - if ("localPath" in session){ - localPath = session["localPath"].str; - } - return localPath; - } - - // save session details to temp file - private void save() - { - std.file.write(sessionFilePath, session.toString()); - } -} diff --git a/src/util.d b/src/util.d deleted file mode 100644 index cbaa5b8e..00000000 --- a/src/util.d +++ /dev/null @@ -1,609 +0,0 @@ -import std.base64; -import std.conv; -import std.digest.crc, std.digest.sha; -import std.net.curl; -import std.datetime; -import std.file; -import std.path; -import std.regex; -import std.socket; -import std.stdio; -import std.string; -import std.algorithm; -import std.uri; -import std.json; -import std.traits; -import qxor; -import core.stdc.stdlib; - -import log; -import config; - -shared string deviceName; - -static this() -{ - deviceName = Socket.hostName; -} - -// gives a new name to the specified file or directory -void safeRename(const(char)[] path) -{ - auto ext = extension(path); - auto newPath = path.chomp(ext) ~ "-" ~ deviceName; - if (exists(newPath ~ ext)) { - int n = 2; - char[] newPath2; - do { - newPath2 = newPath ~ "-" ~ n.to!string; - n++; - } while (exists(newPath2 ~ ext)); - newPath = newPath2; - } - newPath ~= ext; - rename(path, newPath); -} - -// deletes the specified file without throwing an exception if it does not exists -void safeRemove(const(char)[] path) -{ - if (exists(path)) remove(path); -} - -// returns the quickXorHash base64 string of a file -string computeQuickXorHash(string path) -{ - QuickXor qxor; - auto file = File(path, "rb"); - foreach (ubyte[] data; chunks(file, 4096)) { - qxor.put(data); - } - return Base64.encode(qxor.finish()); -} - -// returns the SHA256 hex string of a file -string computeSHA256Hash(string path) { - SHA256 sha256; - auto file = File(path, "rb"); - foreach (ubyte[] data; chunks(file, 4096)) { - sha256.put(data); - } - return sha256.finish().toHexString().dup; -} - -// converts wildcards (*, ?) to regex -Regex!char wild2regex(const(char)[] pattern) -{ - string str; - str.reserve(pattern.length + 2); - str ~= "^"; - foreach (c; pattern) { - switch (c) { - case '*': - str ~= "[^/]*"; - break; - case '.': - str ~= "\\."; - break; - case '?': - str ~= "[^/]"; - break; - case '|': - str ~= "$|^"; - break; - case '+': - str ~= "\\+"; - break; - case ' ': - str ~= "\\s+"; - break; - case '/': - str ~= "\\/"; - break; - case '(': - str ~= "\\("; - break; - case ')': - str ~= "\\)"; - break; - default: - str ~= c; - break; - } - } - str ~= "$"; - return regex(str, "i"); -} - -// returns true if the network connection is available -bool testNetwork(Config cfg) -{ - // Use low level HTTP struct - auto http = HTTP(); - http.url = "https://login.microsoftonline.com"; - // DNS lookup timeout - http.dnsTimeout = (dur!"seconds"(cfg.getValueLong("dns_timeout"))); - // Timeout for connecting - http.connectTimeout = (dur!"seconds"(cfg.getValueLong("connect_timeout"))); - // Data Timeout for HTTPS connections - http.dataTimeout = (dur!"seconds"(cfg.getValueLong("data_timeout"))); - // maximum time any operation is allowed to take - // This includes dns resolution, connecting, data transfer, etc. - http.operationTimeout = (dur!"seconds"(cfg.getValueLong("operation_timeout"))); - // What IP protocol version should be used when using Curl - IPv4 & IPv6, IPv4 or IPv6 - http.handle.set(CurlOption.ipresolve,cfg.getValueLong("ip_protocol_version")); // 0 = IPv4 + IPv6, 1 = IPv4 Only, 2 = IPv6 Only - - // HTTP connection test method - http.method = HTTP.Method.head; - // Attempt to contact the Microsoft Online Service - try { - log.vdebug("Attempting to contact online service"); - http.perform(); - log.vdebug("Shutting down HTTP engine as successfully reached OneDrive Online Service"); - http.shutdown(); - return true; - } catch (SocketException e) { - // Socket issue - log.vdebug("HTTP Socket Issue"); - log.error("Cannot connect to Microsoft OneDrive Service - Socket Issue"); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return false; - } catch (CurlException e) { - // No network connection to OneDrive Service - log.vdebug("No Network Connection"); - log.error("Cannot connect to Microsoft OneDrive Service - Network Connection Issue"); - displayOneDriveErrorMessage(e.msg, getFunctionName!({})); - return false; - } -} - -// Can we read the file - as a permissions issue or file corruption will cause a failure -// https://github.com/abraunegg/onedrive/issues/113 -// returns true if file can be accessed -bool readLocalFile(string path) -{ - try { - // attempt to read up to the first 1 byte of the file - // validates we can 'read' the file based on file permissions - read(path,1); - } catch (std.file.FileException e) { - // unable to read the new local file - displayFileSystemErrorMessage(e.msg, getFunctionName!({})); - return false; - } - return true; -} - -// calls globMatch for each string in pattern separated by '|' -bool multiGlobMatch(const(char)[] path, const(char)[] pattern) -{ - foreach (glob; pattern.split('|')) { - if (globMatch!(std.path.CaseSensitive.yes)(path, glob)) { - return true; - } - } - return false; -} - -bool isValidName(string path) -{ - // Restriction and limitations about windows naming files - // https://msdn.microsoft.com/en-us/library/aa365247 - // https://support.microsoft.com/en-us/help/3125202/restrictions-and-limitations-when-you-sync-files-and-folders - - // allow root item - if (path == ".") { - return true; - } - - bool matched = true; - string itemName = baseName(path); - - auto invalidNameReg = - ctRegex!( - // Leading whitespace and trailing whitespace/dot - `^\s.*|^.*[\s\.]$|` ~ - // Invalid characters - `.*[<>:"\|\?*/\\].*|` ~ - // Reserved device name and trailing .~ - `(?:^CON|^PRN|^AUX|^NUL|^COM[0-9]|^LPT[0-9])(?:[.].+)?$` - ); - auto m = match(itemName, invalidNameReg); - matched = m.empty; - - // Additional explicit validation checks - if (itemName == ".lock") {matched = false;} - if (itemName == "desktop.ini") {matched = false;} - // _vti_ cannot appear anywhere in a file or folder name - if(canFind(itemName, "_vti_")){matched = false;} - // Item name cannot equal '~' - if (itemName == "~") {matched = false;} - - // return response - return matched; -} - -bool containsBadWhiteSpace(string path) -{ - // allow root item - if (path == ".") { - return true; - } - - // https://github.com/abraunegg/onedrive/issues/35 - // Issue #35 presented an interesting issue where the filename contained a newline item - // 'State-of-the-art, challenges, and open issues in the integration of Internet of'$'\n''Things and Cloud Computing.pdf' - // When the check to see if this file was present the GET request queries as follows: - // /v1.0/me/drive/root:/.%2FState-of-the-art%2C%20challenges%2C%20and%20open%20issues%20in%20the%20integration%20of%20Internet%20of%0AThings%20and%20Cloud%20Computing.pdf - // The '$'\n'' is translated to %0A which causes the OneDrive query to fail - // Check for the presence of '%0A' via regex - - string itemName = encodeComponent(baseName(path)); - auto invalidWhitespaceReg = - ctRegex!( - // Check for \n which is %0A when encoded - `%0A` - ); - auto m = match(itemName, invalidWhitespaceReg); - return m.empty; -} - -bool containsASCIIHTMLCodes(string path) -{ - // https://github.com/abraunegg/onedrive/issues/151 - // If a filename contains ASCII HTML codes, regardless of if it gets encoded, it generates an error - // Check if the filename contains an ASCII HTML code sequence - - auto invalidASCIICode = - ctRegex!( - // Check to see if &#XXXX is in the filename - `(?:&#|&#[0-9][0-9]|&#[0-9][0-9][0-9]|&#[0-9][0-9][0-9][0-9])` - ); - - auto m = match(path, invalidASCIICode); - return m.empty; -} - -// Parse and display error message received from OneDrive -void displayOneDriveErrorMessage(string message, string callingFunction) -{ - writeln(); - log.error("ERROR: Microsoft OneDrive API returned an error with the following message:"); - auto errorArray = splitLines(message); - log.error(" Error Message: ", errorArray[0]); - // Extract 'message' as the reason - JSONValue errorMessage = parseJSON(replace(message, errorArray[0], "")); - // extra debug - log.vdebug("Raw Error Data: ", message); - log.vdebug("JSON Message: ", errorMessage); - - // What is the reason for the error - if (errorMessage.type() == JSONType.object) { - // configure the error reason - string errorReason; - string requestDate; - string requestId; - - // set the reason for the error - try { - // Use error_description as reason - errorReason = errorMessage["error_description"].str; - } catch (JSONException e) { - // we dont want to do anything here - } - - // set the reason for the error - try { - // Use ["error"]["message"] as reason - errorReason = errorMessage["error"]["message"].str; - } catch (JSONException e) { - // we dont want to do anything here - } - - // Display the error reason - if (errorReason.startsWith(" currentTime - // display an information warning that there is a new release available - if (releaseGracePeriod.toUnixTime() > currentTime.toUnixTime()) { - // inside release grace period ... set flag to false - displayObsolete = false; - } else { - // outside grace period - displayObsolete = true; - } - } - - // display version response - writeln(); - if (!displayObsolete) { - // display the new version is available message - log.logAndNotify("INFO: A new onedrive client version is available. Please upgrade your client version when possible."); - } else { - // display the obsolete message - log.logAndNotify("WARNING: Your onedrive client version is now obsolete and unsupported. Please upgrade your client version."); - } - log.log("Current Application Version: ", applicationVersion); - log.log("Version Available: ", latestVersion); - writeln(); - } - } -} - -// Unit Tests -unittest -{ - assert(multiGlobMatch(".hidden", ".*")); - assert(multiGlobMatch(".hidden", "file|.*")); - assert(!multiGlobMatch("foo.bar", "foo|bar")); - // that should detect invalid file/directory name. - assert(isValidName(".")); - assert(isValidName("./general.file")); - assert(!isValidName("./ leading_white_space")); - assert(!isValidName("./trailing_white_space ")); - assert(!isValidName("./trailing_dot.")); - assert(!isValidName("./includesin the path")); - assert(!isValidName("./includes:in the path")); - assert(!isValidName(`./includes"in the path`)); - assert(!isValidName("./includes|in the path")); - assert(!isValidName("./includes?in the path")); - assert(!isValidName("./includes*in the path")); - assert(!isValidName("./includes / in the path")); - assert(!isValidName(`./includes\ in the path`)); - assert(!isValidName(`./includes\\ in the path`)); - assert(!isValidName(`./includes\\\\ in the path`)); - assert(!isValidName("./includes\\ in the path")); - assert(!isValidName("./includes\\\\ in the path")); - assert(!isValidName("./CON")); - assert(!isValidName("./CON.text")); - assert(!isValidName("./PRN")); - assert(!isValidName("./AUX")); - assert(!isValidName("./NUL")); - assert(!isValidName("./COM0")); - assert(!isValidName("./COM1")); - assert(!isValidName("./COM2")); - assert(!isValidName("./COM3")); - assert(!isValidName("./COM4")); - assert(!isValidName("./COM5")); - assert(!isValidName("./COM6")); - assert(!isValidName("./COM7")); - assert(!isValidName("./COM8")); - assert(!isValidName("./COM9")); - assert(!isValidName("./LPT0")); - assert(!isValidName("./LPT1")); - assert(!isValidName("./LPT2")); - assert(!isValidName("./LPT3")); - assert(!isValidName("./LPT4")); - assert(!isValidName("./LPT5")); - assert(!isValidName("./LPT6")); - assert(!isValidName("./LPT7")); - assert(!isValidName("./LPT8")); - assert(!isValidName("./LPT9")); -} diff --git a/tests/bad-file-name.tar.xz b/tests/bad-file-name.tar.xz deleted file mode 100644 index 6092add7ab6b0736893718ef6da8428ee804bc2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 499880 zcmV(xKkT>vN;O9X~U8y@FpErQY= zeH8zpwDYg(enm+>L&-{sz7{lprq0hRH|_Ki3b(trS|F5P_2;1GrV z89M%lD1lv}aJ%}8-$8~#*R7dd)A<_r!nfszf|baB4Rm2voJ7)s6@)fHp0c#i-+aIS z5iKOz!jfRLlqYj-{&ocioD-ax*78@{fxA(WS8$#5&VhRg4A|E)EP|lwqQ}dopcRl5 z8Rh}wQ>V1RU@VJ#9m^mDid|rH8i;dKp2zXk&a_(fBpa!Z?`cK|e%+U;{>vgzgBq$u z$IIIzm0d@q8kweY{`^#IA4_%weS_F7rBQ`S?OIlzmzDx?6jnD5!RYu4;b(@|oYuvn zAodHd3RY@q2{r53czyaveFsqs5$XLWuj9P1Grh!tEK((V&X9ySWhDdL{i-vYoP~wl zPoAb@#kR)0vu1;@#B5|(cgJ*o-sFmy)F_FmK3Jm{**4wKiOS^O<*MqQRyQzX(b^OC zww7E`JCpm8;WJP+@oh`&e0zB#9d*4>BC=-K7)IUbTfJxI-yAT{mcZ&hm zm)lTQ`{aM8W5SFPaFQcq!qLG*(*uZ%g*Bddd6te3m8H1d4NBXy0fT-d$)5bOF0$;VWTxBvrm3n#u%!;TUaql3xl*|lwI+$D}kxRMIm>Dw;v zz>KgefxQ*?vmo!D{kC*R_2vTT^ZoUITaH0-vFv>rvl^`@bBtnzMX%~`H!5D2G*l+% z6`##Ei}j1w!Ofv#o_Dp4j?=$1Jd$+dndbY34zoICjZ=rgZGugw`R0df&BS7gX=LZd z=P>3kEsgRSX+)QAP{?|Td|SrVaCoRfKvLzVkEl?}O(CtF#qOxKk0vv<6$!l@DnYc) z{`JZDg#V;+T$@EUv@1xKO0@{!%Yltnm4{XQN^uW0Wc;l$VB`-B+=y^wzUuBS!udoG z{yGj|PtS8rjjKnU9TCeAU2D{F4u)wpPV(i02>;rW)WQ1?H^;m|{Lfk+gXFm~`6&V; zUwhLPv#M_3AbT8KbhZmkk35<6ysCSR`5b`oPHwX^{;i-Ff1PYP5@%VuUPL#wE4-iE zyg`wTrgo`uGW&FpsaP>WK;So*yg9OY=s2;UW9{HMBo&&imNzX7An!4%&@#m0oPr=5 zDiPfvhA@kh#clf#yt`d*_BjrAxk*sH#LnR(|n=_EPX1z@Hp^M zb%uQ?ONgFaTPQ^~uVAxRP4&A=!0iqJ<}XwDq#~vGb`WltNHQ(K%D%NrFtc^ILHhp9 zI|kQ<5+UR4R#u8U_gZL72<(VQ!g{}1zTJoqk%@yVjv4DtMfnd1CdfMxw%Z~K4`pp0 zpnz$bd0u=KX5nDE4fSjuHD4_(&*BsTMc3pwagVkAlK>+{+7>dw-6CaX5<2K!dER9z zqn_{|pczp~R>OJftl753Ft$Z2{GrhMp8FB5(6C%%hd5u-P+tPO-J!@6!nHHKH5uw& z!#zL|;<5&{rgm;kMQ-;uA1B6Qrcblvv0h;qUer1@ zi-#2siF-EU&R6IzzCZ?-KFtT7Y^S#`+Sl4!>l`QH?#QCQ-xBG2U8piGT-tTbNV^zg zO%caWBZjW!nJ-;`Mh@@3wiLWj4r7NdPNqK@C#Mn9!k$}GM}eRpFNJ#Nih!-21RR>; zVyoF+iI^mF!SASovadp?cec^a&__slO!t^fr&$Rg@VCv4IjY7@Z(-K2?9b#c!bTtc zE%m)AzFMErtjX}noDTW>theW`IDi&eUx~`#GHuQ8`Y`BBt82;;J&8ytY2==%VJE*v z*kj-{J=XnpneNflm%n3`oO$Ekt*}K5P|C(swy0wa_ce0SxOg(Z+@X0 z=1(MXUM2@FFDl$ed^`on52Ul5rJF%62A~}s$+!@jjF?Ae7sQZYy?wKaxFGX-Gbr6u z{w!DSLKZzo2$GWI8Ndro>3a^vy5SMNx@i4*N11hOh=X*1ZwJ@5KaI;iZI1Ty1KHN- zYgh<}?^-cgaH&W*7Jg!Lw^bfUTpVHkV`pUz?B?3ricJ?2o4h&=Pevg{2q@S%A%A}_ zyA1F3cOsTYb)<~JO|&9WrJZh^hRAQ90tBZJ3G%> zh{xGYXK54q&smV@`dsaQt=!JSE2pIt5pA5{)BxfkNAA~4X z&5i7RN*E#<#RN;ah#)AeI-&3@VenHujxppo@hivi(`E9k#NmDBrzt3n6(BuQ!}g%0yl{LP%kbb|;5o(i{&qBIrw#=u zRXH3adzxc?C?ROp`CXoN__*>zR!Q8~i<<1M8|%cPm{vL^ZCF`lfj=dA zE5?o+=GttVkDNpFZTEBF$QVnQA}+7vrdm_%#1#XkBA5pNcITxxC!1pj{yr89gd>0# zTc1sA%y?k(fFPJ*)O{#koAMMy{UL#LntB;)RPW{ZSg2HE}}w#<;h`iQbdYXJmwJgmisB%HcLT(orCe5yccCTkRDXQp41 zu^*;)gK%8qVH7&G=UUF9x%g-AbBr)JB3c+JOVsQ;ndH6oO$h2$ihp-F3wuzN6 z_fCK}qQlfKQ+-r7asp^4on>bpbv18ZZ&a?TB{AIdNAEte?0c+b)**P+6@~RRO(`Ix zAB0{jd<LpWvvGT;Y$O&M@t@ZjyK>1!3JQg!cq;lh2pUy z#F=eM9Hf3ixs)=lt_ypstr72@Lay~H;kK!sV~ENK7i5|$n_^_cQ&IQ4HtrG zNFJm{MF2Cv%j*Fp{)|BkFjIXRdq5}g)wZKQu&^VxBll3|j5aM_eh2C1V1v*?ma zxJLkj3^<(RkCRQ3xb)}UOAx=?%@K1O7a;@N|7G*zBu~*$pBO#0SmLfPcu|F{SX-M= zAdvWIN~T`adu4ffjxu1>pMPCzj!tf%E>&Qpv^zV0OZd3xGV;Wer~zbOYST#odJTuD zFr3Fo!H18$;qN|}7B3DQ0#!$Ksvz9r&H)XaP15kg2ESw>9lPo+!H9#BJ3oiAIiO6H z;u&;S0;C)LP}_ItRmcroypXxNU}Q&s$d4I{fez=0$NO!LMO0B!J?Crd0OC;}zjn_rXwgNmD(|eHByWSewgHl6#UYWEn_SPvv;w^fewkzEl^t-q-&K|a0i^|XFjr$pN^sTULQ^<_D5|9;^(cQ@ zA(chr?Ln)zVG2+}t2q?-YgaL3oyWm5`{}A`Su2-{p|`^Yb_7HxHK)lI)e?zr>{vtZ zv`ybFuz%ca-1gvvm3#mmv*bfN$Fjtj!^YQ7(~iC6rikWP88XYPjV0JcAka;C3;FH>y+_ zlLUYXOzvXriO%O|GR3N7mpWClxocR>QHeAHJH@nCf-XEeccH zBu;lF&z73w!qY)diCo24F(=AGGkRsyJ%yb1K%qj|;bN35KXVP0$t3``R5S^Rk|mSc zE27Wz(Np`;)}mJBW&FQWMM7&YyUxCzG2pOw)Icu!JU=WNYcV!&XCujD&&i$@Pj%`` zCs&MP@wnOaQKzC57#|0l4LEI57uV$kT5lS~uV|T_cu!J087m1!2vZqj`U}voZs%AJ6#h5?op1OnkvICtjt(cno7Od z>tCy#4c&W-lknry|FwLA9oX5R_rGE-rdY||3Huhp3L>L{N)bWpEugm~P-gmy9|EQ~ zvD$4}uk_vSrH)-1gK=f7BTzVO1ibSb&svzV#aek#f~!!$a; zjH)9))S@`PY@dOE0VecG`i2RSl{|OA4JQt%O3_af(0s;oYDpMZUgz_-Dik@D@j z8n^o52J^|H?C8Nn%3o(6G$%_TQC3k7- z5PbLsu#zXVZLESGZgJjC`DEnEB#d3RGOt_|=`QHQozV7}&e%^o6+`0jpKDe;*RoKQ zE^S-0kMc5Zz<)!8QyT4D^!1Mb)~|2A2WEEGrZ_UzpMqING^(vqYa2a&(%Vs&?rqK-f@zcWi%& zK^Kgk5HNviR2xjCWOJKpXBzhVS($*K_1KC&@cDw~BE4M$K&ITFsg+6&)Cxgryit5p z7hI?Tx70u=t`fhn>TMGsMG!F7^}#Bgu_?c2aUu-c23!&zH+L_eU8e+jyPvroF5+Lm z%P_JCdh~nwo-?Ta1b!Ane~LQ+hUVDvG%LUN#Lpx^syCR7p=PhuuQg9dF@wSae42hmYNfOwcIl!hv6bLOTrmWRu(r?G?5oJurg96uK+z(Y+VOtb+dVw| z#cr+P!@5io@X^nV*0hD>`Poa0GzqYJ{-D)`gK)2F0lF{%6!%Pm$Uhj%T5RUeT0K?CEbswz9vaZqCj!`}=4G=#4`PW8&Y$=0K6@X(5 zvJgx>!~^yiwR}MjT9xhk*)#^Fkx@!!`xu|?s@U@12MN^JVFzl}o?wE+28oA4{*H8z zMrAMYUbE|8I7Dk!Rw4!G5VJUZ+|5_AqD}|M?McdnCZliv2w}66V6w6sIlqMMQPL6b zfwem(F4?kHJ*^nBNH!T3KS(#VRqb{WcI)oz%HbqAD`HW)vQJE;!<@{#MnV19j%)No zgKjzBIF4m$B+n`n4mI;0%S!)aGlY+t6AtpQTXvjk9@-grf7-0WQX{&eB!Zpg!vVFi z_dBbq&7*itcRRt^kD6EdytA~kk$We{v|{@c&RN+D0vIeCQBg=SRK{%)=dec*wJEK2 z>_|Q)#~B=e5i#&isu}NYR-0Zq*IarA5>EW{N|Q8N+;vUUB#6boWKhs%OfwQJI3(67 zV`i1{>oSR_2GmACk4ghJFu9q$6A%gw)E6BP;pxmgZ2g*ZBbx+4JRs*f(8d-hnv zqAB{5pX?=Qfrr?5QC%cDx?#Ge_`}27?EzvJjd$#L8T84Rq18g<597vFO8ixcj8egW z?-WAy>YvdEx~J`tY}yhPb!&!fr?5b0Pk9o&Q~=&za3exa4rsQt4BuLfxe+V#3K@_G z2X+HpJ~r0y6@q<0nV?K;7tPwjq5j1@M4IheqF(rxdyNiba8tr;|k!2)tf9p@mo%}$Lu zW4^dBAr|R53!oQ-e_|@KUG?9Ff}*&_)X>*MG+fei&X+qGKE(8oN+!2na~M8Ts7iv_ z0r_dWw~`_Vy4&2h;AMI)+nq{rah6ODgIs}iSawH4vl~vCVFmE#a~V8R2hqcb98Qeu zICBz#t!-(_1R2XkEt`TgKEz&`8@XFDXHR0kUGsCRJ^0kvNE6+t))MUz2`ufQjKxdi z3eF#Xx6wR^B};AI0)N2z=rR08r~+c4dA;n&^z+5y>wkt#89*ZF;6mf8Q(_P?FsPI? z)Xidh*lG0PA|66>ibFtgWW= zxKyfM%o}1uPQiUs^1WRkxCNE;L!8R)LywIl-%^{;w{9ix#@QAK%n|d;sdi_cwL5USDfif`EDGZGsYmlj==Wy*FbN{KM1+M?AZSL`PzA})A@qUQ#F1NYqpUqIdN3m=( zHxpesEkNyu(w;wO0!<1}_6U^0ayN544|r!s#!KKB*l=8~%!T~r79|Aora9mm;M`HP z@!!e^38f-dd=s*MTJ0A9n2b};a&)7X zD}H4^vjCAdV&%@lAk1dHvVbJ7&dHN^Yt|u-pRR)5I3aYcABbfkUiupCx@c&LWGaH6 zMk(t%N}>KIvQ9za%iCldW6YB}kkO|Hzmxvut=kwTKXxFZ1AGwvib_^t!Z(>u92C{0 zR1@zU)2a_v4(t=%6>6sO?g54mLCMf#a=juPS+$2S{DNRhxoN`$yK_JkC?k{N5P7WJ zQ(i2WAt0Cog*_%~4!@OfVxgQ(kt@Vu`SJ5N764SMG9xjdq=>qkBqnqz?vb0!LFP%? zJyQgJ!G<{u&58L#m`YDN-T9#diINUJ5eP-g$;HZm=(3f(=okLO_-Q1LwkP+iBB>a9 zDKyX22d{Oe#G9Z0kIZu>gKTbGQ7P9j2qerjh`D>jojN1py`#+(nba?l!~VdxK_w zkvVIi(DXdgk4T5wbw>DPDG;YR8DfQ%Mh29H6h#_4h+QcBYo{Tf!0y^egw!a06;Ox89?r z{INK?vLUR8u_r0{I^bqFtIK1r?VAZ>Td5w~ncki%*H+_2y> z#`b6k;SBVvyL%-B>HI+Ls@TVH^m5)wCeD0kbm4b?wBy#W_e{OVDg2VTt3NOX zO_X)bO?HckJjl)+XKP-P%awh7Tj=l=&SZtcxkiNUM_}xULrY=5fO*((6Kqu~vUTXM zo1hJCuk2Qu8;iq)DF3-i4QoT!aqTj?X98ss#O@BVLrqZ68y?SntD9>oU?uV|PH6GyGR0GCqfymiW5;C~-ABIgar5ugssYSJ{N;~8Q-`Ql zA#<1K&JsrX`6Y9wL`ONMF#};MYoF(yc!QFTzi~aeZt4&i_ej0pVv|Q}Ce1jY)F&tU zXl_@@^OOxt*q1TLm84ixs^Y~p{9w<1e~lZ$KzuQ?e@TQRNd`hdebE{Ld+WvFkPBzF z$&qeemZH^KB8<1st;v}Wx5+L&EEvyrvRuFS6wxZrXCm@I&rweyx;m|d?D&J(gXhAU z+UFSM#fU?)OgwO=QbHXYIk)nd?#FygLQ;63w5}0Y>SJf{HCGDfba&8LD1ZV-CwY7A z?YcOl>X|tbVSso_8Iw;b)l<7}m(;|!%@VAxiQ7?lW&u&e$#7LlK5eCjgC=S}5R=MP zj1Z?$ujlX(Zy@3w@Eym9Vo3yZcv8b^_}(|NGVHA~xa_&S-DA7=9`-P$X(lIfRRFbR z>Oac-SXr5eZ=1S!j>?JE)3QJP>~N-7iu_sKfr!uOjQZb*CTIE-$z^Qvn8>-lGntoo z#{TEQ>=CX5;!Kmx>H^tIn4|A*A@cX1$qTpV0a~p=T+na9nn&4J65sL-FKx!{!}DMN z57UjaObfge%MhpWQ$WwP&J<*6R;~?HN4?*ZE@w;Fc2=Q4dXJ>(CIU4uEFNn%%g7RT zc|x5xc>(~gZ)3$s^)mXSeY?gP+SQMtpE)2sJk@laTM#z*)3&D|t!@oyz_}N)T6`qO z6T8LGi?;;YzV2HPBiM9EZmBj_k!PUQO(+Hi(0N5Q@Mg~=NlR? z7P<|Ey2=n-VQ*f9z^{&nT#%|nOA}7Zq)h{ITX;oKe%wDmKYY;FyrcwmY2h_`yk}j? zvn-HOoke&`Acw5%8=^2LoYt42vIDPjSTSH+ZO2QMN@mb++5fO|@Z)2ZqdPw}YX(jVZVnQLdJeRn^D5o5IDlfW-d9e@LTqNj0w zBSRM1V?i3+0QJ?3DDxr&6PTK@MWQuWY?#U9aY3){lw>M~RT+^Ht zhIt~eB&*(XXo9E@#WJcgS^B4^PU7SEdY)W#I`i0vuyZ}e*JbTd){$RZzV6kTA zvhzb#q_Nh$A9J*w?dq=nP4nF<#u z4NWHx*^XcQbGj6MV}UHfNnt72WJrJ!lh0qBl$-*(%{W2r9Dy%01s0Kv>(VmSY59fI z^nM&cl{iUbTm{(mjnHXuup~;w8E7BI@V1u9!6o!gL?ggN-lzYC5W*srIfd&+;WQkd zLe9|`v3+l_Rzh@hN=;RkOaLW`%&90G^2Y&OSZg~IPy7+_(DTD5h^Jt<0gaoT$=Tzk z`0@@YSKvA^!|rkju?$rWsRJ5w&T+0ZaFgZgJ197ZJOCX=!=N?6Oyv$o^<;B-i0a{i z$mO6Eh*WHkM}ZS~4I}NtFoP`@m&95y5#i2AKOV!--CW77+EV_h9csCJ#zc4eby5I* z?G6#k?*Y@yKU4ohK^OLVUz1ze{tC0SCD~Eq$c0RsjXagJ2AS&~(uLAGe(V>*`RY#* z8oi+r;8`bV(POiB-A^aEH}82L6#>+pwd!|%yY@la&lUxK=CQWT@A)1(q=oQ-Ec&yuLy4Gv-EkH#Rnopgy}kw5tDq?zkExt!+at zZA@D3sCVLIeAK)(MGl56d2X+E^{uvy((3y(x57Z!?`QhRsPkiCv^Yb(oqk4wSG+1N z6R8$LyuX~^x&F9{6N_H5^R}!K%=apcw-h;7)}%6dqfKXDdy{%pCihm_OrP%*+q)}# zE*xvO)eY5PAQ9w5ZP^TOqYAm05AK^x6nH-}z%5%}OocLk$(uLpaFn-A=s^HD*R~&u zZ+lFV?Szgah^}B|+Q%QB%?U5A5vjgj#2m3vP-%@vN*xPHZWOzrIlKs6lH} zOCOx1&+1PN4hai-7r=xIbR*q~--2LRkLJ-x^foFEj zuKVs~7Iv7pat;`42y!MVKst1WVjUFQD^w!dAle`#47=ovskUFy{VJZRE7+D7=lDkY zzaO|4WDOY8eX4_qJMC(6Zd3Q-T`pkQj)-9V)& zm0XWKS#;hYr0*XE#vUrHwHnTSNhlU-hhkT9w-wW~ns52hyHx|GZ_6I`I*m>~{r6T) zAh^zbAY;rsXLxlo{>5uV(ss=ssw^>q?WkuoTHKGnhpnf1M`3KZfo1McDw-UEIO-wT zhYl0Q$dFJ1|8M@|pf+8`CH3@KyVc#{2J&xWvOEl{m>)ILf4je4SHt~7lUOQ1Ofd59 zvdKrao3HTkH!0xbJ+AzLAK(7>b(v3+%^bwyzd~^N?G6i!yGF4@e@mQvt)Z8t7nVof zDcd(9s{t4!$$F&NhrdK63Q9vUz?S-_weu#h7WyLjP)xLfXrvRS;oJf==U=0YTk`<) z3x6J?ZN0IhcALkf)|opdzyLjm2TpW~D@tyrtYgO>v|unN<5y`cRH()w{l?15!{x0Q zHI3rQDY90a5Ta3@*yuL^DfJ&jZ-W&5@7PpDjnzcIhLBwJ*h;l-R(#EKjv4`T#zz{M z00>N*8>NwLJ_?#i;?q#v1QLehqc+ksUGdM@M%cv35Pq0P2A;Wi1s!SB&1frJsF;Ng zuBd6uhOVgkbY2DC2Tp^TBeGG}Z}?cJ={!J303SmbZ;)1znJLcrF^a|M>>%-dH7coM zPL-y<>`7gxXamhAW-3{&j@a$Q(U*p`EY4w5lt}BNPi79~kGTNC-~zslmEE>5;$BJ+ zS=XSEV46rhkxOWYQd#Q=c7YTzj8B*Kv0_2oqYPM&8nlRE8ffd%aZEa7vvL#33c-S) z+ow&*=D}l!tw5Ug7k)vP=zZUsQ%3C>`=|znxf=Qz)Dcv}#N3XdXAFglV?xbNdWQZxg?H@d2jx3gNVtvhj+6$a3UxkQ^!CDxT$o~Om~aDO&!?4U4;!ta|}qM zdQ&2HM}ilc2l+8W7d`F=M$}2fm;0j~sgXQwe-0Y4`eCO&8S8}}F{NDa>i*Zxy9_$# zABo5s-{!g8`zuIY+sawwOq~3%G_0Nr)Y2%_N(jYe@Vd6>Y7?@j11P`rpH*Zgo%uK1 z?}m6vFp+U+CUjff^Lqp5>B@lWaR{I_XgR`Sn9_Jb?2weJaiYE%pfs=E=XSjTZgy@! zws_AJ4+nZ#yw!z(2ZFVvia|1d?lCrJX;kGtB(8p=|ER9iuIO**A*hFy5)iA>PnNmD z1twSBdkk(Y<>BbSQsJn^?+KwEBTTY5h<<3>A8Yj9Tec*aOeSqF$|$|b%9xf{6Ndq$ zkcGr}udXMdlQi}|kRv%;!!nWjvAM`IZYL3ZEq)AB)qB&~R{^2VGxJfaoiR-yk|~93 zZkfeJkK#3u&!Xg8tF;pBS6)8lvW%UNq|W3i>cA5k<;!MZm$D5`_zN(JVJ#2FOQTzC zAOapRQv+WCkv>)=xJT;t!67T*zt^;#6eW#zcmr;V_*^W&dphYx+r9wL559j)!qDqU z%H;F=Rh1eX25pEP!s_8m_eMKocjrPbjXLyYRl&-1 zEMWAQ4ec0Ab{ACdU~Vp*&4q`{F5q-6$p9+H$2AU)gl<)K3 zi}i)isFF#6sxva#m*40d+Q1DcZv*;wf30+;Fj=2i@AK#LlCwBJazjapgon8d-dpVS zi&dL{UUIB>*;ouc$g7> z8p69>btpNpe8oP*WYLBMcX?eVd1usrv3BY|PWO zlD_RfrpQlq;e}9`^*O^L6rn;ddmx_MYf*nKbT8h4M;bs#Uriqq!90E%q9!}pJgVxV z&vQ!^7BcK?h9Tw0xU=C$g{LP)A<}<-+pQhh|B}97=|CySp5oQUIF+6I@_Q)Mxl~A# zT41W20EsIHe0f3*lyo-q)lFY`16rQm4M=DtiF1?>lQSqyp}V4D@U8}a2tj5@@Ajge zSn6qHEfi0{O-v`-9D6Fs;PVMpK7z8W*pm%H;w;aX-2^H_g04I7p^%P=@6TZqucq|- zsDDA3QN>W!w+bYE;KOPzc10T$ba3X%9J8exPFONk6(_pVkSd7VjpFAVEWp^IDf6Oe zgVr^CYJDM>+kMw`jIg-B0d22>-V0%S^E(2SYcbStyXOEjvDfzUx9-AGSY;6O5riRL zzlBQVfTo5IL7~8_1W&VWFVx?1`JW|{CYuFYP~=F?j?v@gmf3w>LUf!5j%QVYl+ZEz zxa>(_;}Q)o5cQ>W@^&%AVpOvqKiolBw-EuF|5$f1p8U9FN5U9jwryO_(e%Q805b71r{` z6NZW6-8MbT%38Q14t-%=I0AtszwIgvP`?LpJKhki^B{iJzJY@-(1Kh0>V zs!gTy}5*!msS=uqoW z9~tuAr%DsB4KIGuU@7k6H|y{D;24Zg;b({qS}ETJ95QsgDQ^0UE6D4?<`|?G*HJ9w z`Vp=T96botN%x^9&U7=Kf^O)$(6IFf$lfb-n(L9Yc=9c70y$JK*SIJ(kwkyCaO-TS zo5+kMKGch(dKWnEmzNqrJ{$kuAKH|1891A2!2e%#qH~f?W54ZGuUrk6!uHP5CcWvo zd(k33bo-yM9Jw4EJe?EuQg{FCo^n|yQil@7Qf{awOtl$Wk7O-s2s&o}RH~Gt!|VK_ zg79{RQk8WbVimshf&9PmJRXwhzwAtjFU@#KeZ7u8?IG=2#?V!N@HX~71aGW$H5Qv3 z9M(e}xb@aZxdER3gef^c?;Id|RcQ__lp;0HEDSpJGs>bDA0U8vaGc-grP@%A zFscLyK;JuAg0i^g65i{iIXD9wzdj?GlY|F7X5ybDX_KhOzL-NbP`M8vpr~H=DfP0BWiSc_v||FgxPScc%z4uTWDdU5yFZg?@TZE9nk8za}~0DJev zkLp=sW2>W!Na8qQy|t#FqoAJ7^#~NOmHL%%eQ0?wuayD$l{PRLB7nPvX82W{n6m3^ z4sEUQVL&z$?TiIbCjhOjEqkOdCckNC^VEkw`|63Uf8%1`#!$tWrAF4doW8E4WEWjN zlCG?vCoGB8{@ZBiEdpjmxHwK#>eTmT%okisEY09~*e(0pJG}oB^xcc;i{=6eRpB=0 z9n6zM=n(SNj0&<`7T3B04?Z&haLE*|@^U#bk}ckG$5Y+Z|IX3}id;|Fe>gKO(x1Pn zK=WU~OH~sHwjTFWEI%zP_?6z~1O_C9`Q~`jiH1*yqQ-)thQeDITgEl0${Zm?Qqkk} z&ShG_GK{KTJ&FNR(zp^JNHrfU7AkEbx%fx%Lh{)(3Fxf9Z9SmDbpY=kg|@*mShU2` zJ@VhY^ceAouRqSN5>Aw?UMY1_=?UV6bx2L7Gp#2_Ofs(H~4;u!_|42-KbdO1io{i#I0>E2TO9tO(cocG`f zf3iymF1b0+{$;mJc_*K}E2~^G9XMcgq#{pi%@1UP7HoxZ*SB`R@O^twTg zg$|6xJD6YNy|7N?69Vyy=4ChnxxsG4$$Er`Jov_iyA3c?u$o!7GIO9EQ%s7@`))`` z*Y)sd3R)8{PX({@XT2ip)T+_$5~AS=mBP(kZ+c1blLg+PU%4)7UsS(%I(YNKcp>n= zK%?g8tB+sQE{8Hx(qm3N?qc`nsa5!15#UX)5~v74NJzCDffHpO%)s{%N)cUT*#sAm zpl(i01zK*F49Cb-CqHm%y?`s=ESg2%m$T7Byi?gZDYxIWk8FZjR%2jJnp&Hw1@oM zO}N9@i{_gfb#gPwNiN(1PV;Yi=l@xIY zCBg8k<$nqbvIp-D8j~yLU_tkL==#M9FN9f2(4egpWCtQ(&EaPA+cOqBA@z8%d~Y+g zGV?rI&n4kBCA>2A2p>MVJ5@2um#(b@AUbrln^7KnnwUn6y~Jfe+&IJk4r7iQu3iKf zYjJFr8*NN$;oTN#%W`F|w9@pMXhkFq{gxUPZ*I~(_)J_TLiObHm5KJ45oMF_a6!Ax zI%WQaheT;k9IQ5AeZej5%=Ww67elI`QQFRed8Q94z_>k+LZad)t?&8bwsh0(EAz1a zOcpeVX|UxN<0EXmen5sJD9&0H~M5z`xTDqJSrsj z3a62laLeWh-08_Lh1PGbQ-i^tMS6%v;!SU-s!U*9Eu- zC{_yz{nriV_^jsh-cQhBY}_ui#cdtCgmjy!WSS4)XG&Tp9T}j=TAhO=bih*yV0&$t z*I8{)h0bar$1cH_l7k!}W1m;QF>{9b2|Pd&-VJ&4o4RBc!eP`9!2petjr5|C!fTbad1pflOV^-`{uB0o({)vgUQQ)NdE9S zFe^Z@Ur6RoV|1y}`ir5cO@tk*s@nG(oPzfVFbImCyKd|*bTERZRlJ3v1rP)}#bWkY zkftcpQE4pQw)(gz@B^t%k}i;)y0pl1e$V+JsA?BQ>otd2NCz_Vi7JA@(9B!pVI+-2444b8JRZH%y07(#xsARo994XGYrEu_9W8M8=f3@u9btGUVeYmXdUhe=MM9@tW z9gSZ4%ui!Y8I}0InZyvxaUS3>X2)B`JktEOt&og=@DP(u_sifz_4xz#U4B z@f(Z!f|R`uSa1A1Gl9#*xcm>_T97w+iB2RQP>+M`EWmtzjlduGdJV5R3{+idvS$k~ zHqh8mkMK>Mug`XR!RPWi?f;A*R7!X$136(FeDLl<4^!C&si>jDETZRfcK2qCDo1u3 z-YG=FzHFw!Dmf#U2~v+a-^o5hLXzh!@jbdG?evxX8OZ>C_wnN_)by1ha-sZGk(30P z@g-4H3JOb&w^9;6MLn%iYo``UotnOtI?^6Bhlp^VpuhihXvwu0-3-Zz%YT!aIs;}b{33i3O7TzYz+R^yx!ALkDW_wsP%9hrNJ%WuMg-x`~W20)%Tz^~_5iD6!0Z_78rIq~;I*Jy; zkM_VKN$|C^Gb(Jh2juyy;RYk2|03fcj~2lEGVfWK{&fNk#VkUrRd*IXDw8!%)@Y&< zr;AMS*G#O*=WPz95^AC9eaJu1IS)fo?tUvsuvybYa4;>(s0x_v2zrz4cG_9K?Dq-a zyfyXEg6Ovw^3qczP*BF7Uyn4mTAvoN?D6Y zcWzu=_Kd{N+KfgsmI_d4P;I^4neR?m&hk(%N&=z7Zak`+Fc>`*FdA9}UgM_V2R;JD zH|Ejj_7DUE52w#)Z-DM14ySHEv?+d5LtNR91S>p}pZhV^G54f`olUX2FW5XQ9)@Y5 zoq5boW-ci32L2yY*@x=fo$V}B!upJoE67URCea9#km)2P6Hdur6ao52P6hRc%xOF~ zz4v`8PoSvg*@s|K69B5(&;sp&?#jYf$txFHc;y#IYb; zWqyWTLZ129fH;W!nzz;%;09boItf*vzZvRal8J1wFxdb>(JcGDXyQNWrx75-P($i6 z+6MJW%8ig8Bg*OfcE`FB+&FvA>)Ix{6swZmBk+-W&8)&=#b$P|3D<~(=q29Y zq!|$l2(U1$g^%!Lt#`!f_ban1IUTg^uDzi$^n$a!8K;nG4*Gpu&0ObZy$y@NFNG;k zbR~ExM{X}#^8d3yYsNNPHGO^#E!u^Ck%lYBZAfSA%S;4#&5^|@E#o~jfHbc9U%$C? z8w2~21g~EB!8i+a=>Es5FBuf#`$@J>wD<_&Xo}ggx&fHJx<|3(#0HfLdrel`E0vYK zsFA-Ak=ISi+F#N^cKXUYp42qY5U=J<9424)h#wAeqtBnK!yYXcmy7x(0|0`I7B(AJ zlU~Q0d@%4RCNZ1DaYD%Jy?#!cgt6wk^6b9h4_fVC&RMTrGdChiDq*$SM}Mx^9)j*= z4PL$hhif$l12lC>@5PXiP~S)f-vlwcNF+N9dd#D4r9VMk4_gS0@88sFf5B@))}{i( zn*Sxpp}A3Atb0j3nzv$ws8S=QS|_i5)ytc)pR$~+#)WO2{vI50*T=)+@t;MnggixE zAX{5RJ=6}33T0C5n`~2?KbAm9V?vT#nqed+##1bz%y5gjroel-Pb6Mcr6zhD&?27M z(3%;HVuE`9-33G4sCrQKk&KV6#>#3_*IeBrgp4r$tq)))L4cng@5)3bbDw1K2#O zxnQNf^)i5QVo{J^{}5oL^82D*E>;9b^+372F^aiWEJPj5S%x?E{ZKxuu85PVkARA0 zB@QgmTJsERoboi}5@vOEd)QEM+U(3S|29WqoErxTA=!Z9N#Nwa6Zm*JQP!wPex~WO zyuLKXfhx`-94TQsudd2tww9AX<7KUPHKn^Fbin+0&(1@3KY}^pvh$Kr!7a^+d43lv zF!_M|ES-!S8G4_)h%WMMWMZ!`6E@vrfseWQN5G$7WG?_OK+wPK9rhjE#t~z{9BaH- zM%=?Dlg`2@UjG4>iPCS!n@YH&f!o;yGjT7++?}J{ic^$FM!T)WQ&do6z(8~=as~m~ zNmVhl9?^o5?@9n?5PIMzN0X$6vPe%0ZbbG<5o9{T=)2@SCM-uPKv@oH{oX8~`-P?> z$_~K|ulcwJr*fd=XKGH4GuQ0Wk`vid;!<8g3au1@x?mMXqYcN1vfIy zvEQNePcN4t)ilP{&Uyi}7_aaJ!;1$j5ZyAnkL_ueKl^9g3EoMn4wRa99*fQE`?OGD zDd7VtqG)qW9{1APNTKHnAX9kP)SuBEC1h*nuE^TDyFdf9r2Jw-%(#Og{6&TGM0{(U zvTWss_P!v9WSDe`!)R$6@h$bR`-*;x6CrZ;LGX>vCKFLx@J3=*)4;pwaS2JiKHKMb z*Y|Uc8inv6BMn6i9uCuXX#yh$#z5B1_tqWDql)i%gmRIkrd+V?{sE#je~)|;cAzXd z914f6h+?qL<8jhpPr6$Vr5A--q#V~|a-z~awHzo@o%=NRJy`=$d`u9e!|jL}Ug)7X(Qjz||8=#mPXo)qa&ilMk$Xn}5c^Z0SR{CwBhc98 z-T^dVbQ7h`VY(3sHH~z&Ise)jxMbqs3G}d5i6j2Ke4R-PV9zcmMO&? zXxB4sC9B}_9?Er+)|0Xp3b0-~(8ie4kv9uOR4@(@0HFP(A!12QBw_G8R%8+_zHZjV8|S$z z8dl6DW!5u-8$tJCglsM)}Q$IDzfSn$@VzeaAA8h}$%pgn-Cum}<;8Y}rPZHL+Sp zXK8vDOmu`@ONBN=derlG=7*}ZN!1=cPS z!yR*(OOP4)c%Qe!W|--TVK>8HQK`!HUNP3@icYp6NQqyY1bxaOz!XsaE}Py`(G5`A zFaW7_npnAHD}*KEx)G7w^X7~}kIj5%=hOjSU0_6UPRY(2qo@T`mra^dwH#U5$y1_c#8oMvl>8PVMkVFQ*=OJD!2X|5=3OO^V8&bR_sK7aW~UP zkM>~*9$$ROJfJ1s4GG)jxnh zN4pzW!$2|*KD{(Wf|`WK0$75zyEkZV6baTa@wKq!wrQ{X^d4Y*Id40B7bgFOLn>(IPNLiDC|Cx-^lb1d4uM1NG(9wVm)5 zP@!y*Z59=PJ7SwH1^)C9cdiBKn@Msx&V5x~MK5=n{a@U%^e1_-+HnH1Wnh+n>QV0GP)KO8p)sqBAk1{Mc zz&Kmt7tOANoV>G?g3^HIg6dQ+^GNicz5^)%4-ULWp zrF-<%zNR*loR9VTh9Mo{{GB43j_cfZRPZ5lPahVsF&MVEt>zaiPtEcO%3uor>c>J6 z^0@ms|Npa~bN|pw_Tc)l{H%%7kiU)6tjrrYgGriqk&9;AL`B|kT_3CreU@+BmPf?- zBJ2rdksJ^oBn^wvw}2g|SzHg)@4kckl(wt3+_A(I7_D=9@B0S!)v*fMDwAO$L?nxNU+eEz`H(&8H7 zc_4MCk1)IfgptRLI+Kpy`RSZ@?)a&YhY4lhcHnmOy#qhodoJQVAqb&;Uxe85+)gwG zn2G79SdvciXOYtQ?;+KV7-9}KYt~!4mpS{*ru@dcZ9B~8Vrw^;KjoUPWUk+U@=%bd zF6hO$?p2ji4OOMOe-oqf_)}sN3vRK0Ic^6-M^W1&N;3?E40P^PhJ+7VZLWRu?|%pr zq`*fimpY3c)&myzausOP@%6F--8M=p@>BvD_@76pPlE7ch4-#(IVEvWE;1r-1rKX2 zv8L{2Z5ty6E{#8DPQ)eh`_6ouNTPNhFeuQ<9%uj22l69CiewY3(&qmYjFX&kRU=nE znp@(_k_RX7gF>h)#}GjW4Y1#u@L36fm%QhpAM1wtU76xy!R+n3Wg?13Jbg@`HbM?e zL8>UHm0tj5VKwB$4v`MPmAw;Gei3JyH1Ccxh7bfI_^CCRUd2*YiT#H|Xd3XD?fP-; zVq#URG_81UH{QNrqK43dtF>ObQ;eQs5j)n@xqraG00SvtGan-P{G+3U*vm4AG*xDB zc{ow?BCC9I{OHhZ>`OQ$8B1uVSShJWry~bMGq!NDzqj;MM)>dON8Y4vA^R)+fTjJ} zzu%yV{yP^r)2*1Co0%I!9M>g!X0EM}1qB4tXv<5nxssf#ZGcQW0=zHjE?+pl$m ztzuaLf%=bKf!PQ#T0arw8VY=Z%o0H`>4-ypjfs=p4#>-}Tf&21ccMlx?@Nx*QI;$B4dr1O{da8B~Ca=pQQeq z(dywJo^7M(z!M&}nR1%VCrJvot+|Fs?p zweRBP0HLvrjY`DZH9O6^L6~D0U6lZ557W$bpz()ADYri4E(%+jpoo(Er>0Z##(0djQV++@kogdM5j_5=oAbCRThInn6vQ<7YExT^3bP~W>AVf1Y3 z%G|uN4GIAI&D7h7Ku?D>wF$_$Wyz0B!?)6ac3`y(x&t@^ap`dCz|n{z4T4?Jwhd`) zz%pmm)%PgJfg6@w*VYiiajHEey%^O&i@5Y@CtKm4k78f>`;1 zNK+6yi1WXCt}uV@3%w>tRYuSRe*Ji~tbrp)1}2Fj`Hn#TFwbI}tg_{r=_C58Xjo88 zs^0oaSsR$gFYGNcNWIcCl9uw) znU_Jz(q!L1v4%0yb)PenD^&nPs(bQ14EQe{Z@10EKW`T%jBQK<$%Gm=>~{UCmsQv- zeP3-EsT#_MvsxMfoG@g*sXrEV9&E3eh%x=MsHKfObVXo@+wpNr4B;|n1xz|hJem#Y zlJ1aqy)=s(4O&zq;C^qwg|1#zu#i)It>r?FCVsgo>%t)h6Cxhk=}P3%erpNDAQFhk z>14!Sd9kW>QzdX*1TJFigG;Oz4w5G-gQvw<&$Xwb52i%w_ksb%tY}?`t$gX=x$|a$ zUtfjpiY}4=!Jw@o1A_-vn~n*%azOchg*_L$@b!v9^Bq}HKc%Jrg|-l;4L!A7XwJ+A zTE9!ar5?_D6B1=P0`W&3)s6EjLnIa+n$Kd(4!~*QNk|IazDyq0*-Lx z1vs^+C%3h;0IW|Vuo$q!!N6b$z}Ib|auCBBRP4(1Qcx&9S~ez1G({9s!P7QNEQ){H|H7o*Cnx1JI! z|7Wv>gi>}~4>}@9Pvum=`uj`LbyETWIlX50Lim*D;Su)~k_rfhhD?P6UPAgY%6Djv z{E#ozQqeRijf{Am+dsm2(**ht%F~2$yY-RgJ(+MIG8zoM=@7;tuBexrR3u*#5} zcx`pZI|~rZb8122@k}wSfUd^a+p908?}AeH&PYs(cOd3`P&pf|Ij?c`@<%$>=JM_s z-D8v3tT;NO*bgX~wll+3Q3{NJUd@J2xqV-7_fr-r6(H5S2poW8K+;X|26#%HRfzw? z{KmH0fIO)CJJf|n(Icsjo;nn&T7QgF;O>o>jjjkNI4$U|N}~B@Dq2p>0seJw)wel%Poq@#rY&mIBRkuL{!SWETD0RSPE zu_iJ&>QNu~z?HzGPjy$!C@*yP{hW!_WL%#Y-#Z%f8da@W!aFPN*$-mdbI+z`N%wQswR_Ga=$;kyt1B%2={Qo}(Y(=c=vzSc+*p4eOZ{mdLEk+lZ_ z(v*&B7*H49{d26<{CuB-a-UT{TbpFXq6TWEFx}}Rc?09FM?sBvsC;fb_J6&p1QEv- z(wBBM!&Lqww~g7qv<{zJBBV@F9dfaqs8@9`cyeqmFCM+RIa;psTQ(c}Qap`hWl??r ze3T6kjTO4P!DWJpW(=QdEV*3OVo4nli}tSHfjBUB;a{prBaQ;Gg5G)Nm8c#mS{WsO zLm%;;`Q1_-1PpRn>6`JWjTEm+>5RQW)vwK8xJDT=KLE>MK2*#29rs)QP)eyYZwazl zy!eH_f~+G(Y#l>HmFN>q2_?;J=ycYX`AUQ>B8o`8db{JKd++|{E(fbd8L9w|D>8Sx zsQv zr~o6Ij2NX=2m)81W4E03y4_Gm+Pn{m>I6HDv<&h8-0}a0W{XkcSdzy_`$p5*t(aZ6 zcmd{yD@;oSnI3Iw%qYR@Jg?BLVQ+ntq+io~ zl<%|+Yo)Rt-qRj0>K#pZ#@~#EVv9l&YYp5~4mYdLWLT_=xdG$b0%|OZVRagFm4s1Q z9=EcfDd$F5)q0ZP_B1k8O;gx$mhSWB7RH216-&M+lghe{n47j#fe?0gm!dg+QYI1H ztS|mq8}Zyb$UEF*Rv-G4M6d|rs5V#O#{&hyd!@kTBWm`$hS~R z)e(oMeu(D>+f1CZ-IT!DS{0c&|A9`0aJg2CBio%SD$I@?Cd9h4&a$shj>D76k|TgX z6A;SS+Py4o+f;<%aE`om**E5NYxbol62|LKhH8-F)2$==~{b;7dyAIeVE zjqg68#0t4?^)H-HA-b%d?)|-QU2=~@ia=&V@}qT|$cPq#X|rou(VZ?)0k~m{*ITmp zFW4~mNS@A!rdoW?2HW2Dw2<3TmGMus`82rS#fN=-YrG{0`gYxbJXbEZBw;kp&LZB_ ze-}s(jH+h7`Ns+^z&SQHIH-;|@C*|~#pwh`_Lw3!Ut`;XzRnbXuHW`Ftx$eE3XVw` zT9JKA`ZLtY1-n6y+9eNX*z3Ml59g>_82mxvUZ_^fvYh9wk2L44atckRk@$I=vG+>a zF1oPzIP{woJOG*7f-(QDpS&fFuoNuP>sI24PZGzU+Et*z^ReI;gmcyLZlhs8nAdo0P~a@5M_Awg^GG=5dqWJs7;6<==g_eI?;>43{@ zFPMFZ2CD2^_}*%<{9Bw@nGPqAnpB-xuOSlW+*F?*MT>J^P~o2rucbTp*rLhA6qjtt3pic#5e2BdNXUSL)3c zhD(_f7oK)Y=l@k|5h7q4!P*KJCp?}%T!JOUm*_tm*p_?HBmq7M#4{$L{6RL zW-S_nOEx<%bG}B7x|ixKIRI9za#en#JjW-(1Z~yU8E~fwd}VGMA+YV2HdXu}Z#}&B z$Fci3c_+kg=-vmfI0aM}!|6pNc{YX7OYw+pN*)jHz#8X4d)0Y2Kq1I1hI?%zjk=cV zn435GQZMb1TCwMUr#@QK7Z)xyBFAZ7g1*%<5CVw~&qLe-WjNC>&AhUuMcMK^E{T-H zptGmtMgyvkB>g;@3IY?=@P0gE10np)NFuZfPVA{6Bvs;I7A_8CSqjnnC^*t+*K%5x z19b96vsVmGs?bR170@mTCiFpEi8>G_n(H|3~dV_ zUo0B<$$i{MF^dseO(NB{Y6AUZa)7=B)H1+!PsPepGJb6E>r}4CWb?sF15R@*6^YS) z0PmxAv7HrJ3Odm6&zCdn=I@by38wo5$(O{kNv$XeJhG%7#s1cnj!k)zhH9F#j|~|W zstULEKusA$1H*!;+QXG(ePFWN8{3f0JG)a6Da{vG$sK074^Woa+zafb6#bWZtV@Ax0v=eu!IkcA>cLd1 zU%|W7W-px0Z=Nhrw-uHWR#nO7FAHOZO@=|(W zeKy|)R3F*Si^$R#(M6~TiavadlIlQ|OQfV~iMj~viS{tJB->5hAqD;Z_LfRLtYqYQ zhZO*}z!~INmPhet$?~V)11r(2!!$p68;sQu(7IFXdwp0eJA~?f<&i@Vy^&i!Jup#o zehW}?_iL#~2Qx0PpVpW#$=BE|k%UA=MqioYAH)pmBWP0aRL7>c(SCXcv9#Xw7mwYG7Xg{y>3(8k! z#~Ffd-*#?w_K(!=%AHE+G70m&s(So$4#Wo#B(Ux-#2u_be)F{o*~~_djk`)gb!?xNIpL>vxv6Qn+?gH5tOra4RExTq zX@zmYmbB1hP=-?}mT!Fhw3EGaaTJW>ALYpxO80;@#=X{$!)p_E{?8beI9?K`PodY3 zZ6zqq{9~S#4r91+()mA6-VX=+<<-xVRe;^x*4UG?AQNbq;(n~V$GDUZsYFZ4C!t6T zg0YJH(rkAOZBd$#+oRhj#OkjIgqfu<7xRmee$1hJy8zoB3QYN%KuCk{CR6A+8LAd1O>y|KpBw@0`LoZ5nc1iwPLWY`U3gT3(~A+x-t9-d*+wD*vb{<#*xXWIcQXZ zDKjGWq}CRonMNn1Tj>@j)!{J@yvSS2^MTrb$G8tn!*4KUrRb_-FqV!$ZGbn4OI3ZS z>WCpat&+~RNdq~$v{$Fp_hU`=03M&p6OR%!udr`(LZ?gc9%+ibyIrdW?Bn1P%ESK$ z;#)CEL{jPnE-JhO@j(L1s?7NkZgdovzTQe7;qANQtkai~N@bWV)mAu47;fL*gElWz z@#z-1gir;ioz=Uc2-N7ETW22J0K$X{v@=wRa?^`36)NMesh6YQk3-I?@{zpAxpsXT z@JVVauFPE?VfJg#gz4o(dVbwVc>7yA5=|G9Vz#oCbM zMh=-u4nw|NV-+}j&fl*$ReM=uP;0biA=0=wWMi^q)H1zt!tU_L#jV%& zxrs84JJ^mvn1wJRJ2(++AUbn z5xerhxDGewkW`K^M?sYq#e!>IzY{1qF?5`LZTIWsLnBA;Qx6|ax222#w!)oQHrdNhE z35_JU zlv03y!Qq|;IC0nfksb(sPf1`){9YTgtnqZ%m#{X#>h^$+N{f>dg>WP=K-%Ux_etQG zPi+(ECofSwwISNzTmMW)#MNgL;JA^%GJx`jx^y*4&9jAJHa9x?X4gwnu8T zwm`jbq7_$(-lwi#XMTnzFE+o6ZRYqa6|1k28}$ww%kNseVh{pWQT|S9-7^t7K%$__ ztYO(#nwv_cX0QzD&O|p_Mxx5KOb;Hx8{Evtwi)A;rKvGQSg|N{<)@I+L}pQ#I7F!m z?GIzDMxy7lML9O!;b1gPR{;N)pRJvs%3oeL$;WWzNs=iUC>J`!u(D%W4c_*imH-Y$ z#Raksm~62U@LFW%7?r>n_}5FknDw_!}Li31DY@uad=cX6Zk zb17B~(+p0K-jfn%>jf~@pQ5QzfYeBqwp&j?XOj;?Q*LJ~OC^-hah>!TyE`bWk4TA1 zh)jcRvf6#6=x+MprmehOELc3Flqoe^VP)Lam=QX*ZSU%8z#)x-f&utIC>)Qdq>4uSppx)4 z96ZS&=6fKG(6wu)8O%BZ*`apz6@!8+iI0pB^{iqXGnsVtPA?=4oQU zrn!MlM_`aQ@^6a$TA(9^t5d)BEhvb0j0106zo5X*HHGVg0tzZ51Aik#sNIWgt(uQ^ zV28SO86cpZuCLoa3)YXuvmA8FXaqpVEjQSBUQS10IC?NTFt5}ul^1#ozpC9$POmy( zDLO(k^D%Kr0h`DzPngru@zF~3kw=<$(|pIv?VHI_K%v{x*?4x$Qs(j-#xM`YzNYgU zNg|6bTmjME0)A!7U#O~!S;P%{epOfxDd5l;MYr_0L6Z!20+NW|B~WL999Q?xQaiTy zYdMSoO2Cqq6u24M4eiia=rV+HaOk@P!$Do4Z@lT1nD?}wO7LARa?GAhpjJ@V=vS_= zcOy>(a@YYkuHtD`YTwv%yMGfrRsz1?Jf1K!pr+1b^RRGwM2s*eR5NrZuTeG)4j!D{ zt9Bh3gF2-9J6JLDEyLy7`tBp$o>JJKzCrthmsQS;-7Bg*y04B_9qf}*G}yq&KCX?Q z@ewY&&{F3XC!F$rvomtZ+D199FgOz`1Jj_yg_@Gz+Z1nYjRl(DaXNvkdNXR_kHksf zj%lCy#L0)D*|%UYJY@4Fo9PqE_&}G=5C4wO+`Abzin{@@%FV7J$WO@^1wwQOJ@w_j zjJZDR<#{w%;K&@TB92`3Ea@0dq5r6tAB%X^G+;5-YuRtH*-bu?sSz~>@)qSE~A8MXu1cBd`>$aMokUpTHIUGA4r zCgdqFG(xfzSS4>2#l7|XQ`Ps(d&)d7Y*@N+y8P0Vl>s^v!Sc#AgqqGSawcQQJ$1K| zVzzMVQhBC62I%o+7EWpjhpF=jE*k}6FP+TO0AE{$W~S8}cJ=Z zKP-ZAt`lat1omX@VX`4A#18*qR3O0qMufE%2}OkriQf-D>nHwg>^n(#Nu`E%j; zz-xFq>kWi4#uN>LTrD_5+++l^Q))f)C7UDDGA#I=Bd2B~|9cN>{lab%OK;$T?tE&M z3my_^FQ{oOtO3#yA4{NnaLuulql@2G3d$!E)eDa~7*rlHab)g#&h|5P{47R0kfOPMJVc&DWEy4Yj~ow=Grq1D{sTy? z4FIXqSmlP8x&gU-=y%u-EtZEy7G0|5+dHxp)_%$uJ2;dX8g{|FMMIlPSKQ-7D3e&a=hb45g)#S^1wzx$K|A9SeJhn5a-Bt zy8qE@f@ROsNr7qLt-*Xri_xszK6K_#iT6Lvt(tBQr&fKv9IL$bYtxHk}DTerpjw>^0-IZRs z${vTp%Rb)=>KeNTxDTMCd_~SIToOiGD}`Dz;#~wIFAy+r+<39@RgPJmQ-8LZ&HC&V zl#eJ-r*?z-45}nO4i;F^8);x;JV}wDQ__C(8kOLj6AuLm*-FR5*XCOTgG0 zu>WVz0C$4caq}_@oW6lmT55#Z?iY`lwZYln-6pD>qoE0=ppoEh?zU&0E7S$7(Dpcv4XL8GY=` zCQsH-6LEfduA{O8A0UXSPNW=>`hn;(IIi{%NvGBoQFZl$<&PQpT+itjzKKE-OOAor z{i6Hna!QDclsEt)2{3knY1??%rRY(tQxO5*L1eb;yl~d+^mQ)SJ|LpxU3V&{I2E^~T&!(Tk!n5sGgW)% z{{>3|IB5=22HPD=2`Bl;Oh=@5Rx)jmdFY)3pWu0P_VQLnVR1>F#${Z*2A9$gR+0`N zmUv%KBST+0sRd59OYDZ(IxF+qdxq8j-MQ%8pwQ-ON)B;&+Weo$jImRhiPwFK!!WKG z%1s>VcfoCn$2-&TxBax)Syi2wCFe;tZo;|1$E$^#H4^rsN^#1o9&N12vkqE~9nGR2 zvWbsWuVmQ-DUinL1ncO3%Sa)To#rJh?0WQ>i8wrCnVXbv#6#Yj)41L>Mk&!+ZaJ4| zTNxoDkyLw`qE>gArC%d-YtrO3l-9Yq3va6mvjFl}ltD0sW>5Vs0H0Vlbb3V*B6zJY zie&*-sCGJ0pN_E&qr2YFbi(XnVv2$5eMN|%5Z4U)3CGRt?&IL?;Fs6YHm&SeS40AH zk7pAmS=QOqdkZ`Au>qb8m7bmHu6%FAT>^-|51Te}RgcH`g9}h|ukUDNs5C0La>p*lqMzkns+@Ev#;a z!+UxQz0z6pU4)a8`=ov*(#WzV!^^p}94f}8ydO8D!+iq5CFj_5nkNPMY3%{4Sxp6n zX-h8yIuD2}As3_#ZU?D&%&fJgt$COqX3|tHVK#!ut2A})Hb-|F zvrv-UTb~35^0kTo1rNqAT85v9IlvXrs01D87V8wsc#gj7O~1}_CA~Tm9Q?VY3HKD> zA&T2bN9p0J#18dOu0}}9HYA)-m2heV{OAJ#y@Q%ONqQ<3(Oy;}*a5nZ@o&~>?U$tV zkDKU$ocXa!;>k0UoOMMWsNOwuW*q%+4UeKx^H~}|;z4aOV7n(F+%2ZVsZeO@IA@<> zV2cI_u}hMx)}_PrBbv+Qd#ZYBqCUTrTN5uA?7!kb$!k{LH}HNWYWr;mgVd|(;6&6~ z#~Hp&cNks>JKmvRo?k{!EDTG5cZJ?MkqgsNw67J|NEW?-WQ~W!q||+uj>9s8)4Y!@ zu-K#A7&UU|0#jz!%E(^k{h>mCz&>kxzQpQMP~nET2V=Epmf`s+(-^`6RWT3aadZyY zycogkt$)obA)W-|NlBo3&0UUY0TXP0Xt*w~*3@dboct2JY_tno{9a1&%mwk1m=yv| zhI-)b#X_i=Od0Sr!mV!d4%npMw18SQ_W|v&tPk@tOC@K{*TQ?dqqRqN9)fldQE^By zEIN-ACrc&F;#yJZD^y6b6zSk+eE?u~^axC0?;M6s#^}YNeW?MJ{68lLYNNlttQ>!- z00>gJH4{XoljzI@#_*+@^5%7JxlU@to9GX#P9ovzAeI*=0ny}0B-OG!y5Bp(J*}T3 zjLbHA)LSGKh?8kFS%!gk{wNDz(=w2?CbSHK@I z0nw@K^P93NC&UlD;#fCn0oNzJk95pUPfjy)w+)pD@bn%yOjBRQJcG%XW(vX$tl)p{ zF=`VUQs(cFE*KeJxvy;azu4Ha{6R^ZA1c<%gVXLno3G~hc5%`Bs7d!ooMjo&S$h1{ z2=-DN>??mFsk+=MmgQ1a9TeV9dqSsTyv_$8lP8_V+$@=RQbE# zQ!)wi8?}@9o{hjcWf@joKxbUougL4c6iaZbNz2O&J6vIqZ6!uTW@|tnV~Kdz-7nQH z9c`0+?0^3gFt+%~%HJwr`J$q+m(uJ{xp^ulxF6v^hWErY%0_jk7x2P9I#qM#WOeVf z!7wY4m^VJh9l~0RQQY7AEZzb2uxxFbuXIuHC@f325f^CRtnJ)xDEly@-_LP4b*hZ_ z<`bt54V)_;P)h2r>2;Ot|3E75VpQ>rzgcUJh7f)TKVf(YYzF8h1X`>h3+GYE-!Oz2nuczqJ@E#NMy;%hC=}%3aNUr|#;qPf^wb=GfU1I-z3|@N|AK@7XY~NUJ!{ z+-0}|Jdt&DmnifP&6(dciWU)s$+9(o{q7zo^ca*gEbxW5e2|>nwv)b`s|qbxA&B(O z*hS#sc?VklaY~~JY$6jAiaV|R2uW7! zipk?M%=a6Qqxn;A=}uDWoO=E2Y`#`wE0C=h2H+d}U87?TfKj6ECEv0vNQy`D?vcQQ zGbfAWXjY(CsO|?*h?@h~=M9G~){<62lT#a+ZsYcoQD6&Y2FNj^-BIswvimsu{ehF( zK57D7d}D(e-7n4 zJCZ%&sL}^;1@JiM&pO{f5MNqFxFuMY`1IEq27v{$LwO<>x`;!uM6^nPCC+;M2;6$O zsdR=HXD)y;hxA&ct%nj>cxGT&eu>l#^7&rlS;)+YW9>fYP>@g5jd(eggToHS`$GxX z#?I!AKVDBF%XCXYd=K0I)!Z#YbbOWN0{p3Jr^#%cKb>2zF(al~^2S0HFP9pzjlAp! zqX7hsAh9%!MXKhcmCswjLlB8mdfqvVi4X3KqjBM*P6SIf&z*9*NIMOmS+~u_!w7~qRdcW- zEVIVPQ3=Ugnq4VjlHqKHL8vnv0oHEUL!+EbyN~Cpr#U)C7 zj2h7k1SaV0&amZ5ufZ77ta)v1ED_iBTM`jAjj00~9{e+X9Qd+5(xk$^gG9v``pA=2 zZ=1u=YmSWh_iGgxlzB*tn1j1h6oT?00y`5dLhlUx^h4ktLq0A3kGJ7H+cN2cHyGXa zkL4f5AOw$DRG^0~WV5naG^%#Nv8Q{$%ul)2X~fbc)$6!ABY4mBx-v^QuItG)Z<*mJ zzG9ZL7AIAh3*kjB;rsmYmNa%T#+`tiM{~`mYqzw2mf&~_b#wsz;ZSdkqm!rnA}3mz zW-GIUJ2KHca;l8>uru7iL5&^8^RRGe9t@eKr%MU;1P(B=@BcTVZB5TTK(1SGuNB?s zY5h4Tv)gxX>;w3JaHaE;V6m-dFEns07H^g8&;<>t3=&MM4S^mkD@vNBCCUHRR6hw2 z7>u(_RH#LbVcmXC)6*A8ilmSwQzdy!PRemJ^PYz>#!)*J=%?0DD7aW1!?OrTw_4um z8T5Dj0)8StJsX~s({r;9xq6R8;&yMx)!`assGrBak<*#`j@Q4ny@D&A5s)RXSu>SeZaQ6@nG)u>Klx!LS1 zU2(JFq&P}vJCv+&q|l`kX#_V!Y@?1@*SVG9Z$|5Dz4l=>LRnH31Xl1QL}j7Rikg8M z;aX!L&vLr&;R1NN+^)pG1vkCksg`NQed{{W$!IC#lc#-(9JVvl`IveQ&Ar`4t~P<$ zx*;JX@pQMdIOGvyIs3z{{^30b8*|sU>G3+(&6)FD+?9zXoGcN#{08b3Zn*OY; zb~_%Us1_;RS8@0)3|zF!H2w)6K1k>j6u_@pO{=qDl2|+B6R7croyc7Iz=}(?!hTLt z+55kr?#fI(ZG&0tm*+7Sh~ph2QB6lgvOmz12n`?$znAbjv_eEm$1)WAkK6+e36^@Y zgP=0D18`%!cfc_SqpjHQu}}ml;Pihe5h0B$19|eyC#h#uM$(jP|F-*x1K1tW>0qTrJ!|DXASit$q0K7SE65n&5T7Idhpx&LSDJ zQr5x>I-mlLP0+9A1U6zQuzAtTMbN$y;5{8P2HFxpBL&g1xerDw%{#m{EwWt%x$YsN z-CrsXdn=5I>!YoInjL_gy@_Xoj4t-vsZCKbH&U!2W`;o!vv>6-O%->zN_@?Y8z5BgZCl!f;>lY%enBpGS)-U_)l*rA49+Ik8KK#A2Yv2x zrz|w|c15!P%tGac72e`5`3;^PDvcQpzgdjDZGtRj_iFFxt}kjPE2?fb9QOjQoZGDz zy(1B@iQT1G)gm^+H`o>FOFX_`g1Q7M!`7bzeJg9{ND+Po*$C2SNp?#*L1CxO6C*W| zdBZrpCsxa-al)Yr8*GyDa~99T_Zr?fQ=DuX7M(5y8lnroe04_n_WVo@1qoOoYK2RQl3pU~6N6+FUlp8I6t* zI!f9VZ8z99lRPh%HdITjxgxQ2*(cSUSMf(*F6`0K?I~9t>#RHAO~#eGN6s`jg@o`e zQ&b-Bu0gpKo}CeE0gS+qK`TxMz6H7l|11o$|LK%zAiWTN$qyUbdHloV95}A#Y&;xR z4XXJg?p4`bF4Yk3+>4OAH$8&`mCCHUc`2>UrQLWK{zihp+TAL<786ka03m2yBC{>D zE;b?>5I!56fledE<3jl* zPJ{(P9&?gcVg~hd7*NboIHPw&1KLJyW_QReRKxy=2yyp1%5`^zg)Wjmr>b*NjI>Tj z0e8rXr&DR&fK0k|m~PkPu{SzY?GG{9TPd_qf)jeSK3>5&pj@~DD@X6+ch$9L(u1b;fxWO;F#0n)X-45-C9exI#b4n&SlH8w?-DzT>yYT2u#{^qL>A z$tbG@#>b8}#){C$|LV`TzNQfRJ))ri%X4jWkYn~wdkhsX=}~I7iE>!ZN}=14dEOd# zH8iac*`U504zxF+8ir{qc+Hjz{b!)4W zoCI=lvDYJID7!=<`u+aA9Y2^Rr#$K)Orrw&Z#wu7u`sPLttttq8_#AcyIFja&1)*v zs+*LZ-MK`=n z`^5Jp34cS!rrXZ1dCv$f=mg@DzeIA>-oRKXt*@JWS8UBDRLx;L480Y}4?A(1N0J)( zi;Rs6*!0`=g;JfDMH4-+%-NhJuNY@FJtarhs0EVmP?8$v#ZKpM_Z@Jib7+W6sO(VP zN+L!KM=SE;KXPn!f^9!o^Ta;L?>dO7x<2Sd!_R3#{9>AWfbp^{B5q!f$@v1S2!?LVSNK?lq{=PMX->(zus@rWQhw>>5j(^%Oc@5qwD z9DWc|N&c+jiFTHS0Rp&5d*0ry3Z89>vf$yF?0@cwPGW9Mabp#v1V`8Y9jr=)1m@rl zD3>JFOy2y4DQN(ZG)y*dC3o{YD8083$rKUt|h zz(sDxLsSE85(>um_A920vJ;sB50d0-K@I~%Y;yKXuVbvkvuB}j9kzcT!a65u;Qr=X zI#8v}Szn(wU@RC*xIs^mOl?ubtW7mM9079O=UHUZpARe7TWGUq>Tvd?Gtm5kZi-><( zmirlFq!NQ^{6=s++RaA8o$Ao$4hD!^;XQ35cB2xq2?=J$lz9F>Z4gLw`kl{yZ|uTD zMi9d28*$HDNJJ+A0E&RH45FolTw?d*la>8g;@2T*hzf*#z-}(OU;Pb2Kw8&~<#1SJ z4#i@8ek|6okr0#TU~dIF#oDZkKE8{m@2$g`iuy@Iu7B`;^yTxHYM3BenmPkK0EUAn zoAtiB7EnITjuPvqvx^PMGzs0mHmB2@o_eKCbo)Bh+*vDs1LocF4n_-zPcDOCO?*L2<<|b5!C7vf}{A{AOdH zh4mc0E>^3!s}ER(0|uto#qbc+5DL>mNsa#+P+)vU*eu$zyAS|1K+3=00R3CbdWJek zb7^oBfRdF@!Dl?1`xQsBe>(oe%~G@dCh(oha6mkn$!xD|4)$DGM!9bZqI_&1WH7}e zpPWadr9Ri4FKCkNjbfRU;LNeAYx5vhI2~iD4g0^iwr8Q@1fI}Vxwsm(z^-$zwgh&M0mxN_qy8r5l#x`SXsH_E#O!e__Nuugkq%aG$}4tG*lCCDDQeGf7yTDYW%0-0 z(SYsHQdvg@);IHOpNSSH>7}H4^egD7-{eCokThQM02IqQC9|}Gu2T6^aXXda^EFAC z+znDE2sKWbTXJ{&jj%pb;~w7ydhBeQPnIZ_H4xX!#vEw4?;42I zr>1Q3ulag;?<%nGY~km`q6L4_6w*B{uyai`Q={lWJHLAZv03x|#KN-UnEg zXd1v;NIeh=Aw71cV+Gc&^3b#L&22EYaC!~AFD@`Yl1;@ajU8mm1I?w?S`NM%dGv|I>UCb`-r=$bs@IPWZ9N8V&8Wp#lB)?#mmu3L={J^2%T0ME2NE(K*`~ zHS`9^lNDZnvWDCh!IVP@vubihW}8)Z^rw=HQg0nomfMQew&kG5rGpm-Uh<4NSSCd2 z3raft>Qz*Y%mXhjA8|ltlBbKIWYawIIDwqK;c_$>V zJ(CfaD=h{PHhQi&!xhWpnUW>U) zOBq{pB+$fXO8^0}jJyc5uW?U`x!dGrS+-fyWT?oje+C|OP~KP05MWx-G<8blHG5LB zG=UP5(kSJ|tWkvrmwkIZ@A$MwjT8z zfPQlHgh4AQd*}7bk#`;YedN-lr|<`~T*$84hrx5fkTm}pQPZstb|SB#m(w$nP-r#` zxt`1pt=m$Oqqla+=AS$16w^0c-1@;NAd2KCxlUh!S+BG$wEm1EH@ELAP#X<<81lH> z9?GrX5ji|9?H^W#GwF^o?CnNuY%^<2fH}F!fDU9>=NBVe!~1r)0cjJ~F6bh=mG6xx zRQ;|6A)l@F*ewbpOuzQ+RV06TcnEvPUXeO=l|F|9YlEbpC@7 zE9gbxftR;RnQbu)JP_tHl@3)jx6!zgiRmO8HJsOd=imv|kMOFC zoQKGv1Kp1UGVcdUc3|^$5Q%877}5-2(hB!hcH{5)>0^gA3<=U(-LCT~*txJ&L(yrc ze*9OLg!6M=Ib`8Ox;0lbOrw}`f|KIXpJfL*Yu@jNz8$@QQ>7Y5A;*v*Jw1<_0KKs) zwu?$u0YL_>8VOKUgVbxgU6O zT`5!47x5GLE`a37h^@6Tt3`QkfFPJ5)&bY<*tPNwdyh|ej?NTuW~%I#@A~6s(qw|A=oV=I6`MLjea8 zRW4m*I`lsQn#dN}7fzP}O*YK2UA3C%C)JO|M6zbbWY{8Cgak}kJaXv#%tLOHpAMc% z4pZjF?JddFf2vDKAMa-yNffPg`jre(zN03O_B4`yuKIFjvYaoL?iXQy6-=bD(a1F? zI(l`_>ogZjGZ>VCkz?L^0@w849jAsm`;~81^HvpsNC=^D(YjP=j;wjA^qOKi*sIWz z+E;Iq8e~ipqo{;x>G{lX?{oFpQO5P@9~(x|%&Darhd7RO;v5M`iZ`OwU=x-(ah3`B zajGfUnTq{12tNf%qIV!HN55y<)fi$IIZ-eXAyid9(P4#k?)pEfzXj6)=}(ta8r9x1 z%mzKs=Kb`{Dn#br=KhoM5AK-im(XgUbUkZ!&jYakPH;%s>Bag5ff&o{Ur>CN!JcXI z(^&o*#VF^c|Do;7DN>^yj)M**dq-hHz;(}Y9?;9JhNwLLa!6{ihII)v>||hgHfZ)U zL#mHEczu`I%#(-FraGXWisz=kvc|tG*ZHTo?P=bveVzh6VzXRPQvnYV9Uvc?n>ovj ziOJk}u)M55Tj{9hq0n|d%9MNu=mhulu(F{OUn5vOmLYe!Rp7369@{Z+iT@!vkZXC9 zvonhPHeH{~oggp3bR{oqljCWQC;kd2{L09O1*x2}14oEZtMFpp#5;(F2ShkNe9n%mxguP|JBS_GUdB2 z%;-dNAo~Em^du)rlClkx=A%7kh7{h#W&q`loUc>6=P&=WP{w=jboDRhagKpMZ}|00 z*j85Ty5xE4Fi?X<&B1_e1}<%X>cE4XLl6E2ceTUbf?aJhI(z~}Gjj7qQ4qB|FpG-Z z=!7n1zsgRd8U4)Ksm@=m>92!dtTe0O_VTPz)b7N|!BdczD^)K-f6WdUv(T%-Y!BBTN zJPkTj(KyPRrAROATHQXh59 zmN7S+s2@Fod^wde516Vc%S>ggr7biT1=za(M%?;G2Ot7-JYpZklf?|5F=JNV_P66l z+7nd3yKsc^k9Y!R;Ec41F~DkXl{Sb5p#L@arwhKaz`;$YJZ}9N>ZZ5@S4>Zpru@TL zThKohG{Fu!ASo!XhhV^E+Hd|tNQkt|NXCsS1WBi^Oe(sL!w zMt<mY2tk9TX|5;<*3RGe)@t6D#blm>C`&99&K>1TVQ8cpe>bPvYVGM{qG!U3)+ zSV65GtO|<5^DgtE8!8lGGk?fMJv^q=3%`s>f18P`0!OOT(6;fZ7k^N8|1bg<*~9#~KD>L=s7A)vl?IEq_-5Y`|FgzYD?@eRqVIwy zwBsHsFA{I@X$*9M0g~KXg7)g}^9)KIB#ijkHL1rqs{iWYc!K>^MKav#ozJV}&lvAt zB=NUlbv;(+77`)RT#`Y*O-Kkaf2~w^o$16e5UXiUYVVd==#=o?bU4uPHP+BWosgYU zxO!;Bk)^2)&yc`ze`O~257H$ogoR3rNm{LXw;xuJPJj$mKo(r9WCV<73m*|4C2p+4 zBVonwM2r*j9vNB>#u=ThCW&Mi88gZ;=JmH}Y3Or0F@TF$@ulm}zCd$&Nq*g+VV0>= za&8h-QxRR7w(8LH>%`LUj6S21R%M+irkK4m?tvddyhiw`4ICbE0KT zv*rp!j(bqP#jR$EyUSZAtW46z3@U6iU^2Uxv7d}o3S)G-(kE{G%ZZ4ob4pUKegSvC z*v6g_Taw&~Ed=B9#Z3Lp0Dqi~;jWq}t`~29PFr~h;hta6fe)d_^#as5Jsez4?alJ$ zgCpIhF)5JYg9sX>b518$`wqRRNxpXSKMAH9fxx%!&p>(?wzNo3%quTfS%L7NJA~3w z40dX!wBSSlH_v?^h5QD+`6E0gMX|S-v%VN^d@?Zm5J;|I4`>^!R!E>zFW`cub%-dM z3}vgDB#yKS@RviXGXBz~8p*q5mlN01P%I^;(nl4sA6kYRMF5FWf1vwZDdJ}NOosTC zgF-0EZg_3P$7Iqr(zHjum^IUAn;6UprW~{iR5~XmVXC!-B61K`VRJF~)=YzT_10}>V44>wy6(>~QbkQP@=39-=Sa2xy7r|JDKi<$`vr@mN<$D!MV#wu< z0#>rO9cO}!1MLDn+yBxT{+_%-lUE9TaRkzbPY(nCbi`uUu?$#pvu#8r^`SAnMreL@#BKW`}OsJhZS?KI2a4u9T*a=alHIW!FqM8pQt} z^F_v=H8ozWf_FwZxeUSTlz*%Dl^ZI*Xb2;)`S7s*bFUY>=RR~i1M${=H3s42XEVE5 zj5M9eF@=M#Adj!P@AX;z?Rpk<)ZX^l?9$woU~;9>chkDzM0LhIR5$F%6W2PFmxCxW zX#X1MfxbpYNW^oopV|TY9`HAI6J}8IXKx{g^np$Q&19RPx(*hjGA82Wy^dy?GJFOH z?pIhU2IcBPA+RxTveJ`Jy}^t_f?v}Jv*aCMrDGwEDF-H@HU6N=g1iFY zb4Eb76rnZerEKF=<-vEFFr#bH@ zCnCX1+`*Ib+E}TaE%3x*+o`cd`y2g-L4rtXJ;J%x+D0jDMmMGkQ;q*eS<8Q=1@23E z4)~l!{{cmVgkGeF=M!*mU{P%3mAlWHw`uY4y6M$)s{FN;yNe3CGw7zgzMC}Sl4qC3 zO5Hbcv6(uL_@HWi32k)F09g|CU_4uYz?= zktd1dYaP>7yr(7uIq1cQ3;!LNGi3flPHN0_9ToFo;F9 z8iF~fW%|oEy3?88EYjy`3#o2u&Rpj_h9(DYoo+{#sphBLPbf^)0rp6(Id3K$Cu7*f z&Th;8`9Sqz)8SNop`fdy()BXt`vj*rU)KfJD{_(7kAuNCxIYIfL4VR6nCFs&o(A2F z$SSB+u7n^2pq8Xwo#P3F7VkgmX2qfO$3`W0Xbt8y*Y#cWC`LoE60yYmyQ_Xf#2c=X z7k`9U3*4U;Etv>o`oM{4R#ia^*yAhh)Y;#V&9H{UpHnsQGxdcl;j+dtpmg3+5z5fj z;^MmfPigGy7S8IwlSjrsM@iv%<(8G9gj;Gy9y;YsQx|FTK5p#&MDtz_3jU>0bBDV| zqcpQ5tb)2cVIoDn%)63mv)Q_;bnbPgq(HfQ>HQ4z6k|y3@e_$Cs?)u_xOLCK)x6nl z2o#Yj`-xb!>6uKKigZd@X4db^LQE2r*Ll%vmgwauk?l=pP5CTc86y$pQZZzyW=rZw zTF6q-PqxKs(v1IAb!vhj7wGY$dlxh<_#k9$2L4{*woQ4Tc2ee20ul2sC{TcRT?JLM z+{RE8!D-P@>Cd_i4perjP)+mRRnQB701!KTg(LD1D*$$xt7G2H zS5moS&8$yKxVc3i zH8?6l_BDbL$E(bdUl&_)OD|+cgKXocC0S__9tQWh9z+cq2LgLx8?Gp%?74v^#{xdS`v9VW8M3tKnEg4m+RIZuEiK#$OzT}3G zh<-#8ULpFxB^<$L@KtjLBMbCI>^#pq~7zYzVIqW07WT?hO z5g|#WZ(W7J0qo&EUSG1fJcb~-umo#imu3AX)D-fGldwJ+1;f8<$Ik;C0j0A*$e!zb z4f@D@@IJ*!o}>2m>?}HnyW+{GTo3_gsc8=oQ#$JrSXz5R5Loee;+Ud-gtIcsi_%TY zr8;jolHXSAyL>p4>KvgZLTF!mPk-@@iIQUItJizB6fXl=f@A5+ibzRz5ofbj%&zTt zA60^z*qj+Gf_#`l{1*rQT&&O%>zNwjO4`bdF4SjTN-VN9dV>%yI}#iJ=8kbrKMFw* z<%_9+JD1yzhBo^4idS|Gib;wB^I~mFI4)#>MU4|AWlB3*DB$?%h0B^f{hJp)6iGW%PxMXHwVv>CM z<5_P~pL6>?y%z=;;4BDYYfH}S*V5zTVWe*%O-9`Yz{My6~YN5t0 zo7G_J;rJU9&4q8FAu*J9M^9Y&v~=^Pf=QUPq{bq6Xj~Y)Mi-+%Qt+k#K9pO&=PI(h zKxXXxqYM{yCLD9~wk@p~UPiD>fgKR75%5M2WLUCwK5KFWESKRp$EdbwCG)*y!8(jX zVrS}7^Gf#X9ZRTXVAQoXIN#4-!B@y)m8~h0=$Kd;-}y|#9{LenGpwJplWY2n(RoK7 zv3cD>uFU%dPv9$(OrkT=zY?&`l#q!NfKO|K@u@zqE$^vz5Yt|KrrfzJNO{aBlNF8I zM5g{4_#_Byi}*A)%ISYVu&ba=9|tven#&%GnKST@>Qf0hh1g)b4td^fj_Kxj6pEBw zPZGbiJL`W>S~~HFKcl)Pyh_iBu15M&sp1{d5Zu^sR5zJiyzUSY@dKAf@66iHzARIp zjK>H|eoC7?bkY4y9}O?N-u=SthPJ9i!<%TQNPvYHkQir>s4Yb|j`5MLA=A6K;rhLf;DA?-!E;s9axML5q$MKNVjr#CoXb+=fp{ zIS_gBYRZEE<}hoj*UN&bYPxbocCv(JKWCM?Rmxz+Kg4aa^1J-I1Ap+7`N)tAN;-TV zTY`xV5(tM;2-%(Ij1%9Ohuc@#*@F3EZsOubAGqR(Uq8n3^XC;JUGZs6Y2nDUkY&$k zMyNu4Jb3j`HyDUqulKyU2qf|>fJIy|sebk48>pQyrD@cu!ygv{Ob;`c0$qntnEo51fGzkBu2Z zBD=y*r~!qPc+=kftF^|&x1x<0W-gqxTGHqSS~@+gVRmjZxJW7bW^maR9pv%a(a4F^ z=n&v?R6TG1WHX3bSAEx!5D>RCe^PZr<1ULx^>VPU742VdLNElsOzI=$x9lkRs$7Nk zOlUc#8@DepSoFN^E}vv(Lh~xv^%8R=UIYPx!Yhy`wZ%5xkKumIX#|(DVl{SDTcn$V z&4q}>)}n7*Yh(5?cS{N1TL!-P@zNi1`>T zPYd-hLJMaC_KUZCo-GK54q@o(|9Epr^_3<*mBaqQ5RReXl74Bj;#@+}Xs#s1sy%zs z$%6e{rYvXu=x+5)wH1q-SUvHwiZUDSv2mkZ&sB$_>B`n<5(C;5Z+m)k;ilJ9uky79 zz5{rg` z@@uvdu`wr&lXe7i1{k$W8^mNWbdkTEU?eC36}iC7@nX;*(c(L8XNNS8lqT2m9f9R~ z)IK8CQ2@(=Zwi%~^9lC3K|@fx-_#8>|m=GQ7v=eDyz zYgg82qP)7^qP)_MeRYRnKUWuVb~Fqbp<9eX09T>ZBD92@VK||aIq?VjzpFIAy)T;~ z?@f9VanfTi=m2IXAQu61USdx1|F!(`th-|>+{SNp`(M=2bfhvZn+3o$x}XN5!L(QD zz~!NW#v02}nf~?P{gu}38;L@luS>PCk%n$f?bdAMYzH&$QcNB7k}zpLFrK)${LNjB zUH>~Fq!ds%N|+rpW$&$WgQ`< zd`k91M+!A=~ACiBD>)`_r* zg?!sMw}+*h1DamZQ_Xtz!cfa?TMJtLFrd1F>2iaY*5#`b8jxZLchn@b#6Y%e_#TRu z`P{5ch%}!k1q}Mf6uV`epdY6~gsjx)^@$UfjZ=QfLE~dnI^8Zs^`@i)WRg@NeQx;- znXG*``FYZNBF=vW{Z^*x9VY3$z?jg3#wq_2ZiHx~W@?MY=4KE{o?tFj-+j~l?oU-! z3^Z%F-%oLFv)tz{SCmtWpzF? z1a`y!#eOiE5g#}Zr@%@hb=$XeBJ>V~`9VwKT9Q$@X*-UeLSj@*%d9-0%#cwG5N?8@ zjG330!)5h#h{}k?l|h{gGonfr4eEazy|LYg|cjQmLFUf}+YMZV@yFWQ9M)1YW@7+>kyyO)` zMN$m1cCeEp8fp)S;NYw>jEJ+;i<=S^y$*(r!>)m_WfDa0gh(GEy{5J+RIJyjMv#r# zR}Ds{zuShajWUM!_V7jH$FI9N;rt-FscyWvX~A7C_P9e+#0pj`hZFZh78;pLBq_$`LD*5j#o5lpZHuUv$K?R$MChG7Yiwvb5_ zEMIHuia1Q=LQ5F;7SrCCy&kV6ps?ioKBe#0w|b*_*c0JP(Sguppl10o=nH*jdZ%+K zMd6XY)GyyIVhNHHz@|#;>65P4#b3Mc^F9S=;aoC_IKEBJRws|U4xfB!e4e%Xp|oRI z!AHz!h>MfJc}%x30g)c|COFp_6Yx7yJBO#bQB3X1qdfsY@wBUJqTg{6m~(C_ zCFB)`5R<}Nwy!UQrKHYXM_)OMDHi|w6nCg2I|098?hxTUElu#J_=ug_pF##;rBEro zP3?LSX&_wd|Mo|Xe0Wh5!{IkiL7)CD4lA0n#~1;R{>E-64c*tY1HbYeM>j`LutB^! zwBe)|V?ryq)*{@G>q&Tv0J-6TYue^Y7{afXb^P}f0fVzngxM+=EN69h^bzY zb$P%&RF%;JxB5XPPEi{(CmsA>V@(l7@-w+y>t-Az%EDLCX96P5jX_4~bf@rIvy2*` z-p8E(dN6Pf+0S6soCt@?Yx$stN>Uf{W-8R%WdSS&faYW|Y)BN445~2zHk69Y6&tP~ zwOs61>$Zcf{Ctj5S&DJ74YSq3OsjO76)xpv`GSB6r}o!!iR=g^#uly*tc4MlmHIRo zoK|e<>k4bE&}K5ldG`D_MRM>7t3K-Ww7bl%dWHy_2<(3fkF1bthO?08rT;QT2r3o~ z+vhKCWGj3Ba5ubfK(=!{eM}IvAtG#<_8b~-l8-CH%;_J!7m=-i&+Q=c0}$n`F8U+qlqMw z3&SEP+xdjc@;J?tz)R+E9^vLkhsQ!Ut2wh#xH&iM9XAu-WLsSk!fRTh5mCj#@fF5t z^B;{wH8-SW2XEKli2a>@6_c1^cq~~J%`k?6`;bN? ze`ff3B;(&9>42J_4&mP29&sH$Fr;*qS7=?% zp7>u)>mS99Y;@o0;NM<%hYp7HkVP!-E%S_J4{yZ#h%Td+3tJB<$*AhGN(u*=Ni3iy zJ(2>u>*8@@tKT-KyOZ-P%;MYkOpXlp_AxpH{3nst@~GUCZW6l<2Xd&~e^aa-pubH& z4GuS$C-&WScIRhUxhrYUNoUODOhxy&;#OmJB?NQb9&B0#ta7*gSgK8$HD9l7*4iz^ zzu`Z9MCuZ`MP_uKSQY^s=Mhbc#<&$)1*_jy0SK6;8@NTLc$GxiexCFaS6AQNL$q&8Y;#eE_j^?5D zDNI(I_01v3DVC2Smb=yi8CR*yf&j<7CYAV#{i$Tot~wuh9@P`6?HRU!^(4iWj(Y!< zwNS;6@!JOvARLoqiobw0N747im8CvpLSmcNCQ2Yv1}Zn@H12!=cF^4`|CH1V!X*^8 zlw_(;hF{UMZ^+W0uXV3^9ldu~rJ${{ zXx_Ozb~EE}1uQRKZ}<$nJJQAiGvdyN9>BP@oU>{D_^!poWT&R`zpQ_QUtx(vSs}3u z{oq;d7Ja&dKaD7lgQE^gZn+mS&ZCI{5-gSgY`n%)lX-1U=tGG}y^e+MH-53ACQGVX z(1>KX3JVa4jgO=*-PGYl;TE$_6%3qh?~w3rkq6@O(}dhP*{gLKrx8FTvxA8uz$(8Z z;@ZJuq66>~j#0*6w_1$WKx_~hwC^*=uBX-dVR<~cyUq|<9J#ub`ZpFOIr9Ih7gpUD z4x{xUlIr(;+B5>YS40{@d#c0CsDK-VKx^TYAeK$_BFg7XI4r<(3n-il@>g!~Wy>4w z#4C63xc=T6OD+Ocn3yq5kp7k$)L{AfC`Y&NjwY?196J<>OfXq))VG3-t|4ybQDm1W zs5`Po9(R?d+np3 zJ#7E5!R7m=p6iF03_AneDG`j8TFS)9-mxO3++@o6DRiLWSwgy)=Z>13Y8a;8S1saN zHV5img-mrIRH7>7UZ7X?!9_*9e(La|fScZ{+>WQAFQ>UGp5crL4;TRrO2S&<@mv5!w4zl?&ZC}uO{i;x-Ytu&5!HL&>>! zS$=&1cN7tKe-8m3S%E+=kwnDl2HmDOQtOzcjJFkvPqp88a{KFoc=&6G2zsRZsUW6L4d0#;YH&zhygDwxiTfZZ> zKsZ>pP#YD*dl$%yB$rSFA%ZpGWXbYUF_l&ZFKJ-|%N;R6(sY;l!Jf}v=*Yr4x*Qdz zR*^2)KSx;$&6+#*@QELs`z`s=IY&#deV*NwE3Ig^>5-^N+|f7Q(r>5(TbF*AUNh6w zmMYPiLYWRoZ3E$WX7O+nuU8zzfl9^R(w{e#tQL)`&jJ8BUf*Y!0<{S_kxq5`j(&7Z ziENu+EZLfJuiEn&e$vf&xLd(}<=d;{ASrKmV|v*3b*N7A2*LjI-$&XAQ5*Qh%+Ro#b3hQ0&&Nj#6UbpOvi zzcI_KP`MtX6>vH*NJanxl19{#bGGm`)3t}3=lwV_9q&W zls=$2+5hdsK3oAk*m+FGU|w$AUpce7vJN}6*P6Hfl+qMhPBjPE^U#jw=0Juso-s*2Y4Dp&SyszU9qtA_hhn zBU7m@Y-#V7Q6aDZSfVaHG5Bv6!_Q0+a|5E6hk|j%FHOH-WY9WkJw>`S?sY zIAf1d@{$Wuk6jJ6nXL7DkBGJYgqE&oiSPy%g`K1$4$JSk63ul|XrjOf@Flv&Pd23e z`C*O=?@?0rUT}*&3SF>-CIWK%5&oyRF^0^Bl125BJp^FaVr z%i&l`3MbPA$#^pMtHeLt=HwLlZ+!S`&V^TUm$uqwUq=t66VxCTIaNU;ZMajg#hv$OG5n_C-n~%RU{oF7}_BgVSj6v{}4k ze2RO_s~$_3&7s?H{PT-U#aJMI#RTMP{Z3i_j+*yH!Httr&Tu#53zJl!b{c@sq+ZBu z4nDlc#H9L5m4j?RYVL2hduSz5xkyArEx8rgKGI|>(X}Bj7on>oh0yXW#QO@y44=6% z%gX6p;3-UU7>e(zL;r`%zjX9H8QDGa=js=skbWQ)rVEo!kn){{f=IlU_ zTmjYFL+a?tAe}G3DRemmkgg1p6vRjpM)TdYxo^4gKiJ;g1Dl{G@H{+)OGn+dLVdeLWRVc{B+QTzPqQ_dd0p4%uo=e4Q$K&s4qk2ZLD+;==B<68GKE)) z+rR9fUM;ccRa7&0N7q2P*P-#E5;rTYbSe*g_2A0fr0V1f2il9vvp67LtFS&m4b#MZ zuKQo!7(AVIMMamL8R)Rj9llt#CkgDZr@EF1iOzO1YZGCd z`|-5qy1Nd5Q0dKaPr+Nc>-C?u#+Tcge3(p zA__-YovVA`R-R{vh=HAO673fx#m@B*fTOn*=|-h$Yn1qJF> zEv;Yt#JbQ=E}L_pUbCoesscqc%JQHSH0k4qrKUaD3iO%BKQJi*FbL{t<$fo`TZ1y> zKp^I-AL<6j-tf+}0y4G1er8FFEd6}U>ZOK7BsPI+!NGi4 z?{=cCdY5sMaDu{#?eaEN;Q{$b67`7_5`YC&=x8d z5RXQft*}QM1=e=BK5d@AvEuy?hR%rQPZVN*D`V1Ep?BpI&9mID!HRs^hs!b*$o-yz z;qgn-Z}ozHlP^e#vE{2ihs{p~6O+_icvn?a;{}G|VNn64Q(Znq@S`#*smN}r@v!iI z!P94CwUtG_u*$+}FleMo^OEBHrI-fVtWJEalgl2yvTJYfdg$T4`OZ((-2W#(?*;cU zbptl@W2;LR0Y9TWG$`lfV_xb{RAxZgKk?^oF1DQR5K_%eyia$P+CbGe+-X7~n!O>m z)N($~HKh(#yTn6dY3a!Lmv2|wEFOCVl+`RWW8L#8{te?`2MOt^s}i>6-s6rZc2yzB zC}#X=@{h&&?K?lmrMvFP=XPq0`X@6Ji0+m1hoUUl)ji-vNP;0I%jDHY4 zb{>Zq`-q|73H!ZTkpOPaFW;|Ofek&(+6|?)Hl7*@H>q~G^v13*9N}fp6O5lVIE+d? ze#6X!x7ImME$}rGZPAMd2w_%L$##$C!ojj>$Ub!#p)ad|Bo=*mysQv%@+KOfek7m= zyBM;BPsfh}g!Icj5F|8Yc4m_^zpmCS8%@}K%Z;iXpCXMh6u~(a6CBGhG2fLj#1l}R`P?;L&o(TwVhp~k$32Cj2>v+>d zd@4BT_y{FGL9?Esm#UQLa!8Q*MiJVFelkThIVv=Hvyj!UC?-=S|G>%zII=)q(4S^?`ep`VC@MgJOD^RK<~ z;(_{$8O>Iy(8m!1qJZI?D2?LYa-_m|0q}}k?Pl%7gq`#L@TxyANJCpwm-z%3EcZJ- z5UdoxWHc@M&J?C6!DDru!8@9TKaecGPN_HJ#F^jyI{j}rs)TE-=arQeKlVW7t9V5V zI>it;U?)S5`Q^H}ekpuJUCyp%FiW0%Lc4}zM^|$<h-PuO>Q&rvMxkv>XNE}>(gduu7Z&;w#)gM55q(wM9joxi>K3su7(m~T{rZ&S42x1}` zLJ3`h*@zK3@{5Yi{y&9kT!;UkAoO|<>P}(*)+c0x4(Sk4Vy!gaX~lN|S)Wiy?OYR8tfntqs!L?37kU z0(j^ee8CX`Q4d$K4ul~-Q%bfbYkUj9lJ*J_|y z0breu41uAi+WFC^DF7^&&s>ZX#Yig7Oo~1PkW8PDL_uNZ7W({qfy51%olmYU%It;l z*~40{V(9=oHq3!&|9gLQd>n#d{#utwDJm}4Co0TDNXEg>88YUY?K45W!VHZ??v2qKv?f1QG z^RKrc@`)IJYB_9LBt$nsI+Jo4`7ZlRz22GMpQ1Jk+qkOFxEKWYOl@e)tDC@er z%YNi23QSXACCY&oXGF4m+r5(HTlgf;8U-FN3DcO4MO0yJs?IY@6!1To$F7I`WMR0rzDIQQ07EAE4+ z*LC_8=uxGvjv1)Rk{ytl-fxXSJz$Oi;DOI1^@-t0)M z-#lrteP7pnQymQ9O~9NI73`JYUa#~IwudIOhGQa9nzMZ6Y*1DxxQD27fWNXxzPQFs zzgT(76c2?938X!Sv;%qLdrEwi^_Q|F?k45J>jRnZhW{ul4aUKyNC(Tu`cWeeU>(Vd zX?Qlt4JB7#oDJNZdcO9&0aB(wmHE!;FCI(x{{6b$8IgPVA<$Po>>45$&4tfL6)I1C zU+Z1<1vvY0qJjal+=-~sm98tuJ3p+u#X*N9HUgGb9jmOJ`D>G?pewR?hB}vz@|RsY z!tA7Xize)MG<>FODehftGb5Kl=Vf57ul)MySX{Yy;9(m*mz5D5bMZ}IIP4>~%0A30 zgmQW0cwTYo+lSNuq$N@k)cDuEU8No}wHGpZ`Zsu?$O&(11-f0E7$047d~jj0UH}e% z)LVSG8#cSj$o_s4-_A|N2W8da*uJFAC5VP~>6{WO@Jx^`;0}UY{%2@=UW0UKy2dfo zui3Rltbu@)dyg?r>W#B*Uxxq%iv7((lJQ4n_-3X5 zza8`Z0=KG7P;b!iC{zp z4Yh*%xj*asg-6aRS53tiS99*ovxN-;jX5Kn+BkAf$0mABG{F~tS+dr2-yJ=hJPq>2 z@jU?+>6EKDwof*47VYBVa%K8hP-TRA{pU)K(cNa>BNG8(^9vN&L8VQJuh=qQSVmIq zTCC6JU8(qc%hl)!lyccNlJ5C&If2MXqUG-)BeEq)_ZZf73{?3L?XE`O9L`ubcg*;M z&*S|iSOpfPlT$ci(ANcb=kq$fY@<2{Li(6YX)|tlC-I>dufSkdOBsv6!x~rUOB~|o zSYA(^Ef<~?$+WxXD2*;;r4<80$AZER zW&U*HO_NN7On|YGIq6iytucjHHuiDjXWbp&Gg)LF>b*zwmwfgQc2_4iTsHS}q_u3= zh;b*4e%j{5#9$N^KmvXdpk3+NoDQ9S$XjP|j)-|9XKj+9!=}nc+e|(&(XOb|tEbS( z@mG?mwo|;-7`QdSwNOgO0Je)UPAIXEDf<)xe__rv|xO49g^oIx`@3@P<#Wb#I1JTDgw5b;d0No=s*Cd!U& z>o%9-cVK0an9RU(te|^yE%_H7(_~;b-!EHsCBc196q~Kkye6HWtKwM1^pXST)=oau zZbVDPskf_&I1Dx1IG*=>pZc+?$spa!^025D^AwT`)A5Bi+1Y=Fz^}j?ybWpL`wvh& zxr0cW=m_z>CLnnFa0<9J!w9&GCSj1Q3pEvd-DHF&81xs`U_TYeOv=-g0ZM!bR~Ypi zh9nd*LPjxhJMsvcY7Ll<6fjO{g5&GG`Iwt6~lxy z+oBuV*wKsp9y&H@aln(g5;FUWLgOf~?fiEpGz3s)rSYIJktcE-JI)0~5vn{$5^KJ8RfBh}*@$O2mcv$h2kdz`q6>TjYy>S^MQUO;r-R zNl}GPA!OI(k6a!3jBb2azwf})c&m+P%xfM&1Rjp~W-S&ir;9K_-L*h%vGWl5ePQ&X z=(dsYqN~zxl`rJrx6NL4Dvf<0Xy0PE-*Z${O!`~FGzTCNv4gS*;TOG6&HA`u&d_l4 zhL6bVVxboX9P&+JY008Pi7-3CK2lS5#9PTKV)YRWzu!;guM=ww&glM%4C&D~07*c$ zzu?fl^fd+1*=(qJomHHe5}(|7Rp9cSAO7NN67tnFZk`xq=7OrH+1py%+rsoSB#9Qy z7q2KQjy#B$vtbsuHAtx;@gZ%{#0}sICHpuMmmPb6R}=Sj3H76aB(o=Q=zS5beT`;p zay}J^VIk%r=UL2#VqDa*T$iyjIVZo17c$NNN-T59UUJ{0S3HCsOk{(QO-JZ ziin+KoapCUtvY#GOcY)7z$*|W3p#_?Hw8hu61e$9x1h?6loJgJ<3ITPw4N;4V;UvO z4io4l)SL*6PgdwFBoVdrM3LJpqHgu?dSJ#|Q=`Bikm78Bm1cPJw@P%mRVV}*eUv@S zF082&8kOF2mX*1}=VQ_8sL6c(>oRIxX09YCeo$(uSVqvbUQ}rRycP`TV}X3w#VCiJ z?WPRB);KigOq%^Nzdg~3B1YG%HcFTp9u2sY$|{-$i?Htk`lIQU%JwR5KjujajNc$n zYpE#1<6IZOX?1q9+#^Z(%OeSMd^uSn#v!r~y_y3&|zrnJmx z=%?O3^h=k2xI>FUtaKmc$mN1GBKf8vVa>Ge;c{V#Vht;6gKCa(6^jw+orJ7HC`2DO z16M$~EEid8oN4dY=tpFdEgt_Fsy3E)t}RY?L;ERU{1U^_M`#Ie695LC`X)k@7ZT)M znQ(^OZ3LCy>?o=-06b5JYs!;6C>^IEzYJ zN`S1P#88GL5?w(n1oua}B~$Ar9Z7)Wlqm4u33kO*YMeHs)%zQLLvg<4j1FRDLxSD! z75)&tZfEut`thhgy6h`7onprYCG;;v&}(5Ea|6eL@%h7yJ-Z_GgrUENmoGpC`-aEs z7Mr=z!2U#kf>S}_io$0wR_9_P(clBLs)f+MNBsh&%AdRIHzxIP%in70p)K~ z&QvIYYM)*)uJ?(*LzDm}Y>QSsin51`W!yRI*I`13Ri=ka(5lAhDEZYlce3v`Btk4z z4$K!ae#y2yXdC0D1!QW3s5Df|T^{gE0+#|>v!ZeGKZ%|7DK@dC1w*og|2pF{{~7 z1<;|-g~Nv0wcAz{n+OyY21p+orFyOn=z6y>6{tV}a-5I;;HTtkeK5+YNM?l3SIkZ> zh<@|K;sq8q+cFeV`bePh&-9~TFNk<$Lp$OpOaUaY-2N|h63%SSm>WzUvMKWQ*%&}% z-Tm~iJN~e@p|*k8s78CBMxX7VWqYIA%$36Pz8=@GPfz?8BG=xv?4Bz_ydMB)rC{w` ziy50ptLt1-_6J>&H9y@<-9IHn8;9%VLZ zu*^CG=u{*4$gqC?bVdwIK=A9Cnlf5tuF$IooX<;{LI-S?!>RSFF1zan`yf@bo{Ysh zJXJCzOPOa;KAe~3+#KVc829<=c-Y+pimIR&0y8A_D}b$dC0A-~aXo?~^cQ;J&^%8E zJcF|U)qTtg53Ty%&Rl#CAo%J0VinyKtAoZ)xj$4jmv1claorw2aE8KecPVks$EvE} zL+%~l16sVwkppuhlG#W0{Kk)2ahU5rEpOOT?52N90GBd|gW-J7pUSi;=mabP(lDxy z%0ZT;cghAIQ+L9Wh`m7>M*H&&aDfW7~k4kk(4JKG1#cpIiS{R zWT#8(9XYy&I^(@G=8-FuS8b!yXrx33Df&I>~JSj%#Ztk4*oJ z$Yid~AVi;EZ?8+Yc#%%+pI-mbea6Cl|AJB=E;d2c7kGuH)zQ9ds=1JpgtD*)vAt<5 z0xbLXU4r-ya@0=xF`RDO^no>lPKV<;GAAN{n-C&B2Po@At5=Tm>AVJxH#IP zL^4^eKgNk>mT%Z;IUMR3oo39A6pz;$-$*fZBr5Hg#{9IU_^r9&= zMiOK6fU>vC4aS5YWWcNZ`t_VTLIH(;u&qE*_C-B0msf1q1d|WXleKMBOq2wGBOL*8 znX4CWlU}dE&Rrc3&+7?YQb+);yMm9Mjwel&tK-_a9^A3C3dRD%?OAc1FBrfQRWsp) zpT6)NH+d9#Z3;GNAG_MuIl${B-3!smJ$TWk&%aoXILNoxi0<`iKj+8*qyE{~b@g{> zTP^8utzYjOWHM3hb8?pCN!}!_w0aPbC?Q2)M?Muyrg$C|iVnYe7g2wg*w-qfk4<76 zwQwL*ZnB?-=0HXd3U_tL&5UQ}xxhWN_He*D*&;vvVv8g3Odtl>42Lwwz>#e5U`j( z^Oq&RC*aowdoZTvOY6*-;ef(EbK`$}+`xhnLTQt3?r#M{Y1)i6W!@J{h}M}E4DUDfsKv9dL35B}?q%Lg5grl7$3B8)TI zG54|E)unZaAIcuO<*GNvl)KEIjh5w-yI8a3F6!ME7yak4C@9_Sy~@d z`G3R=)gFs9IQsaV`dazgg-R~j<1!^^ej?bLMv9=^ueUsjjr?9yDDm@}`T}OzQMaO8 z(xq+FI|9&=jhT1B1$v``GwWeD(YDDRfBhug6Q@1>*QVJf3+@>7xn1xlSYbks$OB-LWmv?i2pLV&C=Be13>cC!{`4>BU z7I#Q+1GzQr0I8oPPYCGY*QzT}kjD3C2W zK`CHd@C*w?fE+TZE@}zF=PJ&|oIB-LMgG|#Wz+E4UQcP))by{G)Mm(Fu?Is7mUg)kIkM8 z#cDRmZZ=yst9H5PlBsD+s?b_-9V=CO!_F8iJ#+@iGO%LgE!`45&MS za10=bHsw_~Iy;Z~8VxeIeh-kUrc0IE8;e@o_xyJ}Ih%S7`b-30X^vMOcel^vSyKAA z)}eMgVR_^ibVa^FhY-pv!}wcMD(f~mShPc*sKeThoBcT@t797HmldUkLM5XICwGF4 zgUTKaRzif%iG^m->wc}w`S)lr@BjL1q$9oo&-?zI(WTBc6SE(*rQ8HkCYz=z!EyG> z=!Cnd6=lIM6tV1oMK0fE?-`%BJ=54UqKNcE2CS|K)zL!%uBZVEGE!-;0_^Sd1C&w;hGP zUo%|`!GCSBig)&Q0cCi(v4Y%xCyKH`UI8Vozt;L1b7n0@QmhEsn~z9_h9vNM!|3fx z6yAED5-6mbdP%Ft3g`Mv5&hOEPt#3ycNd6VM z_Wt46P-ku=$NDu6vlrw{7%W*Bvzr-Vo@(ac4PSz_*vAxX%8uaf^Wp7<2ujs1Yb@`v zykot_y(h}bzmNf!j78>rjOln6P}$oO@{d_JEo>=s)@F<>((zvpIMjyhAtFej75HI0 zit+ULc}JUHEU-CiqRdqVjH{=cqWs6OeVOByjT$?(UMC%A75$*nG_bihz)%LI9k1}% zUlJH*5_yeSu@CCPD*^|r2|xK?TSRHioRYx|j;T1o|C%SCeJD0)(EU3A#l%?zyBE~e zm!k}EqrZY=58WyZ&Kd`e02|i9Rt8L4#j;_I-0zmrxM&aW?+DvggsM}E_d8O7>_;LO z+8@RCo$CD7g5t(_S#3lBhz^f#Xp;L#=so83c>(jC+*3%8-sGPpX#f8KR`Q{nP6r4H z=F{n)MUM`WPc?)I|NOy3lS+J}pKq1Dr3e2v({kpo+5g3-0LBuKkIiCQt;KzHZ;^-C zFeOKrUeP~-lZGadshE8pm}pBf5l;Gn&ryU%s<3rU09{lbI6D{`B7Exo)#cH_1qvAV zT6l^kEHzJ7p)=tQ381VWKZ4({8H>iUM1u$7C|k0t&VYSJIWF3FSqUCer+e>RGEE-5 zuUQFcewe0*-o#v2Ovu^)4BwuCwIjVhva5ppY96G?{jB^DdgQ&IU_qEQ4jEYovsuKG zBqH%kg^>IB3Wy!T;zF~nM80}>I~f(E3Gd%~>R3ESAyY+#w@NTFkBKja1M0e6^p+%n zHgGN%EUn{?Wnm6fwDRHe#=LufM6T+s6kW7jR+|6liR9`xqleCTP_ zz6t;A?%W4{m@0b{Yw)20=!$V?`5PaYV-#cK0by~>XPH58j0XQV<*&xW6RCy~Arw-@ zkNd6?nBWyT48ts=F#$d3?8>vO#B&c9rBkj9WGDHG0g_8q_RdCd|~4`CN{CFBeGZ)MtE+o+qdi!B#82;(aikdoh-m#n`Hu3 z`UDTbQ`E&8-t3{o=(hkdW_4qT~K01zS($xW|K##&RD9CMyq2hxd%hi!zQHM-&|0kT7irR z7GbE!G>BJ{B+&q^R0Jy{7HyXC60WbS!~Z<=DxfB4aCK5q9lt!txG61~3rUlW^ENmd zOjd2y8}1SM{)1*lJ0L!EE$(E0SEXQAZlvu5ma{wk1OIUKO4rds2(mCwJm7Mejttxv zR-#G6G6C3ri&=RIc)5uz_QhdYb-wh1ke&y*@#7_LSorj2mSHf z1u5HW%7b?SS8Q@i!muHs2+7ueo0!?z1Lr0e%Q!2`_sD6l_94>$54Uu6tK~d50W>ffDfnnD)t!0soN#z1ISAgS>3ch8+E@4gcBumYy(#TCICcLx zcS5N5YGUCYoA=Q`1xuH-n%TiHWxXXWBvqfmGdw|QR+p<)`OdNLb$=N#sfqMid|Ja_ ze7sHCczNk7H+hJwyk|6nX6AMCtb^0xzuyc<>@lsLd&7l)bH@Q7hKsQ3>MPD{oM!y_ zbD-e44OvfN@Bxa)O9L;ogo8S<7MB-7Z)QG(0F}l}55hZmNi14bMb!{i(Mon0hRiBh%c&{5A%sL!&t)Y(k^d0vf`V ze#E^_OAx*ot3J!}sDtO2V$WmmVvBht;9dpeO+?m1lws=*IfSfqEgR=xH7qKzV;PSD z?ML(at@#`=Dm!oHgazWuXYeVnE!)%+R_}iusZ_LcojK`fR4mggJxIlyI0zhC!gfvh zGm2`o{!aW3bZL4ijB)h4@08|1P-$B78qguhkSCBT0ZT)93Mg~yC}ExBcptB6=7wYC zyIbLl)SCSq=N7?R6y&BsWT&3;A8BN_$=|DeoI&K+&>OMD9BMW|y-$ z2{g7uPy8uHgsovetD|h}J&9;36bQOl-rDnGgqvDE(p&1zy9S7H!mAn+X0rB<$2y=jr)4>gcS{pjO{{VRrtQhFkoy9 z?3|*$$tXi?J(WjdUJ;N;%5`Yp?&KFJWfmbeuY5D5u?p0 zz-*nIX7X51F?)8|S&b4u|Wy~lvt~L`>GreUdFmpB7$!dTA&<`26_Nt3aIYc(D)tp%22u(haY0w?y z_Kn`&g8po(LSBqjEpT7u2%JbfF}ou)C@`L$^?6X)ajAPhXxy(_q*Whap~Fa(mItM& zc*FpsupphO@N=B4cVbuPYO}sf2TK+v>Fv{XJp@zP@EGMxxa6uTm@zRM;^O|ZPE21l zm+XmgOgpMl2s%eydbnG^L@)pIyeNJmc=ww?_wSL8H#pThF{_sr$FN@+qHY19h0b=h zkos^&*aI6l0gHo+d$(v2du&oz-yt#~sW4$}aIfQ_E_PD`eInmPvF9r>YJ1o_s`zjwYIUm@OHF) z*9_^Q#!!v7pt#oEs=?!~KQ$E#(ng|u@JJ2ih%#UnS*G(t&a?!HWrCgNI0?bJ6fvIN z3PG^HJi?tux{3#>$K<(twZorHN;r6@@`*pkDmjXQcWx_- zQYN-A`TL*d6{-#`8O?(;DUf{g_ee2s`U?&2*dEQ;gZAFY?QJl!FOCYa}xF9s~ z2@{Xb4&eUv!31D70D8Wr4q*DzJmVD{D~+qBMmGhy9ZTN5QGDZ=SXC$pF_3k~I0^s8 zb#a^HwkF)a{ZFv`u^zmmLdYMr2jwNh=RbzZ46phnJMLvp_OeB&^+cHXxcfku21f$f zyV)P~aWG5Nx3B3_$jYt8k%_ia@Jv3Z`ir~-$)y&M@B>Ls`)_PCjAy&B2393eGN3EC21@-H0G+(7;o6KNp z`hdPWqiMv1Egbs#J8Mg*=$=AOW;i>Jj_DlH1tm^2u(5ImP0-5@b?42Iw3~qTIKE?~?%P?=ky}%k z=1^t3(EuJtVf27Xr&H%`QbXBr&3|X0y4owd^lgsAe;}NQJcR@+o$5T*VnZX;5b)PS zK1nTQH~H*07Dv5GP;3d+o-fci!(W4QikeXB=)&(E^C^vqV8Io*ratkYHFa<$f?YhF z)AE@V%YxkbZ0kG~kb^tgcNbL!NrjyZtCFt)FI2HjuIGEI5PDaF8-O@i+RxH6Gm970 zL?~U_TC+mrT+XS5TViZkVt5R$X=2RXJUOU}Az}$cTHSi>t#5>4oVISj-NyWKbf=&vV)>WsAv9Wzc2_nbFzj%P zN(~cA_cIBouc8%3tZUFnLs=(eW!Fs~s{MRkypbAgUVdQ)*Jg(8!?-H?ei`JK>w~oIc(55Oq^M zu`z+i7<{aQVcT00w-3Yle%H%V{O#>cFWkg zlDj?HEk|45YIw^+W6XcX<2GB5#$jN!EhQ9q)0(|?+Ys|sG0<1#Y;1%M>*n#>wmzY? zJAnfe=ze5_BmzKIpOXFCfrVD9O$0%%5RDgTG{!W1ZpKj{r|m3*?BwqXn69Vhd!7nX zGMvW8yn?HgCv%vf{I~J#55kEPlUtmyOP%BmMXoEeM_tP~^byZXfzCouDx>IbS2E0G z;f}p3#l1565_A#6tm4dTXvJhSs3Fwa!4PPvY^j36SuPMmQrPT3L}_AD$JIRB*%ZCq zIPf4)IjXi5W5h`yj7rE3GeQGmDwaVhK$tPx9r@}pqqQ+{^7OJ^v_5p?aH~3e0Ok2} zX7ad^F^tHW_C8o>h5I=AqBg|I2%88)k`{NO5`}&|2CBoE#%FRvQ&zFe-FxP{3XG3l zXI4fl#4ePNafel4GSyr*c=xEGZ?Ne)kxWQuH8qWRoA1J8BE~-Bek>@?r48rJ%`1wU zfADc~NwMoS%eE!_JE^dvkyZXhVC#0KL3#U8Bser9+y#f|h%}xwt#=<`-Zqy3ONn=J zl*Mh&i?&JQ9#-2hebFT@-j8l9Pzlh)N21{^@X-b+3Y;?t!Vm-dpq#Vi>c0_3Tz_hJ+Hqrc z#TT;^8;`Z=6mg7HlXPRXp^1a34Henlcdwy$+#0cb__H!dnX#tMu!Gy!aEVl61Tv>d ziM!iuP~_jrhbZiqKVop*hQR3AA6^(i6VokIG5;Le5e&4t$92H3E~d~~^0$I<$_qz( z?E~}-oJL1vpUFE{Kwn9xbq~jxB0{cJ^(WhFMlSy+&Q(DyiuveK@BGy2U~D*zCt zXyrGu)v?|ekb2IO%(rkbC|vfEQS29lL+7Tk5ab_cShU;E^%WfBpo&eDG%rMX7W;&B zmcoO88LMv08g>uha-n_##MqaBU3tvgr>8HcOvXKR!|Oqg*x6{b=5?5tWtqpqPx9zU zePr>(67^?I*7Gm~h%4&4UT1w2M4A?AQ?(wC6})JspllgS>)N9456hPV_0ydhU!4nH zfTeXFDBu@QdWDeOc4Y9Vt#rA0p_F;jZP;gu%H;@eWTgw-z~jlJsD;^4ZT!FQ1==X} z-;=?7H^ZZSMvr1vcx>*v18P9B#~{0l;vFY^E7t~a#O%JUPnV>u9p6WpZS-H;ujllr znVC%!5-OgtU)p&}<7*Uj<8K=^pRp)6)%IN+DjDR4Dk=-4$VM#=s{pB{Tfc9{FUe;Z zX#SCpTS~N9*zEXMozdtOt8d&I#Pi9>#n~|P6&e0(oQ=`-;|@CIS)J1yGp>Dx0rPpl zOabM4gqk6M!I}J23q%kiKR{?nzGXvU3S;;&{cQ-jzX9Mx$tK~!_EWHXB2B;F%zEMt zSL_<=W<(bD^2T~`5Qp%gsvuyi8kY3o0gYTz>Un+I zP&%}n#?UNb+JIn(HX%HNQ!V@X!SlckAO=JsZKegB))^sfhF;OMChiG9+#8&l8eoIM zqj~}xA!!948tx~kqJ$u_s;ZEkyL<>3X?fCxW+(R8BLEna_#p#|XLZ6zB{~*?s4cU0 z?fF8r8NZ_&(wx*M%0xv!uPag9yzLixpSCSaDo@lv5PaHfJ>hYnuG$Za$`tK4+PV26 zVCf3w`$5$(^UgQ;92eSQqrC%)Ko?^_p5#bQTPmS{ z^M7Cpgu7>encB6|Wago7zMD(f02xRUY2KY)nBv0c>8RXjw-&jEALV~ZC1z9sFQMUD zP~Ou4)2;hgs^CZ+I=*}k9~Z?YsO#xgUk5T=sO(Rk$dTdSozpz_xfdiT*=R7$F%)=+ zNUK|MmFA=F=s~$qVGP_8${r$Ew}68x!DiS^)BU^SPnU-&cuatyfVrF~HJrf^_z}l; z@H~9+{?SU;$v&`dT4;1eQs(kka|CWE*;~yy5I0l;ZCuNxbfQTtHZC+IUi>3yGG-%~iPK~NPiV>mC9 zfcMaJAW2?qyQ4_g5C^4%nR@&IRt`5xaV9hzbL`JPWmEp&4_J#z3&tQ3=&`5}_srkc z!UhY5E@x0oRZZ%-otOY=nNd&4Q$!uiZdMPTVW5BYf6>ccYs*CGsiD4k{5N z(fe(g8l_aKq)sp6IyQ-m4n8mvAC2(>qy%hUQZFruPO>NgVO3)lU#_a~Y94hFa1Ykp-;eO?*Y2M<98KGLu;* zw=*j(lxgEl+{Ux`N_S6{2Z)UQKk*@ys8X}iDX$D}d_ePb2}tY$x%8c!Q0$|#M3S<= z&@iZj7-LdhGdC=r?d7%GPab8yH#5IM@YXnlOXq9Az$Ab8ivl!?F}k3ECz17Wn$l-f zGNl0{jzPzR$W(wnbqXS0&LPrpeqUOG&2?K8WU`W3=9Rg!1l-xLM9(=th2U!Az)4u$ zs$d@(3P!&j=RfL%HJD7r{Rd9ow%)zC#E{k?^$PwfrI@K_lVkbtUI5nGE>xo=iI2#z0%)Ma_CrVQccbn$_DaelUi>mrQ%;xid~ zCwgtl#UKtAm`IgQ3v@J`66DP@-n6H=&t?meT>IL zEpd}ob|2?6LE1{w^vg#3QS{ltx)vXsKihzqVoL^L^(|HTUw^u%rsP`^O8(^K&4{7% zCFai=_XCf^p_lB57Q`u|S!EPOZ}68FH*$1lBZm#Tk(3Q=&VVD|jhDjKIf%AG=$_7E zrdou0;@IEX=UHAWcxsADWRjsLd;~kkZ@HW)%&c)1@RFpx>rhO8dlyG3v+cw{pPfZ% z)3?wSe-KRnAb+LOI7IgIq%eDHj)gEihb4-uJWi!5vWs#!mr6n^rIA}e!jmIJb%d&H zS!IpyHwVvf*Tc`!S*AQ*;D%pUjMF#|)O-KYLp^(nn~0zAH1=MAHsg3rb+dC}n?>fx zqm`tavFzNWa!%uCilX0Wz$YeN5&*&m*B-y z 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 ^ed#oAV=fW2Zrpk#zT%aT<1ids(_*A%B(t@DaA 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 z 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�qs~?~VmHfJ5#giD*r+06W^hwv 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

    fky{nG>Z_gKLaF>M6od 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