Compare commits

...
Author SHA1 Message Date
Kenneth Skovhede 621aada0d1 Reworked the task checker to use polling instead of relying on events 2024-07-19 09:33:20 +02:00
Kenneth Skovhede e5a3fed155 Stringify output 2024-07-19 08:58:48 +02:00
Kenneth Skovhede 67b133a206 Debug restore UI failure 2024-07-19 08:53:09 +02:00
Kenneth Skovhede 5b1df6fa61 Preloading 2024-07-19 08:44:26 +02:00
Kenneth Skovhede f50b49c56e Enable websocket by default 2024-07-18 23:38:33 +02:00
Kenneth Skovhede e05888e557 Testing wait for clickable elements 2024-07-18 23:28:45 +02:00
Kenneth Skovhede c36ec0cbc5 Added support for disabling websocket, and set it to default disabled 2024-07-18 23:17:15 +02:00
Kenneth Skovhede b968dba9ce Guard against multiple guard calls 2024-07-18 23:16:36 +02:00
Kenneth Skovhede 7a85b1763f Better enabling of logging 2024-07-18 22:51:33 +02:00
Kenneth Skovhede 599b4124f2 Test with logs on 2024-07-18 22:44:05 +02:00
Kenneth Skovhede 037e9e179f Merge remote-tracking branch 'origin/master' into feature/remove-httpserver-with-debug 2024-07-18 22:30:09 +02:00
Kenneth Skovhede b9d6084973 Yet-another rework of the reconnect 2024-07-18 22:28:18 +02:00
Kenneth Skovhede c52f08bc6e Added guard against case where the command happens faster than the events can register them 2024-07-18 22:27:39 +02:00
Kenneth Skovhede 85d5b1d74e Fixed error messages for requests with no response 2024-07-18 21:31:16 +02:00
Kenneth Skovhede 63fc93ab38 Updated localization files 2024-07-18 18:18:18 +02:00
Kenneth SkovhedeandGitHub 790f9c06fa Merge pull request #5274 from luixxiul/placeholders
Update placeholders without 'translate'
2024-07-18 18:15:57 +02:00
Suguru Hirahara 12407d4c03 Make placeholders translatable: storj.html and tardigrade.html 2024-07-18 11:28:41 -04:00
Suguru Hirahara 090ecf1af6 Update placeholders without 'translate' 2024-07-18 07:40:50 -04:00
Kenneth Skovhede 105f6e16ff Fixed origin mapping 2024-07-18 10:51:58 +02:00
Kenneth SkovhedeandGitHub 499a3ad575 Merge pull request #5262 from duplicati/feature/update-uplink-fixed-native-binaries
Prepared for new Uplink packages that correctly package the Storj binaries
2024-07-18 10:47:57 +02:00
Kenneth SkovhedeandGitHub f0107b476a Merge pull request #5270 from duplicati/feature/update-localizations
Update localizations
2024-07-18 10:35:37 +02:00
Kenneth Skovhede b7cac4cd51 Removed mixing url with hostnames 2024-07-18 10:34:13 +02:00
Kenneth SkovhedeandGitHub b55ba094b6 Merge pull request #5271 from npodbielski/feature/remove-httpserver
Add websocket support to httpserver
2024-07-18 10:33:04 +02:00
Kenneth Skovhede fecb2bf25a Updated to latest uplink.net packages 2024-07-18 10:08:57 +02:00
Kenneth Skovhede 877b1c811f Updated pot with new strings 2024-07-18 09:57:45 +02:00
Kenneth Skovhede 3bb06311a8 Added localization on additional strings 2024-07-18 09:56:19 +02:00
Kenneth SkovhedeandGitHub 8d96681db6 Merge pull request #5265 from luixxiul/DescriptionS3ClientLong
Fix string `DescriptionS3ClientLong`
2024-07-17 22:18:20 +02:00
Kenneth SkovhedeandGitHub f458355663 Merge pull request #5264 from luixxiul/B2applicationkeyDescription
Replace `B2applicationkeyDescriptionShort` with `B2applicationkeyDescriptionLong`
2024-07-17 22:18:07 +02:00
Kenneth SkovhedeandGitHub e9d7433a72 Merge pull request #5263 from duplicati/feature/report-errors-when-sending-http-report
Added check for valid status code after sending http request with report
2024-07-17 20:48:17 +02:00
Kenneth Skovhede 6195e07808 Updated translation files 2024-07-17 20:32:22 +02:00
Kenneth Skovhede 53059c01dd Pulled from Transifex 2024-07-17 20:26:03 +02:00
Kenneth Skovhede bb274822cc Fixed scripts and config to follow updated TX client 2024-07-17 20:25:35 +02:00
Kenneth Skovhede f74cee2932 Fixed #5267 2024-07-17 19:34:26 +02:00
natan 92f1de07ae Bring back /serverstate endpoint. Fix unauhthorized mdodal when cookie is missing.
Small code cleanup here and there.
2024-07-15 11:07:47 +02:00
Suguru Hirahara f040df8e2f Switch B2applicationkeyDescriptionShort and B2applicationkeyDescriptionLong 2024-07-14 22:38:58 -04:00
Suguru Hirahara ab7e640435 Fix string DescriptionS3ClientLong 2024-07-14 22:36:20 -04:00
natan 27ce549e75 cleanup WebsocketAccessor.cs and StatusService.cs a bit.
Remove /serverstate endpoint
2024-07-12 17:59:21 +02:00
natan 09d30b92e0 Merge remote-tracking branch 'refs/remotes/npodbielski/feature/remove-httpserver' into feature/remove-httpserver 2024-07-12 16:55:59 +02:00
natan bc0a47f768 Merge remote-tracking branch 'origin/feature/remove-httpserver' into feature/remove-httpserver 2024-07-12 16:53:18 +02:00
npodbielskiandGitHub 75bab41034 Merge pull request #1 from duplicati/remove-httpserver-updates-to-reconnect
Changed logic for reconnect to not rely on serverstate API.
2024-07-12 16:53:04 +02:00
Kenneth Skovhede 94d6ab76ee Changed logic for reconnect to not rely on serverstate API.
Changed websocket to send current state on initial connect.
2024-07-11 16:24:22 +02:00
Kenneth Skovhede 1d913e026f Added check for valid status code after sending http request with report 2024-07-10 09:22:07 +02:00
Kenneth Skovhede 974f6bff2f Removed unused file 2024-07-10 09:20:18 +02:00
natan 0ae9990f18 fix 401 in websocket 2024-07-09 15:49:17 +02:00
natan 18ff96c5fe Merge remote-tracking branch 'origin/feature/remove-httpserver' into feature/remove-httpserver 2024-07-09 13:24:13 +02:00
Kenneth Skovhede b39a68bda7 Prepared for new Uplink packages that correctly package the Storj binaries 2024-07-09 12:15:12 +02:00
Kenneth Skovhede 60437d7fc0 Removed duplicate entry in project file 2024-07-09 11:24:17 +02:00
Kenneth SkovhedeandGitHub 618d9015ab Merge pull request #5261 from grzesieklagocki/copyright
Add copyright information to libraries #5258
2024-07-09 11:23:32 +02:00
Grzegorz Łagocki 9b19bac7da Add Copyright Property to *.csproj files 2024-07-09 00:37:18 +02:00
Kenneth Skovhede 6917030417 Treat all auth operation failures as Unauthorized - 401 2024-07-07 10:36:42 +02:00
Kenneth Skovhede 44a45fcd04 Only generate signin token if password is autogenerated 2024-07-07 10:36:11 +02:00
natan f7656f464e fix websocket connection 2024-07-03 22:26:03 +02:00
Kenneth SkovhedeandGitHub 59b00a5a45 Merge pull request #5254 from duplicati/feature/update-mega-library
Updated Mega library to latest
2024-07-03 09:52:51 +02:00
Kenneth Skovhede 31f0cd02ab Updated Mega library to latest 2024-07-02 10:59:18 +02:00
Kenneth Skovhede 627c270025 Merge remote-tracking branch 'origin/master' into feature/remove-httpserver 2024-07-01 22:42:20 +02:00
Kenneth Skovhede 5d0d9fbc80 Simplified initial load fixing for Selenium tests 2024-07-01 22:40:36 +02:00
Kenneth Skovhede 92fea213f9 Removing unsupported custom package files 2024-07-01 22:34:25 +02:00
Kenneth Skovhede 4fe138097b Experiement to detect failed page load 2024-07-01 22:33:54 +02:00
Kenneth Skovhede e25df96d23 Adding cache headers 2024-07-01 22:21:51 +02:00
Kenneth SkovhedeandGitHub 8d82363163 Merge pull request #5251 from duplicati/feature/lower-case-docker-tags
Making docker version tags lowercase
2024-07-01 17:58:31 +02:00
Kenneth Skovhede d7345ad993 Double loading guard 2024-07-01 17:58:16 +02:00
Kenneth Skovhede 85368266ed Added two more loading guards 2024-07-01 14:54:47 +02:00
Kenneth Skovhede 38ac29be13 Making docker version tags lowercase 2024-07-01 14:25:06 +02:00
Kenneth Skovhede 5e9ec4ee98 Fixed selenium tests 2024-07-01 13:36:27 +02:00
Kenneth Skovhede fef9efe4d4 Less agressive push towards the login page on errors 2024-06-30 21:36:06 +02:00
Kenneth Skovhede 2c42108df3 Disable logging to console by default 2024-06-30 21:35:15 +02:00
Kenneth Skovhede f91c4c7328 Removed unused settings 2024-06-30 21:34:54 +02:00
Kenneth Skovhede 54237b1cbb Updated gui test, works locally, not in Docker 2024-06-28 16:03:54 +02:00
Kenneth Skovhede c0f834f511 Removed logspam 2024-06-28 13:22:02 +02:00
Kenneth Skovhede a87984806e Fixed some broken html 2024-06-28 12:49:17 +02:00
Kenneth Skovhede 1c40025666 Fixed missing namespace 2024-06-28 12:21:00 +02:00
Kenneth Skovhede f9170602d3 Re-introduce sudo :'( 2024-06-28 11:58:12 +02:00
Kenneth Skovhede 6cebb6b84f Faster timeout and less sudo 2024-06-28 11:48:07 +02:00
Kenneth Skovhede 8f56220f2f Added support for mandatory login 2024-06-28 11:37:16 +02:00
Kenneth Skovhede be9a3667e6 Fixed a race in refreshing the access token 2024-06-28 11:17:14 +02:00
Kenneth Skovhede c023e29020 Removed proxy support for now 2024-06-28 11:04:12 +02:00
Kenneth Skovhede 8a02dc014e Fixed issue with repair 2024-06-28 10:38:21 +02:00
Kenneth Skovhede 3f51536073 Fixed issue with fetching logs 2024-06-28 10:37:47 +02:00
Kenneth Skovhede d2e91f8db6 Fixed issue with import 2024-06-28 10:37:33 +02:00
Kenneth Skovhede caf7d53583 Fixed auto-login issue 2024-06-28 09:14:08 +02:00
Kenneth Skovhede d3a8ecacad Implemented single operation token.
Fixed export and bugreport download.
2024-06-28 08:56:00 +02:00
Kenneth Skovhede d68d94e7f8 Version bump to v2.0.9.101-2.0.9.101_canary_2024-06-27
You can download this build from:

Binaries: https://updates.duplicati.com/Canary/

Signature file: https://updates.duplicati.com/Canary/2.0.9.101_canary_2024-06-27.signatures.zip
2024-06-27 14:25:55 +02:00
Kenneth Skovhede dab2b23332 Commit before reverting changelog file 2024-06-27 13:42:17 +02:00
Kenneth Skovhede 68ae2a460a Added missing changelog entry 2024-06-27 13:38:25 +02:00
Kenneth Skovhede d261899189 Fixed nonsense commit messages 2024-06-27 13:36:30 +02:00
Kenneth SkovhedeandGitHub 002a47bd1e Merge pull request #5219 from duplicati/feature/update-sharp-aes-crypt
Updated to SharpAESCrypt 2.0
2024-06-27 09:00:25 +02:00
Kenneth SkovhedeandGitHub bb9be1ee4b Merge pull request #5247 from duplicati/feature/recovery-configurable-maxopen-archives
Made the number of open archives configurable
2024-06-27 09:00:07 +02:00
Kenneth SkovhedeandGitHub 17e2266a69 Merge pull request #5231 from duplicati/feature/increase-default-test-coverage
Increase the default test coverage to be 0.1%
2024-06-25 09:58:50 +02:00
Kenneth Skovhede 41b772d385 Documented extra option for recovery tool 2024-06-25 09:54:49 +02:00
Kenneth Skovhede c50e027ecd Made the number of open archives configurable as it can consume some memory. 2024-06-25 09:51:29 +02:00
Kenneth SkovhedeandGitHub a5a03c2a41 Merge pull request #5241 from duplicati/feature/improve-recovery-tool-speed
Improve recovery tool speed
2024-06-25 09:48:58 +02:00
Kenneth SkovhedeandGitHub 991870b9c3 Merge pull request #5234 from duplicati/feature/support-script-arguments
Added support for passing arguments to scripts
2024-06-25 09:48:39 +02:00
Kenneth SkovhedeandGitHub 5de458e74f Merge pull request #5218 from duplicati/feature/upgrade-cocol-1-8
Updated CoCoL library to 1.8
2024-06-25 09:48:02 +02:00
Kenneth SkovhedeandGitHub 8b908cb43d Merge pull request #5246 from duplicati/feature/add-armv7-packages
Added Armv7 deb packages
2024-06-25 09:47:47 +02:00
Kenneth Skovhede 1b3b7fbd1e Removed numbers fro help as they are settings dependent 2024-06-24 19:19:09 +02:00
Kenneth Skovhede b9031e9f20 No need to fix the name, we are only looking for 'token' 2024-06-24 19:17:27 +02:00
Kenneth Skovhede 21fe0f9cfb Fixed a few logic issues, restore flow is now working 2024-06-24 16:00:05 +02:00
Kenneth Skovhede e0d93da2f5 Removed last remaining form input 2024-06-24 15:26:07 +02:00
Kenneth Skovhede b515a6f6f2 Updated to use gzip compression on deb packages 2024-06-24 14:20:28 +02:00
Kenneth Skovhede 6998cc1e28 Added Armv7 deb packages 2024-06-24 12:02:40 +02:00
Kenneth Skovhede 1acda72561 Fixed not creating an empty database 2024-06-24 10:14:03 +02:00
Kenneth Skovhede 246c95c60a Added support for an offset in the recovery tool 2024-06-22 10:27:51 +02:00
Kenneth Skovhede 0855d7fe4d Improved error messages for better client handling options 2024-06-21 16:26:06 +02:00
Kenneth Skovhede 5d7fb66c1c Added some statistics to the output of recovery tool 2024-06-21 15:42:24 +02:00
Kenneth Skovhede 6ab2a63218 Extended recovery tool tests to cover more combinations 2024-06-21 15:15:01 +02:00
Kenneth Skovhede b699937dee Optimized the recovery tool performance. 2024-06-21 15:11:37 +02:00
Kenneth Skovhede d6fe8c1a7d Fixed some naming issues for parameters 2024-06-21 08:00:57 +02:00
Kenneth Skovhede fa7d5e45f7 Fixed auto-reconnect 2024-06-21 07:52:39 +02:00
Kenneth Skovhede 8efab7f746 Fixed an optional parameter 2024-06-21 07:52:28 +02:00
Kenneth Skovhede 3f29331dc6 Fixed a case where listing would fail with empty filters 2024-06-21 07:52:07 +02:00
Kenneth Skovhede 92b82df78b Disabled websocket as it is not complete.
Fallback to longpoll :'(
2024-06-20 22:46:42 +02:00
Kenneth Skovhede fd9b1a1c5b Remove XSRF 2024-06-20 22:43:51 +02:00
Kenneth Skovhede bbff638212 Better support for temporary backups 2024-06-20 22:23:02 +02:00
Kenneth Skovhede c9f8e5786c Minor fixes 2024-06-20 22:22:49 +02:00
Kenneth Skovhede 4392d84da4 Improved startup error detection 2024-06-20 14:27:09 +02:00
Kenneth Skovhede a47af06bb7 Added throttle for request retries on failures 2024-06-20 14:22:49 +02:00
Kenneth Skovhede 34821192fc Fixed hostname filtering 2024-06-20 12:59:24 +02:00
Kenneth Skovhede 3487f2fdf4 Make sure we do not log out users who upgrade 2024-06-20 12:24:14 +02:00
Kenneth Skovhede d3ec5f54fe Missing commit 2024-06-20 12:16:07 +02:00
Kenneth Skovhede 0f1d381c7f Implemented automatic re-login when access token expires 2024-06-20 12:11:54 +02:00
Kenneth Skovhede edc0fb5e4b Fixed re-login for tray-icon 2024-06-20 06:57:46 +02:00
Kenneth Skovhede 25a7667e36 Minor updates to JWT issuance 2024-06-19 22:43:52 +02:00
Kenneth Skovhede d463e6bda7 Removed extra public from interface 2024-06-19 22:38:32 +02:00
Kenneth Skovhede cddf0917e3 Fixed build 2024-06-19 22:37:54 +02:00
Kenneth Skovhede 6945270d49 Setting cookie name depending on port 2024-06-19 14:18:50 +02:00
Kenneth Skovhede 3f29642f6c Implemented JWT tokens.
Added auth requirement on all endpoints.
Added SignIn-, Access- and Refresh-Tokens based on JWT.
Upgraded stored password to be based on PBKDF2.
Added default password assignment.
Updated logic to support auth-header and re-sign-in for tray-icon and web-ui.
2024-06-19 13:54:10 +02:00
Kenneth Skovhede bb73f8f22e Don't pass script as argument to self 2024-06-17 09:54:08 +02:00
Kenneth Skovhede ca4fde938c Revert "Added option to use shell-execute with --run-script-use-document-open"
This reverts commit e32bba1dfc.
2024-06-17 09:47:09 +02:00
Kenneth Skovhede e32bba1dfc Added option to use shell-execute with --run-script-use-document-open 2024-06-17 09:39:17 +02:00
Kenneth Skovhede 1b7e1349ca Adding option for enabling argument parsing in scripts 2024-06-17 09:32:04 +02:00
Kenneth SkovhedeandGitHub 9ca5489ef0 Merge pull request #5235 from IIPoliII/master
Update Poli Systems S3 Provider URL's
2024-06-16 22:20:08 +02:00
PoliandGitHub 9d7fad14cf Update S3Backend.cs - Poli Systems GmbH
Updated the S3 backends of Poli Systems.
S3 became S3-02 and there is a new backend S3-03.
2024-06-15 01:57:45 +02:00
Kenneth Skovhede 828ba240f3 Implemented simpler argument handling based on feedback 2024-06-13 07:40:09 +02:00
Kenneth Skovhede 486e13dd51 Revert "Added support for passing arguments to scripts"
This reverts commit 9b9e8a2adb.
2024-06-13 07:24:17 +02:00
Kenneth Skovhede 37071136b7 Revert "Updated example files to mention the arguments more prominently"
This reverts commit 763048c8bd.
2024-06-13 07:24:07 +02:00
Kenneth Skovhede c0b8129924 Fixed log data 2024-06-13 07:23:30 +02:00
Kenneth Skovhede fe90f8ce3d Consistency update 2024-06-12 22:57:50 +02:00
Kenneth Skovhede 829c434c64 No more appsettings 2024-06-12 22:57:02 +02:00
Kenneth Skovhede 8b8707c811 Moved license to use repo license 2024-06-12 22:56:53 +02:00
Kenneth Skovhede 87c92d40ca Setting commented out conditions, in case we get around to fixing them, 2024-06-12 13:55:15 +02:00
Kenneth Skovhede 763048c8bd Updated example files to mention the arguments more prominently 2024-06-12 11:11:40 +02:00
Kenneth Skovhede 9b9e8a2adb Added support for passing arguments to scripts 2024-06-12 10:54:53 +02:00
Kenneth Skovhede 47ca64ee63 Removed browsertest from sln as it does not currently run on non-windows. 2024-06-11 22:03:07 +02:00
Kenneth Skovhede e88c30edd1 Fixed import/export test 2024-06-11 22:02:45 +02:00
Kenneth Skovhede 4294ee3611 Removed last traces of the custom operating system detection 2024-06-11 21:30:33 +02:00
Kenneth Skovhede 1920b0684d Merge remote-tracking branch 'origin/master' into feature/remove-httpserver 2024-06-11 21:22:29 +02:00
Kenneth SkovhedeandGitHub 973d005c07 Merge pull request #5230 from duplicati/feature/opt-in-local-block-use
Changed logic for local blocks to be opt-in
2024-06-11 10:45:11 +02:00
Kenneth SkovhedeandGitHub f70748359a Merge pull request #5211 from duplicati/feature/remove-alphafs
Removing unsupported AlphaFS.
2024-06-11 10:44:49 +02:00
Kenneth SkovhedeandGitHub 8be2a14f66 Merge pull request #5217 from duplicati/feature/remove-bundled-gpg
Removed bundled GPG binaries for Windows.
2024-06-11 10:43:25 +02:00
Kenneth SkovhedeandGitHub f20d7b9321 Merge pull request #5222 from duplicati/feature/some-s3-option-cleanup
Removed S3 options that have been deprecated for years.
2024-06-11 10:42:32 +02:00
Kenneth SkovhedeandGitHub 1a44be4199 Merge pull request #5221 from duplicati/feature/select-aws-region-endpoint
Update UI to rely on default region hostname for AWS.
2024-06-11 10:41:32 +02:00
Kenneth Skovhede 0e93df8e3e Fixed tests 2024-06-11 07:47:08 +02:00
Kenneth Skovhede bb65e89139 Added missing switch 2024-06-11 06:57:37 +02:00
Kenneth SkovhedeandGitHub e4c578afa2 Merge pull request #5232 from Jojo-1000/fix-compare-exclude
Fix compare on linux showing 0 bytes when exclude filters are used
2024-06-11 06:51:45 +02:00
Kenneth Skovhede d264dbd329 Introducing decimal commandline type 2024-06-11 06:50:25 +02:00
Jojo-1000 e316944a50 Fix compare on linux showing 0 bytes when exclude filters are used.
Closes #5229
2024-06-10 16:00:39 +02:00
Kenneth Skovhede f4dee8a305 Increase the default test coverage to be 0.1% 2024-06-10 11:49:43 +02:00
Kenneth Skovhede f9db217af0 Changed logic for local blocks to be opt-in 2024-06-10 11:37:58 +02:00
Kenneth Skovhede 773f3acfb3 More untangle for FIXMEGlobal 2024-06-09 22:12:03 +02:00
Kenneth Skovhede 270f2f9cea Some work on untangling FIXMEGlobal 2024-06-09 22:00:34 +02:00
Kenneth Skovhede 25bb4f3701 Merge remote-tracking branch 'origin/master' into feature/remove-httpserver 2024-06-09 21:54:02 +02:00
Kenneth SkovhedeandGitHub 9dff0fa953 Merge pull request #5225 from duplicati/feature/license-cleanup
Removed unused Signer.exe program
2024-06-09 21:48:58 +02:00
Kenneth Skovhede 0e12adb210 Fixed usings after merge 2024-06-09 21:33:48 +02:00
Kenneth Skovhede 1d0d7e0152 Fixed library platform usage 2024-06-09 21:24:32 +02:00
Kenneth Skovhede ca425808c5 Merge remote-tracking branch 'origin/master' into feature/remove-httpserver 2024-06-09 21:23:59 +02:00
Kenneth Skovhede 6878a601a9 Fixed XSRF for import/export feature 2024-06-08 13:59:33 +02:00
Kenneth Skovhede 9f79025744 Removed HttpServer.
Re-implemented everything using ASP.NET.
Changed some requests to use JSON instead of FORM data.
Some work towards deleting the FIXMEGlobal instance.
Auth is missing, XSRF does not work correctly.
2024-06-07 15:56:43 +02:00
Kenneth Skovhede 12fb0d36ea Removed unused Signer.exe program
Updated some licenses that snuck in LGPL text from the Kestrel merge.
2024-06-05 11:02:56 +02:00
Kenneth SkovhedeandGitHub bb7b3e5de4 Merge pull request #5220 from duplicati/feature/update-aws-region-list
Added additional AWS S3 regions
2024-06-05 10:55:51 +02:00
Kenneth SkovhedeandGitHub fdc891aa36 Merge pull request #5208 from Jojo-1000/fix-sql-quotes
Fix SQL string quotes
2024-06-04 20:58:20 +02:00
Kenneth Skovhede b11c558ba2 Removed options that have been deprecated for years.
Simplified the parsing of options slightly.
2024-06-04 20:54:57 +02:00
Kenneth Skovhede 2a12fdc493 Update UI to rely on default region hostname for AWS.
This fixes #5214
2024-06-04 20:37:59 +02:00
Kenneth Skovhede c0a49dec2d Removed unused US region, and fixed mapping of location names 2024-06-04 20:13:02 +02:00
Kenneth Skovhede dbdd9a0022 Added additional AWS S3 regions:
- Africa (Cape Town): `af-south-1`
- Asia Pacific (Hyderabad): `ap-south-2`
- Asia Pacific (Jakarta): `ap-southeast-3`
- Asia Pacific (Melbourne): `ap-southeast-4`
- Canada West (Calgary): `ca-west-1`
- Europe (Spain): `eu-south-2`
- Europe (Zurich): `eu-central-2`
- Israel (Tel Aviv): `il-central-1`
- Middle East (UAE): `me-central-1`
- AWS GovCloud (US-East): `us-gov-east-1`
- AWS GovCloud (US-West): `us-gov-west-1`

List of region endpoints is now generated from region table.
2024-06-04 20:00:04 +02:00
Jojo-1000 a5c6c18f26 Prevent SQLite crash on MacOS. 2024-06-04 19:35:06 +02:00
Kenneth SkovhedeandGitHub 516a8e728a Merge pull request #5216 from duplicati/feature/clean-3rdparty-licenses
Updated some licenses
2024-06-04 17:29:00 +02:00
Kenneth Skovhede 95cb6b4e71 Updated to SharpAESCrypt 2.0 2024-06-04 17:27:18 +02:00
Kenneth Skovhede 4b12109e7f Updated CoCoL library to 1.8 2024-06-04 15:33:09 +02:00
Kenneth Skovhede 7dab2f5935 Removed bundled GPG binaries for Windows.
Improved search logic for finding GPG executable on all operating systems.
This also removes the `win-tools` folder.
2024-06-04 15:21:22 +02:00
Kenneth SkovhedeandGitHub 6841ad7fa8 Merge pull request #5215 from duplicati/feature/fix-rendering-issue-angularjs
Fixed an issue rendering non-ascii in AngularJs
2024-06-04 14:07:48 +02:00
Kenneth Skovhede c57321a4f7 Removed Sharp.Xmpp license entries.
Removed duplicate MailKit license.
Added `Artalk.Xmpp` license.
2024-06-04 14:06:15 +02:00
Kenneth SkovhedeandGitHub 245d2f5e95 Merge pull request #5213 from Jojo-1000/xmpp-library-update
Change XMPP library to Artalk.Xmpp
2024-06-04 13:55:56 +02:00
Kenneth SkovhedeandGitHub 9031890456 Merge pull request #5209 from Jojo-1000/platform-support
Fix supported platform warnings (except Captcha)
2024-06-04 11:55:25 +02:00
Kenneth Skovhede 9e0ce5fb0c Fixed an issue rendering non-ascii in AngularJs 2024-06-03 16:17:42 +02:00
Jojo-1000 3ccca04f1c Change XMPP library to Artalk.Xmpp. 2024-06-01 22:44:27 +02:00
Jojo-1000 2b901b511c Change WindowsService to platform-independent target for cross-platform builds. 2024-05-30 21:06:06 +02:00
Kenneth Skovhede 7c9f964eeb Removing unsupported AlphaFS.
This updates #5210
2024-05-30 20:46:25 +02:00
Kenneth Skovhede 5312499107 Fixed issue with incrementing version 2024-05-30 15:38:57 +02:00
Kenneth Skovhede 26b77b29a7 Version bump to v2.0.9.100-2.0.9.100_canary_2024-05-30
You can download this build from:

Binaries: https://updates.duplicati.com/Canary/2.0.9.100_canary_2024-05-30.zip

Signature file: https://updates.duplicati.com/Canary/2.0.9.100_canary_2024-05-30.zip.sig

ASCII signature file: https://updates.duplicati.com/Canary/2.0.9.100_canary_2024-05-30.zip.sig.asc

MD5: 2.0.9.100_canary_2024-05-30.zip.md5

SHA1: 2.0.9.100_canary_2024-05-30.zip.sha1

SHA256: 2.0.9.100_canary_2024-05-30.zip.sha256
2024-05-30 15:35:43 +02:00
Kenneth Skovhede 1fb46d7fd2 Prepare for canary build 2024-05-30 15:34:45 +02:00
Jojo-1000 917e2cd0c4 Remove obsolete custom platform detection. 2024-05-30 02:14:05 +02:00
Jojo-1000 930f1c8bd3 Fix most unsupported platform warnings. 2024-05-30 01:50:02 +02:00
Jojo-1000 1dc1fd83af Add SupportedOSPlatform attribute and target windows where appropriate. 2024-05-30 01:22:16 +02:00
Jojo-1000 2ecfde44f9 Disable double quoted sql string compatibility mode. 2024-05-30 00:00:49 +02:00
Jojo-1000 55284410c7 Fix sql queries that use double quoted strings.
Closes #5202
2024-05-30 00:00:48 +02:00
Kenneth Skovhede 2e7b8fc468 Merge remote-tracking branch 'npodbielski/feature/http-server-modernization' into feature/remove-httpserver 2024-05-28 14:45:26 +02:00
Kenneth SkovhedeandGitHub d9174d1488 Merge pull request #5203 from duplicati/feature/packag-naming-updates
Feature/package naming updates
2024-05-27 13:11:09 +02:00
Kenneth Skovhede 7f2703c5b6 Added option to toggle self-hosted builds 2024-05-27 12:21:19 +02:00
Kenneth SkovhedeandGitHub 2e30a35da9 Merge pull request #5201 from duplicati/feature/fix-direct-restore-blocksize-issue
Implemented test for reproducing issue #5196
2024-05-27 11:14:46 +02:00
Kenneth Skovhede db60e91042 Fixed signature package.
Adde key urls as suggested in #5000
2024-05-27 10:55:06 +02:00
Kenneth Skovhede 3ebc7f7f42 Added support for win-arm64 SQLite 2024-05-26 21:24:21 +02:00
Kenneth Skovhede b13ab5e0cf Support for building win-arm64 2024-05-25 23:36:43 +02:00
Kenneth Skovhede 4b5187561a Better button alignment 2024-05-25 23:24:53 +02:00
Kenneth Skovhede 914316de39 Added missing dialog references 2024-05-25 20:48:23 +02:00
Kenneth Skovhede e0ef501e6a Merged WixUI files into a single extension folder 2024-05-25 20:01:14 +02:00
Kenneth Skovhede 529133d967 Fixed order of buttons.
Fixed running application after install.
2024-05-25 19:39:05 +02:00
Kenneth Skovhede ad4ef7ed95 Implemented the WixUI_FeatureTree with wixl.
Needs a patch for `wixl` to correctly work.
2024-05-25 17:32:26 +02:00
Kenneth Skovhede 58e37e39ef Honor the service flag and do not launch if installed with service intent 2024-05-25 10:34:38 +02:00
Kenneth Skovhede a29b5056ae Implemented slightly better UI for MSI 2024-05-24 14:10:35 +02:00
Kenneth Skovhede 15031af381 Fixed formatting on license.rtf 2024-05-24 14:07:34 +02:00
Kenneth SkovhedeandGitHub d97767689b Merge pull request #5204 from Jojo-1000/fix-stale-permissions
Fix stale action permissions
2024-05-24 11:35:53 +02:00
JanandGitHub fbff3a3ed9 Fix stale action permissions
Action write permissions is required to update the cache (see https://github.com/actions/stale/issues/1133)
2024-05-23 23:29:09 +02:00
Kenneth Skovhede 9ca797944b Remvoved extra license file 2024-05-23 16:37:33 +02:00
Kenneth Skovhede 75bb8942ca Re-introduced the license page in MSI package 2024-05-23 16:25:02 +02:00
Kenneth Skovhede b3f0b1be81 Renamed binaires pre-install for all platforms.
Improved CLI builds, especially for RPM
Fixed permission issues in packages
2024-05-22 13:00:43 +02:00
Kenneth Skovhede 6a1bd40498 Removed BouncyCastle license (project removed 2021) 2024-05-22 11:11:22 +02:00
Kenneth Skovhede 5417099e04 Fixed process name not being set on MacOS 2024-05-22 11:10:43 +02:00
Kenneth Skovhede c890e824ee Implemented test for reproducing issue #5196
Fixed issue by changing order options are written and verified.
Some whitespace changes due to renaming a method
2024-05-21 15:48:29 +02:00
Kenneth SkovhedeandGitHub db79b8e51b Merge pull request #5200 from duplicati/feature/add-missing-executables
Added executables for `SharpAESCrypt` and `Snapshots`
2024-05-21 14:23:45 +02:00
Kenneth SkovhedeandGitHub aaf1067926 Merge pull request #4977 from Jojo-1000/fix-empty-advanced-options
Do not set advanced options with empty string to true
2024-05-21 12:37:43 +02:00
Kenneth Skovhede 9d43685352 Implemented Avalonia debug logger without trace facility 2024-05-21 12:23:02 +02:00
Kenneth Skovhede 95b040d386 Added ConfigurationImporter to the package helper 2024-05-21 12:09:05 +02:00
Kenneth Skovhede afc34e00be Merge remote-tracking branch 'origin/master' into feature/add-missing-executables 2024-05-21 12:08:14 +02:00
Kenneth SkovhedeandGitHub e743cc7b21 Merge pull request #5199 from duplicati/feature/fixes-to-help
Fixed issue with recovery-tool help.
2024-05-21 12:06:36 +02:00
Kenneth Skovhede f3394584af Cleanup for the executable entries 2024-05-21 11:20:40 +02:00
Kenneth Skovhede 095a84dd4b Fixed name for snapshot executable 2024-05-21 11:19:54 +02:00
Kenneth Skovhede 67463adf00 Added configuration importer debug setup 2024-05-21 11:19:34 +02:00
Kenneth Skovhede 2a497acb56 Fixed executable name for configuration import.
Slightly more non-developer friendly error messages
2024-05-21 11:08:41 +02:00
Kenneth Skovhede 8fe14b4754 Fixed namespaces 2024-05-21 10:22:57 +02:00
Kenneth Skovhede 84d7a56ab1 Added executables for SharpAESCrypt and Snapshots 2024-05-21 09:54:01 +02:00
Kenneth Skovhede 05e5fbcf80 Fixed issue with recovery-tool help.
Cleaned up some loading of embedded resourse to rely on type instead of assembly name.
Implemented installed executable name instead of using Windows executable names.
2024-05-21 08:26:40 +02:00
Kenneth SkovhedeandGitHub 9e787c7cce Merge pull request #5195 from duplicati/feature/embed-aliyon-licensedata
Included license data for aliyun-oss with license project
2024-05-21 06:58:41 +02:00
Kenneth SkovhedeandGitHub 54b9063f1b Merge pull request #5198 from Jojo-1000/patch-1 2024-05-19 11:15:52 +02:00
JanandGitHub 3e01a9352c Update stale action and increase operation limit 2024-05-19 10:11:29 +02:00
Kenneth SkovhedeandGitHub a89ccdec7d Merge pull request #5193 from duplicati/feature/avoid-disposing-null
Added null guard for disposing (dispose should not throw)
2024-05-17 14:24:19 +02:00
Kenneth SkovhedeandGitHub e747e8445c Merge pull request #5194 from duplicati/feature/preserve-historic-updater-filelists
Added uploads to store previous versions of the package lists
2024-05-17 14:24:05 +02:00
Kenneth Skovhede 30d68fcf76 Included license data for aliyun-oss with license project 2024-05-17 14:23:52 +02:00
Kenneth Skovhede af75c2e6c9 Added uploads to store previous versions of the package lists during release 2024-05-17 12:19:55 +02:00
Kenneth Skovhede 69a6c09699 Added null guard for disposing (dispose should not throw) 2024-05-17 12:16:55 +02:00
Kenneth SkovhedeandGitHub 91f82a1a1c Merge pull request #5189 from duplicati/feature/micro-fixes-net8
Minor fixes to runtime and packaging
2024-05-16 07:50:43 +02:00
Kenneth SkovhedeandGitHub 6868d1f053 Merge pull request #5191 from Jojo-1000/patch-1
Fix missing auth-username and password in openstack
2024-05-15 17:37:45 +02:00
Kenneth Skovhede 279a0d3ee1 Updated with an init handler that delays all actions posted during init until after the tray icon has initialized 2024-05-15 17:34:49 +02:00
JanandGitHub 55cea2845c Fix missing auth-username and password in openstack.
Closes #5188
2024-05-15 12:09:41 +02:00
Kenneth Skovhede 8c6f4d356d Added more metadata to the latest.json and latest.js files 2024-05-15 11:24:57 +02:00
Kenneth Skovhede 003a70b950 Changed generic update url to match the website 2024-05-15 11:23:14 +02:00
Kenneth SkovhedeandGitHub 058bb26005 Merge pull request #5185 from duplicati/feature/remove-mono-support-in-ssh
Removed support for older Mono builds
2024-05-14 14:42:32 +02:00
Kenneth Skovhede 226f200006 Inject the long-form version name as the version tag 2024-05-14 14:42:15 +02:00
Kenneth Skovhede 238bfdd02c Using Windows identity for reporting username in system info 2024-05-14 14:41:59 +02:00
Kenneth Skovhede 4b15395095 Removed mentions of the managed SSH client. 2024-05-13 22:47:09 +02:00
Kenneth Skovhede 3c3f82ab54 Removed support for older Mono builds 2024-05-13 22:43:23 +02:00
Kenneth SkovhedeandGitHub bda368febf Merge pull request #4975 from Jojo-1000/fix-force-locale
Fix force-locale option for CLI and GUI
2024-05-13 18:28:01 +02:00
Kenneth SkovhedeandGitHub 6a134ce04e Merge pull request #4989 from Jojo-1000/improve-source-file-picker
Improve source file picker
2024-05-13 16:54:27 +02:00
Kenneth SkovhedeandGitHub 67665e47ee Merge pull request #4993 from Jojo-1000/quota-disable
Add option to disable quota and update quota size option
2024-05-13 16:47:48 +02:00
Kenneth SkovhedeandGitHub ca9b48ee98 Merge pull request #4991 from Jojo-1000/fix-recoverytool-empty
Fix recovery tool empty file restore and other bugs
2024-05-13 16:29:48 +02:00
Jojo-1000 e2e8adf02f Merge branch 'master' into fix-force-locale 2024-05-12 00:01:42 +02:00
Jojo-1000 796be8ce29 Merge branch 'master' into improve-source-file-picker 2024-05-11 23:53:57 +02:00
JanandGitHub f79b1f93c4 Merge branch 'master' into fix-recoverytool-empty 2024-05-11 23:49:24 +02:00
Jojo-1000 50808c866b Merge branch 'master' into quota-disable 2024-05-11 23:46:19 +02:00
Kenneth SkovhedeandGitHub 0d6e9487d7 Merge pull request #5182 from duplicati/feature/fix-docker-builds-localization
Added localization support for Docker images
2024-05-08 15:41:58 +02:00
Kenneth Skovhede 7bc34b9ff8 Added missing ca-certificates package to Docker builds 2024-05-08 14:33:09 +02:00
Kenneth SkovhedeandGitHub dac54d4ce5 Merge pull request #5181 from duplicati/feature/fix-decrypt-file-locking
Fixed disposing the read stream before moving the encrypted database.
2024-05-08 13:40:47 +02:00
Kenneth SkovhedeandGitHub c61f8d5b32 Merge pull request #5180 from duplicati/feature/improved-error-handling-for-custom-report-message
Expanded error handling when parsing a custom body for reports.
2024-05-08 13:38:56 +02:00
Kenneth Skovhede 45c7e90479 Added localization support for Docker images 2024-05-08 12:57:40 +02:00
Kenneth Skovhede 83878dcbcd Removed unused pipeline files 2024-05-08 12:52:25 +02:00
Kenneth Skovhede d5f7d69f39 Added missing parenthesis 2024-05-08 12:49:37 +02:00
Kenneth SkovhedeandGitHub e6c347217d Merge pull request #5172 from SimonNyvall/improve-marked-windows-classes
Add SupportedOSPlatform attribute to Windows-specific classes
2024-05-08 12:48:31 +02:00
Kenneth Skovhede 1265f88cf8 Fixed disposing the read stream before moving the encrypted database.
Added more context to the default error message when decryption fails.
2024-05-08 12:48:10 +02:00
Kenneth SkovhedeandGitHub 8968937506 Merge pull request #5179 from duplicati/feature/avalonia-improvement
Removed trace logs from Avaloni by default.
2024-05-08 11:37:22 +02:00
Kenneth Skovhede 792e1abf74 Expanded error handling when parsing a custom body for reports.
This is related to #5170
2024-05-08 11:36:42 +02:00
Kenneth Skovhede a76e692183 Removed trace logs from Avaloni by default.
Addded option to get logs from Avalonia via env variables `DEBUG_AVALONIA=1` or `DEBUG_AVALONIA=2`
Added 1s delay for initializing UI to counter sporadic crashes on startup.
2024-05-07 18:00:45 +02:00
Kenneth SkovhedeandGitHub 33c735c853 Merge pull request #5177 from wjansenw/patch-1
add gnupg to docker
2024-05-06 13:47:47 +02:00
Kenneth SkovhedeandGitHub 288e3534b1 Merge pull request #5176 from duplicati/feature/release-builder-micro-fixes
Micro fixes to release builder logic.
2024-05-06 13:46:40 +02:00
wjansenwandGitHub 81f4e93c09 add gnupg to docker 2024-05-06 13:46:26 +02:00
Kenneth Skovhede 6caa01f051 Micro fixes to release builder logic.
Added example manifest
Fixed not signing pkg files if disabled
2024-05-06 09:43:51 +02:00
Kenneth SkovhedeandGitHub bcd2ec815c Merge pull request #5173 from duplicati/feature/mini-bugfix
Feature/mini bugfix
2024-05-04 09:02:07 +02:00
Kenneth Skovhede f1fb843846 More dynamic handling of files modified during build 2024-05-04 08:13:54 +02:00
Kenneth Skovhede d27a1f1db8 Removed auto-gen file 2024-05-04 08:13:17 +02:00
Kenneth Skovhede b8f9a634e3 Fixed issues with checking signed manifest 2024-05-04 08:01:01 +02:00
Kenneth Skovhede 0e60bf4ca5 Fixed crash in autoupdater help text 2024-05-04 07:34:45 +02:00
Kenneth Skovhede ebd34c853e Safguard against reading older update config 2024-05-04 07:26:54 +02:00
Kenneth Skovhede 43e62f2479 Fixed incorrect and missing image resources 2024-05-04 07:20:31 +02:00
SimonNyvall 7ee8514f7e Add SupportedOSPlatform attribute to Windows-specific classes 2024-05-04 00:31:52 +02:00
Kenneth Skovhede 8458d1efd6 Fixed consolehelper returning wrong content 2024-05-03 15:53:23 +02:00
Kenneth SkovhedeandGitHub 4eedbc118a Merge pull request #5169 from duplicati/feature/add-libicu74
Added `libicu74` to list of icu versions
2024-05-03 11:30:36 +02:00
Kenneth Skovhede f8628d32dc Added libicu74 to list of icu versions 2024-05-03 10:49:15 +02:00
Kenneth SkovhedeandGitHub fe02dfc0de Merge pull request #5163 from duplicati/feature/bundle-storj-binaries
Implemented a workaround for loading Uplink.net binaries
2024-05-03 10:47:20 +02:00
Kenneth SkovhedeandGitHub 2bd921fe25 Merge pull request #5164 from duplicati/feature/revisit-machine-id
Revisiting the machine-id concept.
2024-05-03 10:46:49 +02:00
Kenneth SkovhedeandGitHub 39af4fb521 Merge pull request #5165 from duplicati/feature/updated-handling-of-icons
Reworked tray icons.
2024-05-03 10:46:38 +02:00
Kenneth SkovhedeandGitHub 5646f5ea92 Removed BountySource & AppVeyour. Fixed license badge. 2024-05-03 08:22:48 +02:00
Kenneth SkovhedeandGitHub 9768a7349b Remvoed bountysource and appveyor 2024-05-03 07:48:25 +02:00
Kenneth SkovhedeandGitHub aeb335ce07 Removed open collective signup links 2024-05-03 07:47:42 +02:00
Kenneth Skovhede 67c1213a98 Fixed sending the wrong report message 2024-04-30 10:53:14 +02:00
Kenneth Skovhede c7bde590dc Reworked tray icons.
Added support for light/dark theme.
Updated Avalonia.
Fixed issue with event firing before load.
Changed icons to specific app operating system icons.
New icons for MacOS.
This fixes #5148
2024-04-30 10:28:01 +02:00
Kenneth Skovhede fa73849783 Rename part 2 2024-04-29 21:43:28 +02:00
Kenneth Skovhede cbf98657d3 Rename part 1 2024-04-29 21:43:07 +02:00
Kenneth Skovhede 5b9e294dec Revisiting the machine-id concept.
Removed the text from the update folder as it is no longer being used.
Added a new `machineid.txt` file that controls the machine-id.
De-coupled the machine-id from the installationid as the latter is used for usage reporting, and should not be connected to the machine-id
2024-04-29 17:58:00 +02:00
Kenneth Skovhede 1bdfed6a90 Implemented a workaround for loading Uplink.net binaries 2024-04-29 16:14:09 +02:00
Kenneth SkovhedeandGitHub 8a39372c73 Merge pull request #5152 from duplicati/feature/update-manifest
Implemented the new manifest format
2024-04-29 10:57:09 +02:00
Kenneth SkovhedeandGitHub b51f0ed71e Merge pull request #5155 from duplicati/feature/built-in-sqlite-rc4-decrypt
Added support for decrypting the RC4 encrypted SQLite databases
2024-04-29 10:56:34 +02:00
Kenneth SkovhedeandGitHub b5386573b5 Merge pull request #5158 from duplicati/feature/send-multi-http-reports
Added support for sending reports via HTTP(s) to multiple destinations
2024-04-29 10:54:51 +02:00
Kenneth SkovhedeandGitHub b87ef92e37 Merge pull request #5160 from duplicati/feature/update-ssl-default-to-system-default
Updated TLS version to `None` or `SystemDefault`
2024-04-29 10:54:23 +02:00
Kenneth SkovhedeandGitHub b32c6a4f15 Merge pull request #5161 from duplicati/feature/reduce-dynamic-loader
Reduced dynamic scanning with static linking.
2024-04-29 10:54:09 +02:00
Kenneth SkovhedeandGitHub 7b34dbd022 Merge pull request #5162 from duplicati/feature/add-7z-tests
Added tests for 7z compression
2024-04-29 09:14:29 +02:00
Kenneth Skovhede f708338b2d Added tests for 7z compression 2024-04-29 08:20:24 +02:00
Kenneth Skovhede 508cd56502 Register backend for test 2024-04-27 15:04:18 +02:00
Kenneth Skovhede 2d7e89407c Added option to register custom backends for testing 2024-04-27 14:57:41 +02:00
Kenneth Skovhede 8e25c6e2ab Reduced dynamic scanning with static linking.
Cleaned up some `proj` files.
Added caching to options.
2024-04-26 14:32:41 +02:00
natan a937e8712e fix json naming issue 2024-04-26 09:41:48 +02:00
natan 8625f14744 #1 attempt to fix json naming issue 2024-04-25 17:07:53 +02:00
natan 2773efec93 Selenium Tests - start backup 2024-04-25 17:07:33 +02:00
natan 8fa4875885 fix date handling coming from WS 2024-04-25 17:07:02 +02:00
natan 6e16e1d9ed fix multiple WS messages bug 2024-04-25 17:06:10 +02:00
Kenneth Skovhede dfb6ee0f29 Updated TLS version to None or SystemDefault to use the OS "best match" versions. 2024-04-24 21:17:38 +02:00
natan 097375a6e8 fix LegacyHttpRequestShim.cs not implemented exceptions during backup delete 2024-04-24 16:09:49 +02:00
natan 70741dc99c Merge branch 'refs/heads/master' into feature/http-server-modernization 2024-04-24 15:45:23 +02:00
Kenneth Skovhede db8fb59577 Avoid locking the database file on Windows when looking for an encrypted header 2024-04-24 15:18:45 +02:00
Kenneth SkovhedeandGitHub e927e7230d Merge pull request #5159 from duplicati/feature/update-build-actions-for-ci
Updated dotnet and checkout to use v4
2024-04-24 15:14:52 +02:00
Kenneth SkovhedeandGitHub f80df1b824 Merge pull request #5157 from duplicati/feature/net4-ssl-cleanup
Removed code for supporting .NET < 4.8
2024-04-24 13:32:10 +02:00
Kenneth Skovhede 4657f48a64 Updated dotnet and checkout to use v4 2024-04-24 13:27:14 +02:00
Kenneth SkovhedeandGitHub 8bf183cea1 Merge pull request #5154 from duplicati/feature/move-installer-resources
Some cleanup after introducing the ReleaseBuilder project
2024-04-24 13:22:24 +02:00
Kenneth Skovhede dab3d989e1 Added support for sending reports via HTTP(s) to multiple destinations 2024-04-24 13:16:21 +02:00
Kenneth Skovhede b5f1177bf8 Added typ to the header to align more with JWT 2024-04-24 11:32:40 +02:00
Kenneth Skovhede 60aacaa4bf Merge remote-tracking branch 'origin/master' into feature/move-installer-resources 2024-04-24 11:27:54 +02:00
Kenneth SkovhedeandGitHub 696c431dce Merge pull request #5151 from achims311/master
Minor updates to release
2024-04-24 11:26:10 +02:00
Kenneth Skovhede 787b2bf04f Removed code for supporting .NET < 4.8 2024-04-24 11:21:03 +02:00
Kenneth Skovhede 612f657614 Added nullable to SQLiteLoader.
Avoid checking file if no password is supplied.
2024-04-24 11:16:11 +02:00
Kenneth Skovhede 8961269675 Avoid checking non-existing file 2024-04-24 11:14:35 +02:00
Kenneth SkovhedeandGitHub 76addf3a9e Merge pull request #5153 from duplicati/feature/support-build-only-release-tool
Added option to only build solution (simplifies CI operations)
2024-04-24 09:05:47 +02:00
Kenneth Skovhede 9c0ef09a39 Implemented a built-in decrypt method.
Ported the RC4 algorithm from BouncyCastle.
This fixes #5147
2024-04-23 17:06:32 +02:00
Kenneth Skovhede fe33c4bf93 Fixed some additional paths 2024-04-23 14:28:50 +02:00
Kenneth Skovhede b73e94de7e Removed unused files.
Moved installer files into the ReleaseBuilder
2024-04-23 14:24:38 +02:00
Kenneth Skovhede 6d3e2c7474 Added option to only build solution (simplifies CI operations) 2024-04-23 13:44:22 +02:00
Kenneth Skovhede b6ca526ee7 Implemented the new manifest format
Fixed some issues with the manifest urls.
Changed to records and nullable in some places.
Regenerated the test-keys to use RSA208.
Changed to using built-in JSON for manifest.
This fixes #5145
2024-04-23 13:41:59 +02:00
Achim Schaefer fb45a14e00 minor addition in the command to build the package 2024-04-23 00:26:20 +02:00
Achim Schaefer 37516b7fe6 Additional setting to have idle IO 2024-04-23 00:25:53 +02:00
natan 0fefb316eb Selenium Tests 2024-04-22 10:15:56 +02:00
natan f9f6dfee6b Merge branch 'refs/heads/feature/kestrel-avalonia-upgrade' into feature/http-server-modernization
# Conflicts:
#	Duplicati.Library.RestAPI/UpdatePollThread.cs
#	Duplicati/Server/Program.cs
#	Duplicati/Server/webroot/ngax/scripts/services/ServerStatus.js
#	Duplicati/WebserverCore/DuplicatiWebserver.cs
2024-04-22 09:35:53 +02:00
Kenneth SkovhedeandGitHub 0b566ecf1f Merge pull request #5132 from duplicati/feature/1mib-default-blocksize
1 MiB default blocksize
2024-04-21 09:37:02 +02:00
Kenneth SkovhedeandGitHub a31a288819 Merge pull request #5111 from duplicati/feature/upgrade-to-net8
Feature/upgrade to net8
2024-04-21 09:34:10 +02:00
Kenneth SkovhedeandGitHub 75efe35e3d Merge pull request #5112 from duplicati/feature/kestrel-avalonia-upgrade
Kestrel+avalonia upgrade
2024-04-21 09:33:49 +02:00
natan 85c2864a9b fix XSRF errors at start 2024-04-19 11:44:12 +02:00
Kenneth Skovhede 2dcafc707b Prepare for experimental release 2024-04-19 09:49:32 +02:00
Kenneth Skovhede 1389bac306 Version bump to v2.0.7.103-2.0.7.103_canary_2024-04-19
You can download this build from:

Binaries: https://updates.duplicati.com/canary/duplicati-2.0.7.103_canary_2024-04-19.zip

Signature file: https://updates.duplicati.com/canary/duplicati-2.0.7.103_canary_2024-04-19.zip.sig

ASCII signature file: https://updates.duplicati.com/canary/duplicati-2.0.7.103_canary_2024-04-19.zip.sig.asc

MD5: 2933f0e8508db5708ea4c5ce62243a33

SHA1: aa8914db593d944748cfe03b0692ac5ccbd200d3

SHA256: 3305708527a7bce7228adcaf642a7c3b525479ea7a4674bc4dd833fe40996cb8
2024-04-19 08:36:32 +02:00
natan 23a9c72115 Selenium Tests 2024-04-18 12:43:56 +02:00
natan c45c1b3961 Merge branch 'refs/heads/feature/kestrel-avalonia-upgrade' into feature/http-server-modernization 2024-04-17 21:19:10 +02:00
natan d78b9f2c63 WIP 2024-04-03 10:40:29 +02:00
natan 6041c8f1d9 Merge remote-tracking branch 'npodbielski/feature/http-server-modernization' into feature/http-server-modernization
# Conflicts:
#	Duplicati/WebserverCore/DuplicatiWebserver.cs
#	Duplicati/WebserverCore/Extensions/ServiceCollectionsExtensions.cs
#	Duplicati/WebserverCore/Middlewares/StaticFilesMiddleware.cs
#	Duplicati/WebserverCore/appsettings.Development.json
#	Duplicati/WebserverCore/appsettings.json
2024-03-26 18:33:13 +01:00
natan 5d53eddb32 WIP 2024-03-26 18:32:56 +01:00
natan 32e852a392 WIP 2024-03-24 17:54:21 +01:00
Kenneth Skovhede 7dfa19d48b Merge branch 'master' into feature/1mib-default-blocksize 2024-03-18 15:19:42 +01:00
Jojo-1000 a1349b1af9 Show filter group rules in source file picker.
Add REST handler /systeminfo/filtergroups that returns the filter strings for all filter groups on the system.
Use these filter strings to apply filter groups to the source file picker.
2024-01-16 18:52:31 +01:00
Jojo-1000 74db90924e Fix processing files repeatedly if present in multiple sources.
This can happen if a source is added to force including a system folder.
2024-01-16 18:52:30 +01:00
Jojo-1000 5acf4dbaad Improve file in-/excluding logic in source file picker.
Add filters to the beginning instead of the end of the list, so they are not overridden by more general ones.
If a file is excluded, add an include filter to include the file.
2024-01-16 18:52:30 +01:00
Jojo-1000 9dbdbb1b2f Show files excluded via 'Exclude' section in source picker.
Add more metadata to the file list returned by server, so that the source file picker can display filters based on file attributes.
Correctly display 'X' in file list if a file is excluded by attributes and not included as a source (#4751).
Fix directory separator issue when loading the page and prevent opening folders in source file list.
2024-01-16 18:52:30 +01:00
Jojo-1000 c9e9b0eeb7 Scroll to source data path on double click. 2024-01-16 18:52:30 +01:00
Jojo-1000 aa365f7f34 Update --quota-size option to assign a backup size.
The assigned quota size is checked in addition to the reported backend quota (if available). It can be used to limit the backup size on the remote. Warnings are created according to the same quota warning threshold.
Previously, this option had no function.
2023-07-16 19:49:54 +02:00
Jojo-1000 0754a5cd72 Ignore File backend quota if total size is zero.
Mono sometimes reports the total size of unknown file systems as zero. Because it should be obvious when the size is actually zero, ignore the quota in this case.
2023-07-16 19:24:37 +02:00
Jojo-1000 8af1dbb6fd Add --quota-disable option to disable reported backend quota.
If set, the backend is treated as if it does not report a quota at all. No warnings and errors are created and no quota values are set in the backup log.
This option is intended to be used if a backend reports a wrong quota, so it needs to be disabled.
2023-07-16 19:16:11 +02:00
Jojo-1000 cff1049347 Add unit tests for recovery tool restore paths.
Test that paths are generated correctly from a different source OS.
Also test that empty files are restored by recovery tool.
2023-07-11 22:48:28 +02:00
Jojo-1000 6b5623651c Fix recovery tool restore paths on different OS.
On Linux, paths were created with \ instead of / separators if backup was originally from Windows (#4884).
Also fix bug that created one unnecessary level of directories in the restore path.
2023-07-11 22:37:08 +02:00
Jojo-1000 0f5b4b9707 Fix restore of empty files with recovery tool.
Empty files do not have data blocks stored, so don't try to get a block for empty files.
2023-07-10 16:44:24 +02:00
Jojo-1000 b9d4be60ca Do not set advanced options with empty string to true.
There are advanced options such as force-local that expect an empty string in some cases. While saving, these were replaced with 'true' even though there was an equals sign.
Now only options without an equals sign are set to true.

Closes #4757
2023-06-23 16:38:54 +02:00
Jojo-1000 b6643fa473 Add documentation comments for LocaleChange. 2023-06-23 14:22:36 +02:00
Jojo-1000 708715e705 Fix force-locale option for CLI and GUI.
- Localization culture is set from multiple threads on the server, so use current local context for every new call instead of global context
- Change locale before executing help commands if flag is present
- When force-locale is set, also change locale for log messages

Closes #4176
2023-06-23 13:30:46 +02:00
Russell Smith 6fb3206b22 Use 1MB default block as suggested by users.
This still allows 50 blocks for the default upload size.
It reduces the blocks to track by a factor of 10.
If you are using larger backups, you want a larger block size.

If you have small backups and it's a problem, recreating those
will take less time.
2023-04-22 16:50:48 +10:00
704 changed files with 20683 additions and 19674 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
*.csproj text
*.sh text eol=lf
#The #! in rules won't work if CRLF
/Installer/debian/docker/debian/* text eol=lf
/Installer/fedora/*.sh text eol=lf
/ReleaseBuilder/Resources/debian/* text eol=lf
/ReleaseBuilder/Resources/fedora/*.sh text eol=lf
-105
View File
@@ -1,105 +0,0 @@
name: Artifacts
on:
push:
branches: "**net5**"
pull_request:
branches: "**net5**"
jobs:
zip:
runs-on: ${{ matrix.cfg.os }}
strategy:
fail-fast: false
matrix:
cfg:
- { os: ubuntu-latest, runtime: linux-x64 }
- { os: windows-latest, runtime: win-x64 }
- { os: macos-latest, runtime: osx-x64 }
steps:
- uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Restore dependencies
run: dotnet restore --runtime=${{ matrix.cfg.runtime }} Duplicati.sln
- name: Package
#TODO: investigate #if __WindowsGTK__ || ENABLE_GTK via -p:DefineConstants=ENABLE_GTK Maybe needed for GUI?
run: dotnet publish -c Release --runtime=${{ matrix.cfg.runtime }} -o publish Duplicati.sln
- name: Save Artifacts
uses: actions/upload-artifact@v2
with:
name: duplicati-${{ matrix.cfg.runtime }}
path: publish
deb:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Package
run: Installer/debian/build.sh
- name: Save Artifacts
uses: actions/upload-artifact@v2
with:
name: deb package
path: duplicati*.deb
rpm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Package
run: Installer/fedora/build.sh
- name: Save Artifacts
uses: actions/upload-artifact@v2
with:
name: rpm package
path: duplicati*.rpm
msi:
runs-on: windows-latest
needs: zip
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Download zip
uses: actions/download-artifact@v2
with:
name: duplicati-win-x64
path: bin
- name: Install go-msi
run: choco install -y "go-msi"
- name: Prepare PATH
shell: bash
run: |
echo "$WIX\\bin" >> $GITHUB_PATH
echo "C:\\Program Files\\go-msi" >> $GITHUB_PATH
- name: Build MSI
id: buildmsi
shell: bash
run: |
Installer/Windows/MSI/build.sh
- name: Save Artifacts
uses: actions/upload-artifact@v2
with:
name: duplicati-win-x64-msi
path: duplicati-win-x64.msi
dmg:
runs-on: macos-latest
needs: zip
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Download zip
uses: actions/download-artifact@v2
with:
name: duplicati-osx-x64
path: bin
- name: Build DMG
shell: bash
run: |
Installer/OSX/dmg/build.sh
- name: Save Artifacts
uses: actions/upload-artifact@v2
with:
name: Duplicati.dmg
path: Duplicati.dmg
-105
View File
@@ -1,105 +0,0 @@
name: Build release installers
on:
workflow_dispatch
env:
build_directory: "Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release"
jobs:
macbuild:
name: mac build and installers
runs-on: macos-latest
strategy:
fail-fast: false
steps:
- name: Install NuGet
uses: nuget/setup-nuget@v1.1.1
- name: Checkout Source
uses: actions/checkout@v3
- name: Restore NuGet Packages
run: |
nuget restore Duplicati.sln
nuget restore "BuildTools/UpdateVersionStamp/UpdateVersionStamp.sln"
- name: Build Duplicati (macOS)
run: |
msbuild /property:Configuration=Release "BuildTools/UpdateVersionStamp/UpdateVersionStamp.sln"
RELEASE_TIMESTAMP=$(date +%Y-%m-%d)
RELEASE_TYPE="canary"
RELEASE_INC_VERSION=$(cat Updates/build_version.txt)
RELEASE_INC_VERSION=$((RELEASE_INC_VERSION+1))
RELEASE_VERSION="2.0.7.${RELEASE_INC_VERSION}"
RELEASE_NAME="${RELEASE_VERSION}_${RELEASE_TYPE}_${RELEASE_TIMESTAMP}"
echo "${RELEASE_NAME}" > "Duplicati/License/VersionTag.txt"
echo "${RELEASE_TYPE}" > "Duplicati/Library/AutoUpdater/AutoUpdateBuildChannel.txt"
mono "BuildTools/UpdateVersionStamp/bin/Release/UpdateVersionStamp.exe" --version="${RELEASE_VERSION}"
msbuild -p:DefineConstants=\"ENABLE_GTK\;XAMARIN_MAC\" -p:Configuration=Release Duplicati.sln
- name: Build installers (OSX)
run: |
Installer/OSX/artifact_mac.sh
- name: Upload build results
uses: actions/upload-artifact@v3
with:
retention-days: 5
name: build-results-mac
path: ~/artifacts/**/*
windowsbuild:
name: windows installer
runs-on: windows-latest
needs: macbuild
steps:
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1.3
- name: Install NuGet
uses: nuget/setup-nuget@v1.1.1
- uses: actions/checkout@v3
- name: Retrieve mac build
uses: actions/download-artifact@v3
with:
name: build-results-mac
path: ~/macartifacts
- name: set env
run: echo "ZIPBUILDFILE=$(resolve-path ~ | select-object -expandproperty path)\macartifacts\$(get-childitem ~\macartifacts\*.zip | select-object -first 1 -expandproperty name )" >> $env:GITHUB_ENV
- name: Build installers (Windows)
run: |
echo ${{ env.ZIPBUILDFILE }}
.\\Installer\\Windows\\artifact_win.bat ${{ env.ZIPBUILDFILE }}
- name: Upload build results
uses: actions/upload-artifact@v3
with:
retention-days: 5
name: build-results-windows
path: ~/artifacts/**/*
debianbuild:
name: debian like installer
runs-on: ubuntu-latest
needs: macbuild
steps:
- uses: actions/checkout@v3
- name: Retrieve mac build
uses: actions/download-artifact@v3
with:
name: build-results-mac
path: ~/macartifacts
- name: set env
run: echo "ZIPBUILDFILE=$(ls ~/macartifacts/*.zip | tail -1)" >> $GITHUB_ENV
- name: Build installers (Deb)
run: |
echo ${{ env.ZIPBUILDFILE }}
sudo apt install debhelper
Installer/debian/artifact_deb.sh ${{ env.ZIPBUILDFILE }}
- name: Upload build results
uses: actions/upload-artifact@v3
with:
retention-days: 5
name: build-results-linux
path: ~/artifacts/**/*
-108
View File
@@ -1,108 +0,0 @@
name: Build release installers from PR
on:
pull_request:
types: [ labeled ]
env:
build_directory: "Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release"
jobs:
macbuild:
if: ${{ github.event.label.name == 'testbuild' }}
name: mac build and installers
runs-on: macos-latest
strategy:
fail-fast: false
steps:
- name: Install NuGet
uses: nuget/setup-nuget@v1.1.1
- name: Checkout Source
uses: actions/checkout@v3
- name: Restore NuGet Packages
run: |
nuget restore Duplicati.sln
nuget restore "BuildTools/UpdateVersionStamp/UpdateVersionStamp.sln"
- name: Build Duplicati (macOS)
run: |
msbuild /property:Configuration=Release "BuildTools/UpdateVersionStamp/UpdateVersionStamp.sln"
RELEASE_TIMESTAMP=$(date +%Y-%m-%d)
RELEASE_TYPE="canary"
RELEASE_INC_VERSION=$(cat Updates/build_version.txt)
RELEASE_INC_VERSION=$((RELEASE_INC_VERSION+1))
RELEASE_VERSION="2.0.7.${RELEASE_INC_VERSION}"
RELEASE_NAME="${RELEASE_VERSION}_${RELEASE_TYPE}_${RELEASE_TIMESTAMP}"
echo "${RELEASE_NAME}" > "Duplicati/License/VersionTag.txt"
echo "${RELEASE_TYPE}" > "Duplicati/Library/AutoUpdater/AutoUpdateBuildChannel.txt"
mono "BuildTools/UpdateVersionStamp/bin/Release/UpdateVersionStamp.exe" --version="${RELEASE_VERSION}"
msbuild -p:DefineConstants=\"ENABLE_GTK\;XAMARIN_MAC\" -p:Configuration=Release Duplicati.sln
- name: Build installers (OSX)
run: |
Installer/OSX/artifact_mac.sh
- name: Upload build results
uses: actions/upload-artifact@v3
with:
retention-days: 5
name: build-results-mac
path: ~/artifacts/**/*
windowsbuild:
name: windows installer
runs-on: windows-latest
needs: macbuild
steps:
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1.3
- name: Install NuGet
uses: nuget/setup-nuget@v1.1.1
- uses: actions/checkout@v3
- name: Retrieve mac build
uses: actions/download-artifact@v3
with:
name: build-results-mac
path: ~/macartifacts
- name: set env
run: echo "ZIPBUILDFILE=$(resolve-path ~ | select-object -expandproperty path)\macartifacts\$(get-childitem ~\macartifacts\*.zip | select-object -first 1 -expandproperty name )" >> $env:GITHUB_ENV
- name: Build installers (Windows)
run: |
echo ${{ env.ZIPBUILDFILE }}
.\\Installer\\Windows\\artifact_win.bat ${{ env.ZIPBUILDFILE }}
- name: Upload build results
uses: actions/upload-artifact@v3
with:
retention-days: 5
name: build-results-windows
path: ~/artifacts/**/*
debianbuild:
name: debian like installer
runs-on: ubuntu-latest
needs: macbuild
steps:
- uses: actions/checkout@v3
- name: Retrieve mac build
uses: actions/download-artifact@v3
with:
name: build-results-mac
path: ~/macartifacts
- name: set env
run: echo "ZIPBUILDFILE=$(ls ~/macartifacts/*.zip | tail -1)" >> $GITHUB_ENV
- name: Build installers (Deb)
run: |
echo ${{ env.ZIPBUILDFILE }}
sudo apt install debhelper
Installer/debian/artifact_deb.sh ${{ env.ZIPBUILDFILE }}
- name: Upload build results
uses: actions/upload-artifact@v3
with:
retention-days: 5
name: build-results-linux
path: ~/artifacts/**/*
+3 -2
View File
@@ -8,8 +8,9 @@ jobs:
runs-on: ubuntu-latest
permissions:
issues: write
actions: write
steps:
- uses: actions/stale@v8
- uses: actions/stale@v9
with:
days-before-issue-stale: 15
days-before-issue-close: 15
@@ -22,4 +23,4 @@ jobs:
only-labels: "pending user feedback"
exempt-issue-labels: "bug,enhancement,good first issue,backend enhancement,backend issue,backup corruption,bounty,bugreport attached,core logic,docker,filters,help wanted,linux,localization,MacOS,mono,performance issue,reproduced,server side,ssl/tls issue,Synology,tests,translation,UI,windows"
repo-token: ${{ secrets.GITHUB_TOKEN }}
operations-per-run: 500
+4 -4
View File
@@ -13,12 +13,12 @@ jobs:
steps:
- name: Set up .NET
uses: actions/setup-dotnet@v1.8.0
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
dotnet-version: 8.x
- name: Checkout source
uses: actions/checkout@v2.3.4
uses: actions/checkout@v4
- name: Restore NuGet dependencies
run: dotnet restore Duplicati.sln
@@ -32,6 +32,6 @@ jobs:
selenium:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Selenium
run: pipeline/selenium/test.sh
+2 -14
View File
@@ -4,29 +4,17 @@ obj/
*.userprefs
*.user
*.suo
Installer/Paraffin.exe
Installer/account_info
Duplicati/Localization/compiled
Duplicati/Localization/*/
Duplicati/Localization/*.diff.csv
Duplicati/Localization/*.new.csv
Duplicati/Localization/*.updated.csv
Duplicati/Localization/report.*.xml
Installer/incLocFiles.*.wxs
Duplicati/Server/webroot/extjs-*
Installer/binfiles.wxs
Installer/generated-keys.xml
Installer/oem
Updates/build
packages
changelog-news.txt
binfiles.wxs
.idea/
.vs/
/publish/
Installer/debian/*.buildinfo
/*.deb
/*.rpm
ReleaseBuilder/build-temp/
ReleaseBuilder/build-temp/
ReleaseBuilder/Resources/Windows/binfiles.wxs
+34 -1
View File
@@ -31,7 +31,40 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Executables/net8/Duplicati.Server/bin/Debug/net8.0/Duplicati.CommandLine",
"program": "${workspaceFolder}/Executables/net8/Duplicati.CommandLine/bin/Debug/net8.0/Duplicati.CommandLine",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "internalConsole"
},
{
"name": "Launch AutoUpdater executable",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Executables/net8/Duplicati.CommandLine.AutoUpdater/bin/Debug/net8.0/Duplicati.CommandLine.AutoUpdater",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "internalConsole"
},
{
"name": "Launch ConfigurationImporter executable",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Executables/net8/Duplicati.CommandLine.ConfigurationImporter/bin/Debug/net8.0/Duplicati.CommandLine.ConfigurationImporter",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "internalConsole"
},
{
"name": "Launch RecoveryTool executable",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Executables/net8/Duplicati.CommandLine.RecoveryTool/bin/Debug/net8.0/Duplicati.CommandLine.RecoveryTool",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
@@ -1,19 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.11.5" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
@@ -1,17 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyFinder", "DependencyFinder.csproj", "{3E891818-579A-4C06-8066-4C702BFA5921}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x86 = Debug|x86
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3E891818-579A-4C06-8066-4C702BFA5921}.Debug|x86.ActiveCfg = Debug|x86
{3E891818-579A-4C06-8066-4C702BFA5921}.Debug|x86.Build.0 = Debug|x86
{3E891818-579A-4C06-8066-4C702BFA5921}.Release|x86.ActiveCfg = Release|x86
{3E891818-579A-4C06-8066-4C702BFA5921}.Release|x86.Build.0 = Release|x86
EndGlobalSection
EndGlobal
-85
View File
@@ -1,85 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using Mono.Cecil;
using System.IO;
using System.Linq;
namespace DependencyFinder
{
class MainClass
{
public static void Main(string[] args)
{
var lst =
Directory.EnumerateFiles(args[0], "*.exe", SearchOption.AllDirectories).Union(
Directory.EnumerateFiles(args[0], "*.dll", SearchOption.AllDirectories))
.Select(x => {
try { return AssemblyDefinition.ReadAssembly(x); }
catch { return null; }
}).Where(x => x != null)
.Distinct();
PoC(lst, Console.Out, new [] { args[0] });
}
//Adapted from: https://stackoverflow.com/questions/9262464/tool-to-show-assembly-dependencies
public static void PoC(IEnumerable<AssemblyDefinition> assemblies, TextWriter writer, IEnumerable<string> searchfolders)
{
var resolver = new DefaultAssemblyResolver();
searchfolders.ToList().ForEach(x => resolver.AddSearchDirectory(x));
//writer.WriteLine("digraph Dependencies {");
var loaded = assemblies
.SelectMany(a => a.Modules.Cast<ModuleDefinition>())
.SelectMany(m => m.AssemblyReferences.Cast<AssemblyNameReference>())
.Distinct()
.Select(asm => {
var dllname = asm.Name + ".dll";
//Console.WriteLine("Probing for {0}", dllname);
try { return AssemblyDefinition.ReadAssembly(dllname); }
catch { }
try { return resolver.Resolve(asm); }
catch { }
return null;
})
.Where(assembly => assembly != null)
.ToList();
//loaded.ForEach(a => a.MainModule.ReadSymbols());
loaded.Select(x => x.FullName).Distinct().OrderBy(x => x).ToList().ForEach(x => writer.WriteLine("{0}", x));
/*loaded.ForEach(a =>
{
foreach (var r in a.MainModule.AssemblyReferences.Cast<AssemblyNameReference>())
writer.WriteLine(@"""{0}"" -> ""{1}"";", r.Name, a.Name.Name);
} );*/
//writer.WriteLine("}");
}
}
}
@@ -1,42 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29123.88
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GnupgSigningTool", "GnupgSigningTool.csproj", "{72B910F2-8E80-4955-A0D6-ED4C35CEE665}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Utility", "..\..\Duplicati\Library\Utility\Duplicati.Library.Utility.csproj", "{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Encryption", "..\..\Duplicati\Library\Encryption\Duplicati.Library.Encryption.csproj", "{2CF2D90E-C25B-47AD-91E0-98451BAB8058}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Interface", "..\..\Duplicati\Library\Interface\Duplicati.Library.Interface.csproj", "{C5899F45-B0FF-483C-9D38-24A9FCAAB237}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x86 = Debug|x86
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{72B910F2-8E80-4955-A0D6-ED4C35CEE665}.Debug|x86.ActiveCfg = Debug|x86
{72B910F2-8E80-4955-A0D6-ED4C35CEE665}.Debug|x86.Build.0 = Debug|x86
{72B910F2-8E80-4955-A0D6-ED4C35CEE665}.Release|x86.ActiveCfg = Release|x86
{72B910F2-8E80-4955-A0D6-ED4C35CEE665}.Release|x86.Build.0 = Release|x86
{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}.Debug|x86.ActiveCfg = Debug|Any CPU
{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}.Debug|x86.Build.0 = Debug|Any CPU
{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}.Release|x86.ActiveCfg = Release|Any CPU
{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}.Release|x86.Build.0 = Release|Any CPU
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Debug|x86.ActiveCfg = Debug|Any CPU
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Debug|x86.Build.0 = Debug|Any CPU
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Release|x86.ActiveCfg = Release|Any CPU
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Release|x86.Build.0 = Release|Any CPU
{C5899F45-B0FF-483C-9D38-24A9FCAAB237}.Debug|x86.ActiveCfg = Debug|Any CPU
{C5899F45-B0FF-483C-9D38-24A9FCAAB237}.Debug|x86.Build.0 = Debug|Any CPU
{C5899F45-B0FF-483C-9D38-24A9FCAAB237}.Release|x86.ActiveCfg = Release|Any CPU
{C5899F45-B0FF-483C-9D38-24A9FCAAB237}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {71CC6B1F-7F52-4059-8761-30B3D0501ECC}
EndGlobalSection
EndGlobal
-128
View File
@@ -1,128 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
namespace GnupgSigningTool
{
public static class Program
{
private static string keyfilepassword;
private static string gpgkeypassphrase;
private static string gpgkeyfile;
private static string gpgpath;
private static string gpgkeyid;
private static bool useArmor;
private static string inputFile;
private static string signatureFile;
private static void SpawnGPG()
{
var armorOption = useArmor ? "--armor" : "";
var gpgArgument = string.Format("--pinentry-mode loopback --passphrase-fd 0 --batch --yes {0} -u \"{1}\" --output \"{2}\" --detach-sig \"{3}\"",
armorOption,
gpgkeyid,
signatureFile,
inputFile);
var proc = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = gpgpath,
Arguments = gpgArgument,
RedirectStandardInput = true,
UseShellExecute = false
});
proc.StandardInput.WriteLine(gpgkeypassphrase);
proc.WaitForExit();
}
private static void LoadGPGKeyIdAndPassphrase()
{
using (var enc = new Duplicati.Library.Encryption.AESEncryption(keyfilepassword, new Dictionary<string, string>()))
using (var ms = new System.IO.MemoryStream())
using (var fs = System.IO.File.OpenRead(gpgkeyfile))
{
try
{
enc.Decrypt(fs, ms);
}
catch (System.Security.Cryptography.CryptographicException e)
{
throw new ArgumentException("Failed to decrypt gpg secret credentials file: {0}\n", e.Message);
}
ms.Position = 0;
using (var sr = new System.IO.StreamReader(ms))
{
var lines = sr.ReadToEnd().Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
gpgkeyid = lines[0];
gpgkeypassphrase = lines[1];
}
}
}
public static void Main(string[] _args)
{
var args = new List<string>(_args);
var opts = Duplicati.Library.Utility.CommandLineParser.ExtractOptions(args);
opts.TryGetValue("inputfile", out inputFile);
opts.TryGetValue("signaturefile", out signatureFile);
opts.TryGetValue("keyfile-password", out keyfilepassword);
opts.TryGetValue("gpgkeyfile", out gpgkeyfile);
opts.TryGetValue("gpgpath", out gpgpath);
opts.TryGetValue("armor", out string armor);
useArmor = Boolean.TryParse(armor, out useArmor) && useArmor;
if (string.IsNullOrWhiteSpace(gpgkeyfile))
{
throw new ArgumentException("No gpgfile with encrypted credentials specified.");
}
if (!System.IO.File.Exists(gpgkeyfile))
{
throw new ArgumentException("Specified file with encrypted gpg credentials not found.");
}
LoadGPGKeyIdAndPassphrase();
if (gpgkeyid is null || gpgkeypassphrase is null)
{
throw new ArgumentException("Could not fetch gpg key id or gpg passphrase.");
}
gpgpath = gpgpath ?? Duplicati.Library.Encryption.GPGEncryption.GetGpgProgramPath();
SpawnGPG();
}
}
}
+13 -11
View File
@@ -1,11 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>license_upgrader</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>license_upgrader</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
</PropertyGroup>
</Project>
-130
View File
@@ -1,130 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace UpdateVersionStamp
{
public static class Program
{
private static readonly string DIR_SEP = Path.DirectorySeparatorChar.ToString();
private static readonly Dictionary<string, Regex> FILEMAP;
static Program()
{
var versionre = @"(?<version>\d+\.\d+\.(\*|(\d+(\.(\*|\d+)))?))";
FILEMAP = new Dictionary<string, Regex>(StringComparer.InvariantCultureIgnoreCase);
FILEMAP.Add("UpgradeData.wxi", new Regex(@"\<\?define ProductVersion\=\""" + versionre + @"\"" \?\>"));
FILEMAP.Add("AssemblyRedirects.xml", new Regex(@"newVersion\=\""" + versionre + @"\"""));
FILEMAP.Add("index.html", new Regex(@"\?v\=" + versionre));
FILEMAP.Add("login.html", new Regex(@"\?v\=" + versionre));
FILEMAP.Add("app.js", new Regex(@"\?v\=" + versionre));
}
private class Options
{
public string sourcefolder = Path.GetFullPath(Environment.CurrentDirectory);
public string ignorefilter = null;
public string version = null;
public string versiontag = null;
public void Fixup()
{
sourcefolder = Duplicati.Library.Common.IO.Util.AppendDirSeparator(System.IO.Path.GetFullPath(sourcefolder.Replace("/", DIR_SEP)));
if (ignorefilter != null)
ignorefilter =ignorefilter.Replace("/", DIR_SEP);
}
}
public static void Main(string[] _args)
{
List<string> args = new List<string>(_args);
Dictionary<string, string> options = Duplicati.Library.Utility.CommandLineParser.ExtractOptions(args);
Options opt = new Options();
foreach (FieldInfo fi in opt.GetType().GetFields())
if (options.ContainsKey(fi.Name))
fi.SetValue(opt, options[fi.Name]);
opt.Fixup();
Duplicati.Library.Utility.IFilter filter = null;
if (!string.IsNullOrEmpty(opt.ignorefilter))
filter = new Duplicati.Library.Utility.FilterExpression(opt.ignorefilter, false);
Func<string, bool> isFile = (string x) => !x.EndsWith(DIR_SEP);
var paths = Duplicati.Library.Utility.Utility.EnumerateFileSystemEntries(opt.sourcefolder)
.Where(x => Duplicati.Library.Utility.FilterExpression.Matches(filter, x))
.Where(x => isFile(x) && FILEMAP.ContainsKey(Path.GetFileName(x)))
.Select(x =>
{
var m = FILEMAP[Path.GetFileName(x)].Match(File.ReadAllText(x));
return m.Success ?
new { File = x, Version = new Version(m.Groups["version"].Value.Replace("*", "0")), Display = m.Groups["version"].Value }
: null;
})
.Where(x => x != null)
.ToArray(); //No need to re-eval
if (paths.Count() == 0)
{
Console.WriteLine("No files found to update...");
return;
}
foreach (var p in paths)
Console.WriteLine("{0}\t:{1}", p.Display, p.File);
if (string.IsNullOrWhiteSpace(opt.version))
{
var maxv = paths.Select(x => x.Version).Max();
opt.version = new Version(
maxv.Major,
maxv.Minor,
maxv.Build,
maxv.Revision).ToString();
}
//Sanity check
var nv = new Version(opt.version).ToString(4);
foreach (var p in paths)
{
var re = FILEMAP[Path.GetFileName(p.File)];
var txt = File.ReadAllText(p.File);
//var m = re.Match(txt).Groups["version"];
txt = re.Replace(txt, (m) => {
var t = m.Groups["version"];
return m.Value.Replace(t.Value, nv);
});
File.WriteAllText(p.File, txt);
}
Console.WriteLine("Updated {0} files to version {1}", paths.Count(), opt.version);
}
}
}
@@ -1,35 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 11.00
# Visual Studio 2010
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UpdateVersionStamp", "UpdateVersionStamp.csproj", "{1920C3B0-98A0-410F-8972-9C19FA616FE5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Utility", "..\..\Duplicati\Library\Utility\Duplicati.Library.Utility.csproj", "{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Common", "..\..\Duplicati\Library\Common\Duplicati.Library.Common.csproj", "{D63E53E4-A458-4C2F-914D-92F715F58ACF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1920C3B0-98A0-410F-8972-9C19FA616FE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1920C3B0-98A0-410F-8972-9C19FA616FE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1920C3B0-98A0-410F-8972-9C19FA616FE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1920C3B0-98A0-410F-8972-9C19FA616FE5}.Release|Any CPU.Build.0 = Release|Any CPU
{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}.Release|Any CPU.Build.0 = Release|Any CPU
{D63E53E4-A458-4C2F-914D-92F715F58ACF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D63E53E4-A458-4C2F-914D-92F715F58ACF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D63E53E4-A458-4C2F-914D-92F715F58ACF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D63E53E4-A458-4C2F-914D-92F715F58ACF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(MonoDevelopProperties) = preSolution
StartupItem = UpdateVersionStamp.csproj
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using Duplicati.Library.Utility;
using Duplicati.Server;
using Duplicati.Server.Serialization.Interface;
namespace Duplicati.WebserverCore.Abstractions;
public interface IScheduler
{
/// <summary>
/// Initializes scheduler
/// </summary>
/// <param name="worker">The worker thread</param>
void Init(WorkerThread<Runner.IRunnerData> worker);
IList<Tuple<long, string>> GetSchedulerQueueIds();
/// <summary>
/// Terminates the thread. Any items still in queue will be removed
/// </summary>
/// <param name="wait">True if the call should block until the thread has exited, false otherwise</param>
void Terminate(bool wait);
/// <summary>
/// Subscribes to the event that is triggered when the schedule changes
/// </summary>
void SubScribeToNewSchedule(Action handler);
/// <summary>
/// A snapshot copy of the current schedule list
/// </summary>
List<KeyValuePair<DateTime, ISchedule>> Schedule { get; }
/// <summary>
/// A snapshot copy of the current worker queue, that is items that are scheduled, but waiting for execution
/// </summary>
List<Runner.IRunnerData> WorkerQueue { get; }
/// <summary>
/// Forces the scheduler to re-evaluate the order.
/// Call this method if something changes
/// </summary>
void Reschedule();
}
@@ -0,0 +1,17 @@
#nullable enable
using System;
using Duplicati.Library.Utility;
using Duplicati.Server;
namespace Duplicati.Library.RestAPI.Abstractions;
public interface IWorkerThreadsManager
{
void Spawn(Action<Runner.IRunnerData> item);
Tuple<long, string>? CurrentTask { get; }
WorkerThread<Runner.IRunnerData>? WorkerThread { get; }
void UpdateThrottleSpeeds();
long AddTask(Runner.IRunnerData data, bool skipQueue = false);
}
@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Duplicati.Server.Database;
using Duplicati.Server.Serialization;
using Duplicati.Server.Serialization.Interface;
namespace Duplicati.Library.RestAPI;
public static class BackupImportExportHandler
{
public static void RemovePasswords(IBackup backup)
{
backup.SanitizeSettings();
backup.SanitizeTargetUrl();
}
public static byte[] ExportToJSON(Connection connection, IBackup backup, string passphrase)
{
Server.Serializable.ImportExportStructure ipx = connection.PrepareBackupForExport(backup);
byte[] data;
using (var ms = new System.IO.MemoryStream())
{
using (var sw = new System.IO.StreamWriter(ms))
{
Serializer.SerializeJson(sw, ipx, true);
if (!string.IsNullOrWhiteSpace(passphrase))
{
ms.Position = 0;
using (var ms2 = new System.IO.MemoryStream())
{
using (var m = new Duplicati.Library.Encryption.AESEncryption(passphrase, new Dictionary<string, string>()))
{
m.Encrypt(ms, ms2);
data = ms2.ToArray();
}
}
}
else
{
data = ms.ToArray();
}
}
}
return data;
}
public static Server.Serializable.ImportExportStructure ImportBackup(string configurationFile, bool importMetadata, Func<string> getPassword, Dictionary<string, string> advancedOptions)
{
// This removes the ID and DBPath from the backup configuration.
Server.Serializable.ImportExportStructure importedStructure = LoadConfiguration(configurationFile, importMetadata, getPassword);
// This will create the Duplicati-server.sqlite database file if it doesn't exist.
using (Duplicati.Server.Database.Connection connection = FIXMEGlobal.GetDatabaseConnection(advancedOptions))
{
if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase)))
{
throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists.");
}
string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule);
if (!string.IsNullOrWhiteSpace(error))
{
throw new InvalidOperationException(error);
}
// This creates a new ID and DBPath.
connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule);
}
return importedStructure;
}
public static Server.Serializable.ImportExportStructure LoadConfiguration(string filename, bool importMetadata, Func<string> getPassword)
{
Server.Serializable.ImportExportStructure ipx;
var buf = new byte[3];
using (var fs = System.IO.File.OpenRead(filename))
{
Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buf, buf.Length);
fs.Position = 0;
if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S')
{
using (var m = new Duplicati.Library.Encryption.AESEncryption(getPassword(), new Dictionary<string, string>()))
{
using (var m2 = m.Decrypt(fs))
{
using (var sr = new System.IO.StreamReader(m2))
{
ipx = Serializer.Deserialize<Server.Serializable.ImportExportStructure>(sr);
}
}
}
}
else
{
using (var sr = new System.IO.StreamReader(fs))
{
ipx = Serializer.Deserialize<Server.Serializable.ImportExportStructure>(sr);
}
}
}
if (ipx.Backup == null)
{
throw new Exception("No backup found in document");
}
if (ipx.Backup.Metadata == null)
{
ipx.Backup.Metadata = new Dictionary<string, string>();
}
if (!importMetadata)
{
ipx.Backup.Metadata.Clear();
}
ipx.Backup.ID = null;
ipx.Backup.DBPath = null;
if (ipx.Schedule != null)
{
ipx.Schedule.ID = -1;
}
return ipx;
}
}
+30 -24
View File
@@ -1,20 +1,24 @@
// Copyright (C) 2015, The Duplicati Team
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// http://www.duplicati.com, info@duplicati.com
//
// 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
using System;
using Duplicati.Server.Serialization.Interface;
using System.Collections.Generic;
@@ -46,7 +50,7 @@ namespace Duplicati.Server.Database
{
this.ID = null;
}
internal void LoadChildren(Connection con)
{
if (this.IsTemporary)
@@ -65,7 +69,9 @@ namespace Duplicati.Server.Database
this.Metadata = con.GetMetadata(id);
}
}
protected void SetDBPath(string path) => this.DBPath = path;
/// <summary>
/// The backup ID
/// </summary>
@@ -90,27 +96,27 @@ namespace Duplicati.Server.Database
/// The path to the local database
/// </summary>
public string DBPath { get; internal set; }
/// <summary>
/// The backup source folders and files
/// </summary>
public string[] Sources { get; set; }
/// <summary>
/// The backup settings
/// </summary>
public ISetting[] Settings { get; set; }
/// <summary>
/// The filters applied to the source files
/// </summary>
public IFilter[] Filters { get; set; }
/// <summary>
/// The backup metadata
/// </summary>
public IDictionary<string, string> Metadata { get; set; }
public IDictionary<string, string> Metadata { get; set; }
/// <summary>
/// Gets a value indicating if this instance is not persisted to the database
/// </summary>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,9 @@
/*
Token Family table
*/
CREATE TABLE "TokenFamily" (
"ID" TEXT PRIMARY KEY,
"UserId" TEXT NOT NULL,
"Counter" INTEGER NOT NULL,
"LastUpdated" INTEGER NOT NULL
);
@@ -154,5 +154,15 @@ CREATE TABLE "TempFile" (
"Expires" INTEGER NOT NULL
);
INSERT INTO "Version" ("Version") VALUES (6);
/*
Token Family table
*/
CREATE TABLE "TokenFamily" (
"ID" TEXT PRIMARY KEY,
"UserId" TEXT NOT NULL,
"Counter" INTEGER NOT NULL,
"LastUpdated" INTEGER NOT NULL
);
INSERT INTO "Version" ("Version") VALUES (7);
+20 -16
View File
@@ -1,20 +1,24 @@
// Copyright (C) 2015, The Duplicati Team
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// http://www.duplicati.com, info@duplicati.com
//
// 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
using System;
namespace Duplicati.Server.Database
@@ -1,20 +1,24 @@
// Copyright (C) 2015, The Duplicati Team
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// http://www.duplicati.com, info@duplicati.com
//
// 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
using System;
namespace Duplicati.Server.Database
@@ -29,7 +33,7 @@ namespace Duplicati.Server.Database
public string Exception { get; set; }
public string BackupID { get; set; }
public string Action { get; set; }
public DateTime Timestamp { get; set; }
public DateTime Timestamp { get; set; }
public string LogEntryID { get; set; }
public string MessageID { get; set; }
public string MessageLogTag { get; set; }
+37 -33
View File
@@ -1,20 +1,24 @@
// Copyright (C) 2015, The Duplicati Team
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// http://www.duplicati.com, info@duplicati.com
//
// 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
using System;
using System.Linq;
using Duplicati.Server.Serialization.Interface;
@@ -29,38 +33,38 @@ namespace Duplicati.Server.Database
public string Repeat { get; set; }
public DateTime LastRun { get; set; }
public string Rule { get; set; }
public DayOfWeek[] AllowedDays
{
{
get
{
if (string.IsNullOrEmpty(this.Rule))
return null;
var days = (from n in this.Rule.Split(new string[] { ";" }, StringSplitOptions.RemoveEmptyEntries)
where n.StartsWith("AllowedWeekDays=", StringComparison.OrdinalIgnoreCase)
select n.Substring("AllowedWeekDays=".Length).Split(new char[] {','}, StringSplitOptions.RemoveEmptyEntries))
var days = (from n in this.Rule.Split(new string[] { ";" }, StringSplitOptions.RemoveEmptyEntries)
where n.StartsWith("AllowedWeekDays=", StringComparison.OrdinalIgnoreCase)
select n.Substring("AllowedWeekDays=".Length).Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
.FirstOrDefault();
if (days == null)
return null;
return (from n in days
return (from n in days
where Enum.TryParse<DayOfWeek>(n, true, out _)
select (DayOfWeek)Enum.Parse(typeof(DayOfWeek), n, true))
.ToArray();
}
set
{
var parts =
string.IsNullOrEmpty(this.Rule) ?
var parts =
string.IsNullOrEmpty(this.Rule) ?
new string[0] :
(from n in this.Rule.Split(new string[] { ";" }, StringSplitOptions.RemoveEmptyEntries)
where !n.StartsWith("AllowedWeekDays=", StringComparison.OrdinalIgnoreCase)
select n);
(from n in this.Rule.Split(new string[] { ";" }, StringSplitOptions.RemoveEmptyEntries)
where !n.StartsWith("AllowedWeekDays=", StringComparison.OrdinalIgnoreCase)
select n);
if (value != null && value.Length != 0)
parts = parts.Union(new string[] {
"AllowedWeekDays=" +
@@ -70,7 +74,7 @@ namespace Duplicati.Server.Database
select Enum.GetName(typeof(DayOfWeek), n)).Distinct()
)
}).Distinct();
this.Rule = string.Join(";", parts);
}
}
@@ -1,33 +1,38 @@
// Copyright (C) 2015, The Duplicati Team
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// http://www.duplicati.com, info@duplicati.com
//
// 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
using System;
using System.Linq;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Duplicati.Library.Common;
using Duplicati.Library.RestAPI;
using System.Text.Json;
using System.Text;
namespace Duplicati.Server.Database
{
public class ServerSettings
{
private static class CONST
public static class CONST
{
public const string STARTUP_DELAY = "startup-delay";
public const string DOWNLOAD_SPEED_LIMIT = "max-download-speed";
@@ -38,8 +43,6 @@ namespace Duplicati.Server.Database
public const string SERVER_PORT_CHANGED = "server-port-changed";
public const string SERVER_PASSPHRASE = "server-passphrase";
public const string SERVER_PASSPHRASE_SALT = "server-passphrase-salt";
public const string SERVER_PASSPHRASETRAYICON = "server-passphrase-trayicon";
public const string SERVER_PASSPHRASETRAYICONHASH = "server-passphrase-trayicon-hash";
public const string UPDATE_CHECK_LAST = "last-update-check";
public const string UPDATE_CHECK_INTERVAL = "update-check-interval";
public const string UPDATE_CHECK_NEW_VERSION = "update-check-latest";
@@ -50,10 +53,12 @@ namespace Duplicati.Server.Database
public const string HAS_FIXED_INVALID_BACKUPID = "has-fixed-invalid-backup-id";
public const string UPDATE_CHANNEL = "update-channel";
public const string USAGE_REPORTER_LEVEL = "usage-reporter-level";
public const string HAS_ASKED_FOR_PASSWORD_PROTECTION = "has-asked-for-password-protection";
public const string DISABLE_TRAY_ICON_LOGIN = "disable-tray-icon-login";
public const string SERVER_ALLOWED_HOSTNAMES = "allowed-hostnames";
}
public const string JWT_CONFIG = "jwt-config";
public const string PBKDF_CONFIG = "pbkdf-config";
public const string AUTOGENERATED_PASSPHRASE = "autogenerated-passphrase";
}
private readonly Dictionary<string, string> settings;
private readonly Connection databaseConnection;
@@ -68,12 +73,12 @@ namespace Duplicati.Server.Database
public void ReloadSettings()
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
{
settings.Clear();
foreach(var n in typeof(CONST).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Static).Select(x => (string)x.GetValue(null)))
foreach (var n in typeof(CONST).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Static).Select(x => (string)x.GetValue(null)))
settings[n] = null;
foreach(var n in databaseConnection.GetSettings(Connection.SERVER_SETTINGS_ID))
foreach (var n in databaseConnection.GetSettings(Connection.SERVER_SETTINGS_ID))
settings[n.Name] = n.Value;
}
}
@@ -83,59 +88,64 @@ namespace Duplicati.Server.Database
if (newsettings == null)
throw new ArgumentNullException();
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
{
m_latestUpdate = null;
if (clearExisting)
settings.Clear();
foreach(var k in newsettings)
foreach (var k in newsettings)
if (!clearExisting && newsettings[k.Key] == null && k.Key.StartsWith("--", StringComparison.Ordinal))
settings.Remove(k.Key);
else
settings[k.Key] = newsettings[k.Key];
// Prevent user from logging themselves out, by disabling the login and not knowing the password
if (DisableTrayIconLogin && AutogeneratedPassphrase)
settings[CONST.DISABLE_TRAY_ICON_LOGIN] = false.ToString();
}
SaveSettings();
if (newsettings.Keys.Contains(CONST.SERVER_PASSPHRASE))
GenerateWebserverPasswordTrayIcon();
}
private void SaveSettings()
{
databaseConnection.SetSettings(
from n in settings
select (Duplicati.Server.Serialization.Interface.ISetting)new Setting() {
select (Duplicati.Server.Serialization.Interface.ISetting)new Setting()
{
Filter = "",
Name = n.Key,
Value = n.Value
}, Database.Connection.SERVER_SETTINGS_ID);
}, Database.Connection.SERVER_SETTINGS_ID);
FIXMEGlobal.IncrementLastDataUpdateID();
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
if (FIXMEGlobal.IsServerStarted)
{
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
// If throttle options were changed, update now
FIXMEGlobal.WorkerThreadsManager.UpdateThrottleSpeeds();
}
// In case the usage reporter is enabled or disabled, refresh now
FIXMEGlobal.StartOrStopUsageReporter();
// If throttle options were changed, update now
FIXMEGlobal.UpdateThrottleSpeeds();
// In case the usage reporter is enabled or disabled, refresh now
if (FIXMEGlobal.StartOrStopUsageReporter != null)
FIXMEGlobal.StartOrStopUsageReporter();
}
public string StartupDelayDuration
{
get
get
{
return settings[CONST.STARTUP_DELAY];
}
set
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.STARTUP_DELAY] = value;
SaveSettings();
}
}
public System.Threading.ThreadPriority? ThreadPriorityOverride
{
get
@@ -143,43 +153,43 @@ namespace Duplicati.Server.Database
var tp = settings[CONST.THREAD_PRIORITY];
if (string.IsNullOrEmpty(tp))
return null;
System.Threading.ThreadPriority r;
System.Threading.ThreadPriority r;
if (Enum.TryParse<System.Threading.ThreadPriority>(tp, true, out r))
return r;
return null;
}
set
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.THREAD_PRIORITY] = value.HasValue ? Enum.GetName(typeof(System.Threading.ThreadPriority), value.Value) : null;
}
}
public string DownloadSpeedLimit
{
get
get
{
return settings[CONST.DOWNLOAD_SPEED_LIMIT];
}
set
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.DOWNLOAD_SPEED_LIMIT] = value;
SaveSettings();
}
}
public string UploadSpeedLimit
{
get
get
{
return settings[CONST.UPLOAD_SPEED_LIMIT];
}
set
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.UPLOAD_SPEED_LIMIT] = value;
SaveSettings();
}
@@ -192,23 +202,9 @@ namespace Duplicati.Server.Database
return Duplicati.Library.Utility.Utility.ParseBoolOption(settings, CONST.IS_FIRST_RUN);
}
set
{
lock(databaseConnection.m_lock)
settings[CONST.IS_FIRST_RUN] = value.ToString();
SaveSettings();
}
}
public bool HasAskedForPasswordProtection
{
get
{
return Duplicati.Library.Utility.Utility.ParseBoolOption(settings, CONST.HAS_ASKED_FOR_PASSWORD_PROTECTION);
}
set
{
lock (databaseConnection.m_lock)
settings[CONST.HAS_ASKED_FOR_PASSWORD_PROTECTION] = value.ToString();
settings[CONST.IS_FIRST_RUN] = value.ToString();
SaveSettings();
}
}
@@ -221,7 +217,7 @@ namespace Duplicati.Server.Database
}
set
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.UNACKED_ERROR] = value.ToString();
SaveSettings();
}
@@ -235,7 +231,7 @@ namespace Duplicati.Server.Database
}
set
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.UNACKED_WARNING] = value.ToString();
SaveSettings();
}
@@ -249,7 +245,7 @@ namespace Duplicati.Server.Database
}
set
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.SERVER_PORT_CHANGED] = value.ToString();
SaveSettings();
}
@@ -269,6 +265,14 @@ namespace Duplicati.Server.Database
}
}
public bool AutogeneratedPassphrase
{
get
{
return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.AUTOGENERATED_PASSPHRASE], false);
}
}
public int LastWebserverPort
{
get
@@ -282,57 +286,150 @@ namespace Duplicati.Server.Database
}
set
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.LAST_WEBSERVER_PORT] = value.ToString();
SaveSettings();
}
}
public string WebserverPassword
/// <summary>
/// This class is used to store the PBKDF configuration parameters
/// </summary>
private record PbkdfConfig(string Algorithm, int Version, string Salt, int Iterations, string HashAlorithm, string Hash)
{
get
/// <summary>
/// The version to embed in the configuration
/// </summary>
private const int _Version = 1;
/// <summary>
/// The algorithm to use
/// </summary>
private const string _Algorithm = "PBKDF2";
/// <summary>
/// The hash algorithm to use
/// </summary>
private const string _HashAlorithm = "SHA256";
/// <summary>
/// The number of iterations to use
/// </summary>
private const int _Iterations = 10000;
/// <summary>
/// The size of the hash
/// </summary>
private const int _HashSize = 32;
/// <summary>
/// Creates a new PBKDF2 configuration with a random salt
/// </summary>
/// <param name="password">The password to hash</param>
public static PbkdfConfig CreatePBKDF2(string password)
{
return settings[CONST.SERVER_PASSPHRASE];
var prng = RandomNumberGenerator.Create();
var buf = new byte[_HashSize];
prng.GetBytes(buf);
var salt = Convert.ToBase64String(buf);
var pbkdf2 = new Rfc2898DeriveBytes(password, buf, _Iterations, new HashAlgorithmName(_HashAlorithm));
var pwd = Convert.ToBase64String(pbkdf2.GetBytes(_HashSize));
return new PbkdfConfig(_Algorithm, _Version, salt, _Iterations, _HashAlorithm, pwd);
}
/// <summary>
/// Verifies a password against a PBKDF2 configuration
/// </summary>
/// <param name="password">The password to verify</param>
/// <returns>True if the password matches the configuration</returns>
public bool VerifyPassword(string password)
{
var pbkdf2 = new Rfc2898DeriveBytes(password, Convert.FromBase64String(Salt), Iterations, new HashAlgorithmName(HashAlorithm));
var pwd = Convert.ToBase64String(pbkdf2.GetBytes(_HashSize));
return pwd == Hash;
}
}
public string WebserverPasswordSalt
/// <summary>
/// Verifies a password against the stored PBKDF configuration
/// </summary>
public bool VerifyWebserverPassword(string password)
{
get
{
return settings[CONST.SERVER_PASSPHRASE_SALT];
}
var config = settings[CONST.PBKDF_CONFIG];
if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(config))
return false;
return JsonSerializer.Deserialize<PbkdfConfig>(config).VerifyPassword(LegacyPreparePassword(password));
}
/// <summary>
/// Prepares a password by pre-hashing it with a legacy salt, if needed
/// </summary>
/// <param name="password">The password to hash</param>
/// <returns>The hashed password</returns>
private string LegacyPreparePassword(string password)
{
if (string.IsNullOrWhiteSpace(settings[CONST.SERVER_PASSPHRASE_SALT]))
return password;
var buf = Convert.FromBase64String(settings[CONST.SERVER_PASSPHRASE_SALT]);
var sha256 = SHA256.Create();
var str = Encoding.UTF8.GetBytes(password);
sha256.TransformBlock(str, 0, str.Length, str, 0);
sha256.TransformFinalBlock(buf, 0, buf.Length);
return Convert.ToBase64String(sha256.Hash);
}
/// <summary>
/// Upgrades the password to a PBKDF configuration, if using the legacy password setup
/// </summary>
public void UpgradePasswordToKBDF()
{
if (!string.IsNullOrWhiteSpace(settings[CONST.PBKDF_CONFIG]))
return;
// Generate a random password if one is not set
var password = settings[CONST.SERVER_PASSPHRASE];
var autogenerated = false;
if (string.IsNullOrWhiteSpace(password))
{
password = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
settings[CONST.SERVER_PASSPHRASE_SALT] = null;
autogenerated = true;
}
// This will create a new PBKDF2 configuration
// In case the password already exists in the database,
// it will need use the pre-salted password as the password
var config = PbkdfConfig.CreatePBKDF2(password);
lock (databaseConnection.m_lock)
{
settings[CONST.PBKDF_CONFIG] = JsonSerializer.Serialize(config);
settings[CONST.SERVER_PASSPHRASE] = null;
settings[CONST.AUTOGENERATED_PASSPHRASE] = autogenerated.ToString();
if (autogenerated)
settings[CONST.DISABLE_TRAY_ICON_LOGIN] = false.ToString();
}
SaveSettings();
}
/// <summary>
/// Sets the webserver password
/// </summary>
/// <param name="password">The password to set</param>
public void SetWebserverPassword(string password)
{
if (string.IsNullOrWhiteSpace(password))
throw new Exception("Disabling password protection is not supported");
var config = PbkdfConfig.CreatePBKDF2(password);
lock (databaseConnection.m_lock)
{
lock(databaseConnection.m_lock)
{
settings[CONST.SERVER_PASSPHRASE] = "";
settings[CONST.SERVER_PASSPHRASE_SALT] = "";
}
}
else
{
var prng = RandomNumberGenerator.Create();
var buf = new byte[32];
prng.GetBytes(buf);
var salt = Convert.ToBase64String(buf);
var sha256 = System.Security.Cryptography.SHA256.Create();
var str = System.Text.Encoding.UTF8.GetBytes(password);
sha256.TransformBlock(str, 0, str.Length, str, 0);
sha256.TransformFinalBlock(buf, 0, buf.Length);
var pwd = Convert.ToBase64String(sha256.Hash);
lock(databaseConnection.m_lock)
{
settings[CONST.SERVER_PASSPHRASE] = pwd;
settings[CONST.SERVER_PASSPHRASE_SALT] = salt;
}
settings[CONST.SERVER_PASSPHRASE] = null;
settings[CONST.SERVER_PASSPHRASE_SALT] = null;
settings[CONST.AUTOGENERATED_PASSPHRASE] = false.ToString();
settings[CONST.PBKDF_CONFIG] = JsonSerializer.Serialize(config);
}
SaveSettings();
@@ -346,42 +443,22 @@ namespace Duplicati.Server.Database
SaveSettings();
}
public string WebserverPasswordTrayIcon => settings[CONST.SERVER_PASSPHRASETRAYICON];
public string WebserverPasswordTrayIconHash => settings[CONST.SERVER_PASSPHRASETRAYICONHASH];
public string AllowedHostnames => settings[CONST.SERVER_ALLOWED_HOSTNAMES];
public void GenerateWebserverPasswordTrayIcon()
public string JWTConfig
{
var password = "";
var pwd = "";
if (!string.IsNullOrEmpty(settings[CONST.SERVER_PASSPHRASE]))
get => settings[CONST.JWT_CONFIG];
set
{
password = Guid.NewGuid().ToString();
var buf = Convert.FromBase64String(settings[CONST.SERVER_PASSPHRASE_SALT]);
var sha256 = System.Security.Cryptography.SHA256.Create();
var str = System.Text.Encoding.UTF8.GetBytes(password);
sha256.TransformBlock(str, 0, str.Length, str, 0);
sha256.TransformFinalBlock(buf, 0, buf.Length);
pwd = Convert.ToBase64String(sha256.Hash);
lock (databaseConnection.m_lock)
settings[CONST.JWT_CONFIG] = value;
SaveSettings();
}
lock (databaseConnection.m_lock)
{
settings[CONST.SERVER_PASSPHRASETRAYICON] = password;
settings[CONST.SERVER_PASSPHRASETRAYICONHASH] = pwd;
}
SaveSettings();
}
public DateTime LastUpdateCheck
{
get
get
{
long t;
if (long.TryParse(settings[CONST.UPDATE_CHECK_LAST], out t))
@@ -391,7 +468,7 @@ namespace Duplicati.Server.Database
}
set
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.UPDATE_CHECK_LAST] = value.ToUniversalTime().Ticks.ToString();
SaveSettings();
}
@@ -409,7 +486,7 @@ namespace Duplicati.Server.Database
}
set
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.UPDATE_CHECK_INTERVAL] = value;
SaveSettings();
FIXMEGlobal.UpdatePoller.Reschedule();
@@ -443,7 +520,7 @@ namespace Duplicati.Server.Database
if (m_latestUpdate != null)
return m_latestUpdate;
using(var tr = new System.IO.StringReader(settings[CONST.UPDATE_CHECK_NEW_VERSION]))
using (var tr = new System.IO.StringReader(settings[CONST.UPDATE_CHECK_NEW_VERSION]))
return m_latestUpdate = Server.Serialization.Serializer.Deserialize<Library.AutoUpdater.UpdateInfo>(tr);
}
catch
@@ -458,14 +535,14 @@ namespace Duplicati.Server.Database
if (value != null)
{
var sb = new System.Text.StringBuilder();
using(var tw = new System.IO.StringWriter(sb))
using (var tw = new System.IO.StringWriter(sb))
Server.Serialization.Serializer.SerializeJson(tw, value);
result = sb.ToString();
}
m_latestUpdate = value;
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.UPDATE_CHECK_NEW_VERSION] = result;
SaveSettings();
@@ -474,13 +551,13 @@ namespace Duplicati.Server.Database
public string ServerListenInterface
{
get
get
{
return settings[CONST.SERVER_LISTEN_INTERFACE];
}
set
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.SERVER_LISTEN_INTERFACE] = value;
SaveSettings();
}
@@ -493,7 +570,7 @@ namespace Duplicati.Server.Database
if (String.IsNullOrEmpty(settings[CONST.SERVER_SSL_CERTIFICATE]))
return null;
if (Platform.IsClientWindows)
if (OperatingSystem.IsWindows())
return new X509Certificate2(Convert.FromBase64String(settings[CONST.SERVER_SSL_CERTIFICATE]));
else
return new X509Certificate2(Convert.FromBase64String(settings[CONST.SERVER_SSL_CERTIFICATE]), "");
@@ -507,7 +584,7 @@ namespace Duplicati.Server.Database
}
else
{
if (Platform.IsClientWindows)
if (OperatingSystem.IsWindows())
lock (databaseConnection.m_lock)
settings[CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(value.Export(X509ContentType.Pkcs12));
else
@@ -526,7 +603,7 @@ namespace Duplicati.Server.Database
}
set
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.HAS_FIXED_INVALID_BACKUPID] = value.ToString();
SaveSettings();
}
@@ -534,13 +611,13 @@ namespace Duplicati.Server.Database
public string UpdateChannel
{
get
get
{
return settings[CONST.UPDATE_CHANNEL];
}
set
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.UPDATE_CHANNEL] = value;
SaveSettings();
}
@@ -548,13 +625,13 @@ namespace Duplicati.Server.Database
public string UsageReporterLevel
{
get
get
{
return settings[CONST.USAGE_REPORTER_LEVEL];
}
set
{
lock(databaseConnection.m_lock)
lock (databaseConnection.m_lock)
settings[CONST.USAGE_REPORTER_LEVEL] = value;
SaveSettings();
}
+20 -16
View File
@@ -1,20 +1,24 @@
// Copyright (C) 2015, The Duplicati Team
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// http://www.duplicati.com, info@duplicati.com
//
// 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
using System;
namespace Duplicati.Server.Database
+20 -16
View File
@@ -1,20 +1,24 @@
// Copyright (C) 2015, The Duplicati Team
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// http://www.duplicati.com, info@duplicati.com
//
// 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
using System;
namespace Duplicati.Server.Database
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
</PropertyGroup>
<ItemGroup>
@@ -12,16 +13,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Win32.SystemEvents" Version="6.0.0" />
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
<PackageReference Include="Microsoft.Win32.SystemEvents" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<Reference Include="HttpServer">
<HintPath>..\thirdparty\HttpServer\HttpServer.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Duplicati\CommandLine\CLI\Duplicati.CommandLine.csproj" />
<ProjectReference Include="..\Duplicati\Library\Common\Duplicati.Library.Common.csproj" />
@@ -32,4 +26,9 @@
<ProjectReference Include="..\Duplicati\Server\Duplicati.Server.Serialization\Duplicati.Server.Serialization.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="WebServer\Kestrel\" />
</ItemGroup>
</Project>
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Duplicati.Server
{
+27 -17
View File
@@ -2,6 +2,10 @@
using Duplicati.Server;
using System;
using System.Collections.Generic;
using Duplicati.Library.RestAPI.Abstractions;
using Duplicati.Library.Utility;
using Duplicati.WebserverCore.Abstractions;
using Microsoft.Extensions.DependencyInjection;
namespace Duplicati.Library.RestAPI
{
@@ -11,17 +15,18 @@ namespace Duplicati.Library.RestAPI
*/
public static class FIXMEGlobal
{
/// <summary>
/// The placeholder for passwords in the UI
/// </summary>
public const string PASSWORD_PLACEHOLDER = "**********";
public static IServiceProvider Provider { get; set; }
/// <summary>
/// This is the only access to the database
/// </summary>
public static Server.Database.Connection DataConnection;
/// <summary>
/// The controller interface for pause/resume and throttle options
/// </summary>
public static LiveControls LiveControl;
/// <summary>
/// A delegate method for creating a copy of the current progress state
/// </summary>
@@ -30,24 +35,28 @@ namespace Duplicati.Library.RestAPI
/// <summary>
/// The status event signaler, used to control long polling of status updates
/// </summary>
public static readonly EventPollNotify StatusEventNotifyer = new EventPollNotify();
public static EventPollNotify StatusEventNotifyer => Provider.GetRequiredService<EventPollNotify>();
/// <summary>
/// For keeping and incrementing last last events Ids of db save and last notification
/// </summary>
public static INotificationUpdateService NotificationUpdateService => Provider.GetRequiredService<INotificationUpdateService>();
/// <summary>
/// Checks if the server has started and is listening for events
/// </summary>
public static bool IsServerStarted => Provider != null;
/// <summary>
/// This is the working thread
/// </summary>
public static Duplicati.Library.Utility.WorkerThread<Runner.IRunnerData> WorkThread;
public static WorkerThread<Runner.IRunnerData> WorkThread =>
Provider.GetRequiredService<IWorkerThreadsManager>().WorkerThread;
public static Func<long> PeekLastDataUpdateID;
public static Func<long> PeekLastNotificationUpdateID;
public static Action IncrementLastDataUpdateID;
public static Action IncrementLastNotificationUpdateID;
public static IWorkerThreadsManager WorkerThreadsManager =>
Provider.GetRequiredService<IWorkerThreadsManager>();
public static Action StartOrStopUsageReporter;
public static Action UpdateThrottleSpeeds;
/// <summary>
/// Gets the folder where Duplicati data is stored
/// </summary>
@@ -56,7 +65,7 @@ namespace Duplicati.Library.RestAPI
/// <summary>
/// This is the scheduling thread
/// </summary>
public static Scheduler Scheduler;
public static IScheduler Scheduler => Provider.GetRequiredService<IScheduler>();
/// <summary>
/// The log redirect handler
@@ -68,7 +77,7 @@ namespace Duplicati.Library.RestAPI
/// <summary>
/// The update poll thread.
/// </summary>
public static UpdatePollThread UpdatePoller;
public static UpdatePollThread UpdatePoller => Provider.GetRequiredService<UpdatePollThread>();
/// <summary>
@@ -93,5 +102,6 @@ namespace Duplicati.Library.RestAPI
/// This is the lock to be used before manipulating the shared resources
/// </summary>
public static readonly object MainLock = new object();
}
}
+55 -34
View File
@@ -1,27 +1,28 @@
#region Disclaimer / License
// Copyright (C) 2015, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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.
// 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:
//
// 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.
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
#endregion
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Text;
using Duplicati.Library.Common;
using Duplicati.Library.RestAPI;
using System.Runtime.Versioning;
using Duplicati.Library.IO;
using Duplicati.Server.Database;
namespace Duplicati.Server
{
@@ -29,7 +30,7 @@ namespace Duplicati.Server
/// This class keeps track of the users modifications regarding
/// throttling and pause/resume
/// </summary>
public class LiveControls
public class LiveControls : ILiveControls
{
/// <summary>
/// The tag used for logging
@@ -86,6 +87,8 @@ namespace Duplicati.Server
/// </summary>
public LiveControlState State { get { return m_state; } }
public bool IsPaused => State == LiveControlState.Paused;
/// <summary>
/// The internal variable that tracks the the priority
/// </summary>
@@ -109,8 +112,8 @@ namespace Duplicati.Server
/// <summary>
/// Gets the current overridden thread priority
/// </summary>
public System.Threading.ThreadPriority? ThreadPriority
{
public System.Threading.ThreadPriority? ThreadPriority
{
get { return m_priority; }
set
{
@@ -126,8 +129,8 @@ namespace Duplicati.Server
/// <summary>
/// Gets the current upload limit in bps
/// </summary>
public long? UploadLimit
{
public long? UploadLimit
{
get { return m_uploadLimit; }
set
{
@@ -143,8 +146,8 @@ namespace Duplicati.Server
/// <summary>
/// Gets the download limit in bps
/// </summary>
public long? DownloadLimit
{
public long? DownloadLimit
{
get { return m_downloadLimit; }
set
{
@@ -160,18 +163,34 @@ namespace Duplicati.Server
/// <summary>
/// The timer that is activated after a pause period.
/// </summary>
private readonly System.Threading.Timer m_waitTimer;
private System.Threading.Timer m_waitTimer;
/// <summary>
/// The time that the current pause is expected to expire
/// </summary>
private DateTime m_waitTimeExpiration = new DateTime(0);
/// <summary>
/// The connection to use
/// </summary>
private readonly Connection m_connection;
/// <summary>
/// Constructs a new instance of the LiveControl
/// </summary>
public LiveControls(Database.ServerSettings settings)
/// <param name="connection">The connection to use</param>
public LiveControls(Connection connection)
{
m_connection = connection;
Init();
}
/// <summary>
/// Constructs a new instance of the LiveControl
/// </summary>
private void Init()
{
var settings = m_connection.ApplicationSettings;
m_state = LiveControlState.Running;
m_waitTimer = new System.Threading.Timer(m_waitTimer_Tick, this, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
@@ -179,7 +198,7 @@ namespace Duplicati.Server
{
long milliseconds = 0;
try { milliseconds = (long)Duplicati.Library.Utility.Timeparser.ParseTimeSpan(settings.StartupDelayDuration).TotalMilliseconds; }
catch {}
catch { }
if (milliseconds > 0)
{
@@ -212,7 +231,7 @@ namespace Duplicati.Server
try
{
if (!Platform.IsClientPosix)
if (OperatingSystem.IsWindows())
RegisterHibernateMonitor();
}
catch { }
@@ -222,7 +241,7 @@ namespace Duplicati.Server
/// Event that occurs when the timeout duration is exceeded
/// </summary>
/// <param name="sender">The sender of the event</param>
private void m_waitTimer_Tick(object sender)
private void m_waitTimer_Tick(object sender)
{
lock (m_lock)
Resume();
@@ -269,7 +288,7 @@ namespace Duplicati.Server
/// </summary>
public void Pause()
{
lock(m_lock)
lock (m_lock)
{
var fireEvent = m_waitTimeExpiration.Ticks != 0 && m_state == LiveControlState.Paused && StateChanged != null;
@@ -337,6 +356,7 @@ namespace Duplicati.Server
/// <summary>
/// Method for calling a Win32 API
/// </summary>
[SupportedOSPlatform("windows")]
private void RegisterHibernateMonitor()
{
Microsoft.Win32.SystemEvents.PowerModeChanged += new Microsoft.Win32.PowerModeChangedEventHandler(SystemEvents_PowerModeChanged);
@@ -347,6 +367,7 @@ namespace Duplicati.Server
/// </summary>
/// <param name="sender">Unused sender parameter</param>
/// <param name="_e">The event information</param>
[SupportedOSPlatform("windows")]
private void SystemEvents_PowerModeChanged(object sender, object _e)
{
Microsoft.Win32.PowerModeChangedEventArgs e = _e as Microsoft.Win32.PowerModeChangedEventArgs;
@@ -380,7 +401,7 @@ namespace Duplicati.Server
{
long delayTicks = (m_suspendMinimumPause - DateTime.Now).Ticks;
var appset = FIXMEGlobal.DataConnection.ApplicationSettings;
var appset = m_connection.ApplicationSettings;
if (!string.IsNullOrEmpty(appset.StartupDelayDuration) && appset.StartupDelayDuration != "0")
try { delayTicks = Math.Max(delayTicks, Library.Utility.Timeparser.ParseTimeSpan(appset.StartupDelayDuration).Ticks); }
catch { }
+44 -38
View File
@@ -1,20 +1,24 @@
// Copyright (C) 2015, The Duplicati Team
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// http://www.duplicati.com, info@duplicati.com
//
// 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
using System;
using System.Linq;
using Duplicati.Library.Logging;
@@ -130,7 +134,7 @@ namespace Duplicati.Server
this.ExceptionID = exception.HelpID;
else
this.ExceptionID = entry.Exception.GetType().FullName;
}
}
@@ -150,15 +154,15 @@ namespace Duplicati.Server
{
m_buffer = new T[size];
if (initial != null)
foreach(var t in initial)
foreach (var t in initial)
this.Enqueue(t);
}
public int Length { get { return m_length; } }
public void Enqueue(T item)
{
lock(m_lock)
lock (m_lock)
{
m_key++;
m_buffer[m_head] = item;
@@ -174,7 +178,7 @@ namespace Duplicati.Server
public IEnumerator<T> GetEnumerator()
{
var k = m_key;
for(var i = 0; i < m_length; i++)
for (var i = 0; i < m_length; i++)
if (m_key != k)
throw new InvalidOperationException("Buffer was modified while reading");
else
@@ -190,7 +194,7 @@ namespace Duplicati.Server
public T[] FlatArray(Func<T, bool> filter = null)
{
lock(m_lock)
lock (m_lock)
if (filter == null)
return this.ToArray();
else
@@ -218,7 +222,7 @@ namespace Duplicati.Server
public void RenewTimeout(LogMessageType type)
{
lock(m_lock)
lock (m_lock)
{
m_timeouts[(int)type] = DateTime.Now.AddSeconds(30);
m_anytimeouts = true;
@@ -245,12 +249,12 @@ namespace Duplicati.Server
UpdateLogLevel();
offset = offset.ToUniversalTime();
lock(m_lock)
lock (m_lock)
{
if (m_buffer == null)
return new LogEntry[0];
return m_buffer.FlatArray((x) => x.When > offset && x.Type >= level );
return m_buffer.FlatArray((x) => x.When > offset && x.Type >= level);
}
}
@@ -259,18 +263,20 @@ namespace Duplicati.Server
RenewTimeout(level);
UpdateLogLevel();
lock(m_lock)
lock (m_lock)
{
if (m_buffer == null)
return new LogEntry[0];
var buffer = m_buffer.FlatArray((x) => x.ID > id && x.Type >= level );
var buffer = m_buffer.FlatArray((x) => x.ID > id && x.Type >= level);
// Return the <page_size> newest entries
if (buffer.Length > pagesize) {
if (buffer.Length > pagesize)
{
var index = buffer.Length - pagesize;
return buffer.Skip(index).Take(pagesize).ToArray();
}
else {
else
{
return buffer;
}
}
@@ -280,13 +286,13 @@ namespace Duplicati.Server
{
var i = 0;
return (from n in m_timeouts
let ix = i++
where n > DateTime.Now
select ix).ToArray();
let ix = i++
where n > DateTime.Now
select ix).ToArray();
}
private void UpdateLogLevel()
{
{
m_logLevel =
(LogMessageType)(GetActiveTimeouts().Union(new int[] { (int)m_serverloglevel }).Min());
}
@@ -296,9 +302,9 @@ namespace Duplicati.Server
public void WriteMessage(Duplicati.Library.Logging.LogEntry entry)
{
if (entry.Level < m_logLevel)
if (entry.Level < m_logLevel)
return;
if (m_serverfile != null && entry.Level >= m_serverloglevel)
try
{
@@ -308,7 +314,7 @@ namespace Duplicati.Server
{
}
lock(m_lock)
lock (m_lock)
{
if (m_anytimeouts)
{
@@ -0,0 +1,50 @@
namespace Duplicati.Library.RestAPI;
public interface INotificationUpdateService
{
/// <summary>
/// An event ID that increases whenever the database is updated
/// </summary>
long LastDataUpdateId { get; }
/// <summary>
/// An event ID that increases whenever a notification is updated
/// </summary>
long LastNotificationUpdateId { get; }
void IncrementLastDataUpdateId();
void IncrementLastNotificationUpdateId();
}
public class NotificationUpdateService : INotificationUpdateService
{
/// <summary>
/// An event ID that increases whenever the database is updated
/// </summary>
public long LastDataUpdateId { get; private set; } = 0;
private readonly object _lastDataUpdateIdLock = new();
/// <summary>
/// An event ID that increases whenever a notification is updated
/// </summary>
public long LastNotificationUpdateId { get; private set; } = 0;
private readonly object _lastNotificationUpdateIdLock = new();
public void IncrementLastDataUpdateId()
{
lock (_lastDataUpdateIdLock)
{
LastDataUpdateId++;
}
}
public void IncrementLastNotificationUpdateId()
{
lock (_lastNotificationUpdateIdLock)
{
LastNotificationUpdateId++;
}
}
}
@@ -1,57 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Utility;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class Acknowledgements : IRESTMethodGET, IRESTMethodDocumented
{
private class GetResponse
{
public string Status;
public string Acknowledgements;
}
public void GET(string key, RequestInfo info)
{
var path = SystemIO.IO_OS.PathCombine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "acknowledgements.txt");
info.OutputOK(new GetResponse() {
Status = "OK",
Acknowledgements = System.IO.File.ReadAllText(path)
});
}
public string Description { get { return "Gets all acknowledgements"; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(GetResponse)),
};
}
}
}
}
@@ -1,742 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using Duplicati.Server.Serialization;
using System.IO;
using System.Linq;
using Duplicati.Server.Serialization.Interface;
using Duplicati.Library.RestAPI;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class Backup : IRESTMethodGET, IRESTMethodPUT, IRESTMethodPOST, IRESTMethodDELETE, IRESTMethodDocumented
{
public class GetResponse
{
public class GetResponseData
{
public Serialization.Interface.ISchedule Schedule;
public Serialization.Interface.IBackup Backup;
public Dictionary<string, string> DisplayNames;
}
public bool success;
public GetResponseData data;
}
private void SearchFiles(IBackup backup, string filterstring, RequestInfo info)
{
var filter = filterstring;
var timestring = info.Request.QueryString["time"].Value;
var allversion = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["all-versions"].Value, false);
if (string.IsNullOrWhiteSpace(timestring) && !allversion)
{
info.ReportClientError("Invalid or missing time", System.Net.HttpStatusCode.BadRequest);
return;
}
var prefixonly = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["prefix-only"].Value, false);
var foldercontents = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["folder-contents"].Value, false);
var time = new DateTime();
if (!allversion)
time = Duplicati.Library.Utility.Timeparser.ParseTimeInterval(timestring, DateTime.Now);
var r = Runner.Run(Runner.CreateListTask(backup, new string[] { filter }, prefixonly, allversion, foldercontents, time), false) as Duplicati.Library.Interface.IListResults;
var result = new Dictionary<string, object>();
foreach(HttpServer.HttpInputItem n in info.Request.QueryString)
result[n.Name] = n.Value;
result["Filesets"] = r.Filesets;
result["Files"] = r.Files
// Group directories first - support either directory separator here as we may be restoring data from an alternate platform
.OrderByDescending(f => (f.Path.StartsWith("/", StringComparison.Ordinal) && f.Path.EndsWith("/", StringComparison.Ordinal)) || (!f.Path.StartsWith("/", StringComparison.Ordinal) && f.Path.EndsWith("\\", StringComparison.Ordinal)))
// Sort both groups (directories and files) alphabetically
.ThenBy(f => f.Path);
info.OutputOK(result);
}
private void ListFileSets(IBackup backup, RequestInfo info)
{
var input = info.Request.QueryString;
var extra = new Dictionary<string, string>
{
["list-sets-only"] = "true"
};
if (input["include-metadata"].Value != null)
extra["list-sets-only"] = (!Library.Utility.Utility.ParseBool(input["include-metadata"].Value, false)).ToString();
if (input["from-remote-only"].Value != null)
extra["no-local-db"] = Library.Utility.Utility.ParseBool(input["from-remote-only"].Value, false).ToString();
var r = Runner.Run(Runner.CreateTask(DuplicatiOperation.List, backup, extra), false) as Duplicati.Library.Interface.IListResults;
if (r.EncryptedFiles && backup.Settings.Any(x => string.Equals("--no-encryption", x.Name, StringComparison.OrdinalIgnoreCase)))
info.ReportServerError("encrypted-storage");
else
info.OutputOK(r.Filesets);
}
private void FetchLogData(IBackup backup, RequestInfo info)
{
using(var con = Duplicati.Library.SQLiteHelper.SQLiteLoader.LoadConnection(backup.DBPath))
using(var cmd = con.CreateCommand())
info.OutputOK(LogData.DumpTable(cmd, "LogData", "ID", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value));
}
private void FetchRemoteLogData(IBackup backup, RequestInfo info)
{
using(var con = Duplicati.Library.SQLiteHelper.SQLiteLoader.LoadConnection(backup.DBPath))
using(var cmd = con.CreateCommand())
{
var dt = LogData.DumpTable(cmd, "RemoteOperation", "ID", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value);
// Unwrap raw data to a string
foreach(var n in dt)
try { n["Data"] = System.Text.Encoding.UTF8.GetString((byte[])n["Data"]); }
catch { }
info.OutputOK(dt);
}
}
private void IsDBUsedElseWhere(IBackup backup, RequestInfo info)
{
info.OutputOK(new { inuse = Library.Main.DatabaseLocator.IsDatabasePathInUse(backup.DBPath) });
}
public static void RemovePasswords(IBackup backup)
{
backup.SanitizeSettings();
backup.SanitizeTargetUrl();
}
private void Export(IBackup backup, RequestInfo info)
{
var cmdline = Library.Utility.Utility.ParseBool(info.Request.QueryString["cmdline"].Value, false);
var argsonly = Library.Utility.Utility.ParseBool(info.Request.QueryString["argsonly"].Value, false);
var exportPasswords = Library.Utility.Utility.ParseBool(info.Request.QueryString["export-passwords"].Value, false);
if (!exportPasswords)
{
Backup.RemovePasswords(backup);
}
if (cmdline)
{
info.OutputOK(new { Command = Runner.GetCommandLine(Runner.CreateTask(DuplicatiOperation.Backup, backup)) });
}
else if (argsonly)
{
var parts = Runner.GetCommandLineParts(Runner.CreateTask(DuplicatiOperation.Backup, backup));
info.OutputOK(new {
Backend = parts.First(),
Arguments = parts.Skip(1).Where(x => !x.StartsWith("--", StringComparison.Ordinal)),
Options = parts.Skip(1).Where(x => x.StartsWith("--", StringComparison.Ordinal))
});
}
else
{
var passphrase = info.Request.QueryString["passphrase"].Value;
byte[] data = Backup.ExportToJSON(backup, passphrase);
string filename = Library.Utility.Uri.UrlEncode(backup.Name) + "-duplicati-config.json";
if (!string.IsNullOrWhiteSpace(passphrase))
{
filename += ".aes";
}
info.Response.ContentLength = data.Length;
info.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", filename));
info.Response.ContentType = "application/octet-stream";
info.BodyWriter.SetOK();
info.Response.SendHeaders();
info.Response.SendBody(data);
}
}
public static byte[] ExportToJSON(IBackup backup, string passphrase)
{
Serializable.ImportExportStructure ipx = FIXMEGlobal.DataConnection.PrepareBackupForExport(backup);
byte[] data;
using (MemoryStream ms = new System.IO.MemoryStream())
{
using (StreamWriter sw = new System.IO.StreamWriter(ms))
{
Serializer.SerializeJson(sw, ipx, true);
if (!string.IsNullOrWhiteSpace(passphrase))
{
ms.Position = 0;
using (MemoryStream ms2 = new System.IO.MemoryStream())
{
using (Library.Encryption.AESEncryption m = new Duplicati.Library.Encryption.AESEncryption(passphrase, new Dictionary<string, string>()))
{
m.Encrypt(ms, ms2);
data = ms2.ToArray();
}
}
}
else
{
data = ms.ToArray();
}
}
}
return data;
}
private void RestoreFiles(IBackup backup, RequestInfo info)
{
var input = info.Request.Form;
string[] filters = parsePaths(input["paths"].Value ?? string.Empty);
var passphrase = string.IsNullOrEmpty(input["passphrase"].Value) ? null : input["passphrase"].Value;
var time = Duplicati.Library.Utility.Timeparser.ParseTimeInterval(input["time"].Value, DateTime.Now);
var restoreTarget = input["restore-path"].Value;
var overwrite = Duplicati.Library.Utility.Utility.ParseBool(input["overwrite"].Value, false);
var permissions = Duplicati.Library.Utility.Utility.ParseBool(input["permissions"].Value, false);
var skip_metadata = Duplicati.Library.Utility.Utility.ParseBool(input["skip-metadata"].Value, false);
var task = Runner.CreateRestoreTask(backup, filters, time, restoreTarget, overwrite, permissions, skip_metadata, passphrase);
FIXMEGlobal.WorkThread.AddTask(task);
info.OutputOK(new { TaskID = task.TaskID });
}
private void CreateReport(IBackup backup, RequestInfo info)
{
var task = Runner.CreateTask(DuplicatiOperation.CreateReport, backup);
FIXMEGlobal.WorkThread.AddTask(task);
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
info.OutputOK(new { Status = "OK", ID = task.TaskID });
}
private void ReportRemoteSize(IBackup backup, RequestInfo info)
{
var task = Runner.CreateTask(DuplicatiOperation.ListRemote, backup);
FIXMEGlobal.WorkThread.AddTask(task);
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
info.OutputOK(new { Status = "OK", ID = task.TaskID });
}
private void Repair(IBackup backup, RequestInfo info)
{
DoRepair(backup, info, false);
}
private void RepairUpdate(IBackup backup, RequestInfo info)
{
DoRepair(backup, info, true);
}
private void Vacuum(IBackup backup, RequestInfo info)
{
var task = Runner.CreateTask(DuplicatiOperation.Vacuum, backup);
FIXMEGlobal.WorkThread.AddTask(task);
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
info.OutputOK(new { Status = "OK", ID = task.TaskID });
}
private void Verify(IBackup backup, RequestInfo info)
{
var task = Runner.CreateTask(DuplicatiOperation.Verify, backup);
FIXMEGlobal.WorkThread.AddTask(task);
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
info.OutputOK(new {Status = "OK", ID = task.TaskID});
}
private void Compact(IBackup backup, RequestInfo info)
{
var task = Runner.CreateTask(DuplicatiOperation.Compact, backup);
FIXMEGlobal.WorkThread.AddTask(task);
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
info.OutputOK(new { Status = "OK", ID = task.TaskID });
}
private string[] parsePaths(string paths)
{
string[] filters;
var rawpaths = (paths ?? string.Empty).Trim();
// We send the file list as a JSON array to avoid encoding issues with the path separator
// as it is an allowed character in file and path names.
// We also accept the old way, for compatibility with the greeno theme
if (!string.IsNullOrWhiteSpace(rawpaths) && rawpaths.StartsWith("[", StringComparison.Ordinal) && rawpaths.EndsWith("]", StringComparison.Ordinal))
filters = Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>(rawpaths);
else
filters = paths.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries);
return filters;
}
private void DoRepair(IBackup backup, RequestInfo info, bool repairUpdate)
{
var input = info.Request.Form;
string[] filters = null;
var extra = new Dictionary<string, string>();
if (input["only-paths"].Value != null)
extra["repair-only-paths"] = (Library.Utility.Utility.ParseBool(input["only-paths"].Value, false)).ToString();
if (input["time"].Value != null)
extra["time"] = input["time"].Value;
if (input["version"].Value != null)
extra["version"] = input["version"].Value;
if (input["paths"].Value != null)
filters = parsePaths(input["paths"].Value);
var task = Runner.CreateTask(repairUpdate ? DuplicatiOperation.RepairUpdate : DuplicatiOperation.Repair, backup, extra, filters);
FIXMEGlobal.WorkThread.AddTask(task);
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
info.OutputOK(new {Status = "OK", ID = task.TaskID});
}
private void RunBackup(IBackup backup, RequestInfo info)
{
var t = FIXMEGlobal.WorkThread.CurrentTask;
var bt = t == null ? null : t.Backup;
if (bt != null && backup.ID == bt.ID)
{
// Already running
}
else if (FIXMEGlobal.WorkThread.CurrentTasks.Any(x => {
var bn = x?.Backup;
return bn == null || bn.ID == backup.ID;
}))
{
// Already in queue
}
else
{
FIXMEGlobal.WorkThread.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup), true);
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
}
info.OutputOK();
}
private void IsActive(IBackup backup, RequestInfo info)
{
var t = FIXMEGlobal.WorkThread.CurrentTask;
var bt = t?.Backup;
if (bt != null && backup.ID == bt.ID)
{
info.OutputOK(new { Status = "OK", Active = true });
return;
}
else if (FIXMEGlobal.WorkThread.CurrentTasks.Any(x =>
{
var bn = x?.Backup;
return bn == null || bn.ID == backup.ID;
}))
{
info.OutputOK(new { Status = "OK", Active = true });
return;
}
else
{
info.OutputOK(new { Status = "OK", Active = false });
return;
}
}
private void UpdateDatabasePath(IBackup backup, RequestInfo info, bool move)
{
var np = info.Request.Form["path"].Value;
if (string.IsNullOrWhiteSpace(np))
info.ReportClientError("No target path supplied", System.Net.HttpStatusCode.BadRequest);
else if (!Path.IsPathRooted(np))
info.ReportClientError("Target path is relative, please supply a fully qualified path", System.Net.HttpStatusCode.BadRequest);
else
{
if (move && (File.Exists(np) || Directory.Exists(np)))
info.ReportClientError("A file already exists at the new location", System.Net.HttpStatusCode.Conflict);
else
{
if (move)
File.Move(backup.DBPath, np);
FIXMEGlobal.DataConnection.UpdateBackupDBPath(backup, np);
}
}
}
public void GET(string key, RequestInfo info)
{
var parts = (key ?? "").Split(new char[] { '/' }, 2);
var bk = FIXMEGlobal.DataConnection.GetBackup(parts.First());
if (bk == null)
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
else
{
if (parts.Length > 1)
{
var operation = parts.Last().Split(new char[] {'/'}).First().ToLowerInvariant();
switch (operation)
{
case "files":
var filter = parts.Last().Split(new char[] { '/' }, 2).Skip(1).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(info.Request.QueryString["filter"].Value))
filter = info.Request.QueryString["filter"].Value;
SearchFiles(bk, filter, info);
return;
case "log":
FetchLogData(bk, info);
return;
case "remotelog":
FetchRemoteLogData(bk, info);
return;
case "filesets":
ListFileSets(bk, info);
return;
case "export":
Export(bk, info);
return;
case "isdbusedelsewhere":
IsDBUsedElseWhere(bk, info);
return;
case "isactive":
IsActive(bk, info);
return;
default:
info.ReportClientError(string.Format("Invalid component: {0}", operation), System.Net.HttpStatusCode.BadRequest);
return;
}
}
var scheduleId = FIXMEGlobal.DataConnection.GetScheduleIDsFromTags(new string[] { "ID=" + bk.ID });
var schedule = scheduleId.Any() ? FIXMEGlobal.DataConnection.GetSchedule(scheduleId.First()) : null;
var sourcenames = SpecialFolders.GetSourceNames(bk);
//TODO: Filter out the password in both settings and the target url
info.OutputOK(new GetResponse()
{
success = true,
data = new GetResponse.GetResponseData {
Schedule = schedule,
Backup = bk,
DisplayNames = sourcenames
}
});
}
}
public void POST(string key, RequestInfo info)
{
var parts = (key ?? "").Split(new char[] { '/' }, 2);
var bk = FIXMEGlobal.DataConnection.GetBackup(parts.First());
if (bk == null)
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
else
{
if (parts.Length > 1)
{
var operation = parts.Last().Split(new char[] { '/' }).First().ToLowerInvariant();
switch (operation)
{
case "deletedb":
System.IO.File.Delete(bk.DBPath);
info.OutputOK();
return;
case "movedb":
UpdateDatabasePath(bk, info, true);
return;
case "updatedb":
UpdateDatabasePath(bk, info, false);
return;
case "restore":
RestoreFiles(bk, info);
return;
case "createreport":
CreateReport(bk, info);
return;
case "repair":
Repair(bk, info);
return;
case "repairupdate":
RepairUpdate(bk, info);
return;
case "vacuum":
Vacuum(bk, info);
return;
case "verify":
Verify(bk, info);
return;
case "compact":
Compact(bk, info);
return;
case "start":
case "run":
RunBackup(bk, info);
return;
case "report-remote-size":
ReportRemoteSize(bk, info);
return;
case "copytotemp":
var ipx = Serializer.Deserialize<Database.Backup>(new StringReader(Newtonsoft.Json.JsonConvert.SerializeObject(bk)));
using(var tf = new Duplicati.Library.Utility.TempFile())
ipx.DBPath = tf;
ipx.ID = null;
info.OutputOK(new { status = "OK", ID = FIXMEGlobal.DataConnection.RegisterTemporaryBackup(ipx) });
return;
}
}
info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest);
}
}
public void PUT(string key, RequestInfo info)
{
string str = info.Request.Form["data"].Value;
if (string.IsNullOrWhiteSpace(str))
str = System.Threading.Tasks.Task.Run(async () =>
{
using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
return await sr.ReadToEndAsync();
}).GetAwaiter().GetResult();
if (string.IsNullOrWhiteSpace(str))
{
info.ReportClientError("Missing backup object", System.Net.HttpStatusCode.BadRequest);
return;
}
Backups.AddOrUpdateBackupData data = null;
try
{
data = Serializer.Deserialize<Backups.AddOrUpdateBackupData>(new StringReader(str));
if (data.Backup == null)
{
info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest);
return;
}
if (!string.IsNullOrEmpty(key))
data.Backup.ID = key;
if (string.IsNullOrEmpty(data.Backup.ID))
{
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.BadRequest);
return;
}
if (data.Backup.IsTemporary)
{
var backup = FIXMEGlobal.DataConnection.GetBackup(data.Backup.ID);
if (backup.IsTemporary)
throw new InvalidDataException("External is temporary but internal is not?");
FIXMEGlobal.DataConnection.UpdateTemporaryBackup(backup);
info.OutputOK();
}
else
{
lock(FIXMEGlobal.DataConnection.m_lock)
{
var backup = FIXMEGlobal.DataConnection.GetBackup(data.Backup.ID);
if (backup == null)
{
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
return;
}
if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase) && x.ID != data.Backup.ID))
{
info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict);
return;
}
var err = FIXMEGlobal.DataConnection.ValidateBackup(data.Backup, data.Schedule);
if (!string.IsNullOrWhiteSpace(err))
{
info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest);
return;
}
//TODO: Merge in real passwords where the placeholder is found
FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule);
}
info.OutputOK();
}
}
catch (Exception ex)
{
if (data == null)
info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
else
info.ReportClientError(string.Format("Unable to save backup or schedule: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError);
}
}
public void DELETE(string key, RequestInfo info)
{
var backup = FIXMEGlobal.DataConnection.GetBackup(key);
if (backup == null)
{
info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
return;
}
var delete_remote_files = Library.Utility.Utility.ParseBool(info.Request.Param["delete-remote-files"].Value, false);
if (delete_remote_files)
{
var captcha_token = info.Request.Param["captcha-token"].Value;
var captcha_answer = info.Request.Param["captcha-answer"].Value;
if (string.IsNullOrWhiteSpace(captcha_token) || string.IsNullOrWhiteSpace(captcha_answer))
{
info.ReportClientError("Missing captcha", System.Net.HttpStatusCode.Unauthorized);
return;
}
if (!Captcha.SolvedCaptcha(captcha_token, "DELETE /backup/" + backup.ID, captcha_answer))
{
info.ReportClientError("Invalid captcha", System.Net.HttpStatusCode.Forbidden);
return;
}
}
if (FIXMEGlobal.WorkThread.Active)
{
try
{
//TODO: It's not safe to access the values like this,
//because the runner thread might interfere
var nt = FIXMEGlobal.WorkThread.CurrentTask;
if (backup.Equals(nt == null ? null : nt.Backup))
{
bool force;
if (!bool.TryParse(info.Request.QueryString["force"].Value, out force))
force = false;
if (!force)
{
info.OutputError(new { status = "failed", reason = "backup-in-progress" });
return;
}
bool hasPaused = FIXMEGlobal.LiveControl.State == LiveControls.LiveControlState.Paused;
FIXMEGlobal.LiveControl.Pause();
for(int i = 0; i < 10; i++)
if (FIXMEGlobal.WorkThread.Active)
{
var t = FIXMEGlobal.WorkThread.CurrentTask;
if (backup.Equals(t == null ? null : t.Backup))
System.Threading.Thread.Sleep(1000);
else
break;
}
else
break;
if (FIXMEGlobal.WorkThread.Active)
{
var t = FIXMEGlobal.WorkThread.CurrentTask;
if (backup.Equals(t == null ? null : t.Backup))
{
if (hasPaused)
FIXMEGlobal.LiveControl.Resume();
info.OutputError(new { status = "failed", reason = "backup-unstoppable" });
return;
}
}
if (hasPaused)
FIXMEGlobal.LiveControl.Resume();
}
}
catch (Exception ex)
{
info.OutputError(new { status = "error", message = ex.Message });
return;
}
}
var extra = new Dictionary<string, string>();
if (!string.IsNullOrWhiteSpace(info.Request.Param["delete-local-db"].Value))
extra["delete-local-db"] = info.Request.Param["delete-local-db"].Value;
if (delete_remote_files)
extra["delete-remote-files"] = "true";
var task = Runner.CreateTask(DuplicatiOperation.Delete, backup, extra);
FIXMEGlobal.WorkThread.AddTask(task);
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
info.OutputOK(new { Status = "OK", ID = task.TaskID });
}
public string Description { get { return "Retrieves, updates or deletes an existing backup and schedule"; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(GetResponse)),
new KeyValuePair<string, Type>(HttpServer.Method.Put, typeof(Backups.AddOrUpdateBackupData)),
new KeyValuePair<string, Type>(HttpServer.Method.Delete, typeof(long))
};
}
}
}
}
@@ -1,141 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
using Duplicati.Library.Common.IO;
using Duplicati.Library.RestAPI;
using Duplicati.Library.Utility;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class BackupDefaults : IRESTMethodGET
{
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType<BackupDefaults>();
public void GET(string key, RequestInfo info)
{
// Start with a scratch object
var o = new Newtonsoft.Json.Linq.JObject();
// Add application wide settings
o.Add("ApplicationOptions", new Newtonsoft.Json.Linq.JArray(
from n in FIXMEGlobal.DataConnection.Settings
select Newtonsoft.Json.Linq.JObject.FromObject(n)
));
try
{
// Add built-in defaults
Newtonsoft.Json.Linq.JObject n;
using(var s = new System.IO.StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(FIXMEGlobal), "newbackup.json")))
n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(s.ReadToEnd());
MergeJsonObjects(o, n);
}
catch (Exception e)
{
Library.Logging.Log.WriteErrorMessage(LOGTAG, "BackupDefaultsError", e, "Failed to locate embeded backup defaults");
}
try
{
// Add install defaults/overrides, if present
var path = SystemIO.IO_OS.PathCombine(Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR, "newbackup.json");
if (System.IO.File.Exists(path))
{
Newtonsoft.Json.Linq.JObject n;
n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(System.IO.File.ReadAllText(path));
MergeJsonObjects(o, n);
}
}
catch (Exception e)
{
Library.Logging.Log.WriteErrorMessage(LOGTAG, "BackupDefaultsError", e, "Failed to process newbackup.json");
}
info.OutputOK(new
{
success = true,
data = o
});
}
private static void MergeJsonObjects(Newtonsoft.Json.Linq.JObject self, Newtonsoft.Json.Linq.JObject other)
{
foreach(var p in other.Properties())
{
var sp = self.Property(p.Name);
if (sp == null)
self.Add(p);
else
{
switch (p.Type)
{
// Primitives override
case Newtonsoft.Json.Linq.JTokenType.Boolean:
case Newtonsoft.Json.Linq.JTokenType.Bytes:
case Newtonsoft.Json.Linq.JTokenType.Comment:
case Newtonsoft.Json.Linq.JTokenType.Constructor:
case Newtonsoft.Json.Linq.JTokenType.Date:
case Newtonsoft.Json.Linq.JTokenType.Float:
case Newtonsoft.Json.Linq.JTokenType.Guid:
case Newtonsoft.Json.Linq.JTokenType.Integer:
case Newtonsoft.Json.Linq.JTokenType.String:
case Newtonsoft.Json.Linq.JTokenType.TimeSpan:
case Newtonsoft.Json.Linq.JTokenType.Uri:
case Newtonsoft.Json.Linq.JTokenType.None:
case Newtonsoft.Json.Linq.JTokenType.Null:
case Newtonsoft.Json.Linq.JTokenType.Undefined:
self.Replace(p);
break;
// Arrays merge
case Newtonsoft.Json.Linq.JTokenType.Array:
if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Array)
sp.Value = new Newtonsoft.Json.Linq.JArray(((Newtonsoft.Json.Linq.JArray)sp.Value).Union((Newtonsoft.Json.Linq.JArray)p.Value));
else
{
var a = new Newtonsoft.Json.Linq.JArray(sp.Value);
sp.Value = new Newtonsoft.Json.Linq.JArray(a.Union((Newtonsoft.Json.Linq.JArray)p.Value));
}
break;
// Objects merge
case Newtonsoft.Json.Linq.JTokenType.Object:
if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Object)
MergeJsonObjects((Newtonsoft.Json.Linq.JObject)sp.Value, (Newtonsoft.Json.Linq.JObject)p.Value);
else
sp.Value = p.Value;
break;
// Ignore other stuff
default:
break;
}
}
}
}
}
}
@@ -1,310 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
using System.Collections.Generic;
using Duplicati.Server.Serialization;
using System.IO;
using Duplicati.Library.RestAPI;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class Backups : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented
{
public class AddOrUpdateBackupData
{
public Boolean IsUnencryptedOrPassphraseStored { get; set;}
public Database.Schedule Schedule { get; set;}
public Database.Backup Backup { get; set;}
}
public void GET(string key, RequestInfo info)
{
var schedules = FIXMEGlobal.DataConnection.Schedules;
var backups = FIXMEGlobal.DataConnection.Backups;
var all = from n in backups
select new AddOrUpdateBackupData {
IsUnencryptedOrPassphraseStored = FIXMEGlobal.DataConnection.IsUnencryptedOrPassphraseStored(long.Parse(n.ID)),
Backup = (Database.Backup)n,
Schedule =
(from x in schedules
where x.Tags != null && x.Tags.Contains("ID=" + n.ID)
select (Database.Schedule)x).FirstOrDefault()
};
info.BodyWriter.OutputOK(all.ToArray());
}
private void ImportBackup(RequestInfo info)
{
var output_template = "<html><body><script type=\"text/javascript\">var jso = 'JSO'; var rp = null; try { rp = parent['CBM']; } catch (e) {}; if (rp) { rp('MSG', jso); } else { alert; rp('MSG'); };</script></body></html>";
//output_template = "<html><body><script type=\"text/javascript\">alert('MSG');</script></body></html>";
try
{
var input = info.Request.Form;
var cmdline = Library.Utility.Utility.ParseBool(input["cmdline"].Value, false);
var import_metadata = Library.Utility.Utility.ParseBool(input["import_metadata"].Value, false);
var direct = Library.Utility.Utility.ParseBool(input["direct"].Value, false);
output_template = output_template.Replace("CBM", input["callback"].Value);
if (cmdline)
{
info.Response.ContentType = "text/html";
info.BodyWriter.Write(output_template.Replace("MSG", "Import from commandline not yet implemented"));
}
else
{
var file = info.Request.Form.GetFile("config");
if (file == null)
throw new Exception("No file uploaded");
Serializable.ImportExportStructure ipx = Backups.LoadConfiguration(file.Filename, import_metadata, () => input["passphrase"].Value);
if (direct)
{
lock (FIXMEGlobal.DataConnection.m_lock)
{
var basename = ipx.Backup.Name;
var c = 0;
while (c++ < 100 && FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase)))
ipx.Backup.Name = basename + " (" + c.ToString() + ")";
if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase)))
{
info.BodyWriter.SetOK();
info.Response.ContentType = "text/html";
info.BodyWriter.Write(output_template.Replace("MSG", "There already exists a backup with the name: " + basename.Replace("\'", "\\'")));
}
var err = FIXMEGlobal.DataConnection.ValidateBackup(ipx.Backup, ipx.Schedule);
if (!string.IsNullOrWhiteSpace(err))
{
info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest);
return;
}
FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(ipx.Backup, ipx.Schedule);
}
info.Response.ContentType = "text/html";
info.BodyWriter.Write(output_template.Replace("MSG", "OK"));
}
else
{
using (var sw = new StringWriter())
{
Serializer.SerializeJson(sw, ipx, true);
output_template = output_template.Replace("'JSO'", sw.ToString());
}
info.BodyWriter.Write(output_template.Replace("MSG", "Import completed, but a browser issue prevents loading the contents. Try using the direct import method instead."));
}
}
}
catch (Exception ex)
{
FIXMEGlobal.DataConnection.LogError("", "Failed to import backup", ex);
info.Response.ContentType = "text/html";
info.BodyWriter.Write(output_template.Replace("MSG", ex.Message.Replace("\'", "\\'").Replace("\r", "\\r").Replace("\n", "\\n")));
}
}
public static Serializable.ImportExportStructure ImportBackup(string configurationFile, bool importMetadata, Func<string> getPassword, Dictionary<string, string> advancedOptions)
{
// This removes the ID and DBPath from the backup configuration.
Serializable.ImportExportStructure importedStructure = Backups.LoadConfiguration(configurationFile, importMetadata, getPassword);
// This will create the Duplicati-server.sqlite database file if it doesn't exist.
using (Duplicati.Server.Database.Connection connection = FIXMEGlobal.GetDatabaseConnection(advancedOptions))
{
if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase)))
{
throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists.");
}
string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule);
if (!string.IsNullOrWhiteSpace(error))
{
throw new InvalidOperationException(error);
}
// This creates a new ID and DBPath.
connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule);
}
return importedStructure;
}
private static Serializable.ImportExportStructure LoadConfiguration(string filename, bool importMetadata, Func<string> getPassword)
{
Serializable.ImportExportStructure ipx;
var buf = new byte[3];
using (var fs = System.IO.File.OpenRead(filename))
{
Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buf, buf.Length);
fs.Position = 0;
if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S')
{
using (var m = new Duplicati.Library.Encryption.AESEncryption(getPassword(), new Dictionary<string, string>()))
{
using (var m2 = m.Decrypt(fs))
{
using (var sr = new System.IO.StreamReader(m2))
{
ipx = Serializer.Deserialize<Serializable.ImportExportStructure>(sr);
}
}
}
}
else
{
using (var sr = new System.IO.StreamReader(fs))
{
ipx = Serializer.Deserialize<Serializable.ImportExportStructure>(sr);
}
}
}
if (ipx.Backup == null)
{
throw new Exception("No backup found in document");
}
if (ipx.Backup.Metadata == null)
{
ipx.Backup.Metadata = new Dictionary<string, string>();
}
if (!importMetadata)
{
ipx.Backup.Metadata.Clear();
}
ipx.Backup.ID = null;
ipx.Backup.DBPath = null;
if (ipx.Schedule != null)
{
ipx.Schedule.ID = -1;
}
return ipx;
}
public void POST(string key, RequestInfo info)
{
if ("import".Equals(key, StringComparison.OrdinalIgnoreCase))
{
ImportBackup(info);
return;
}
AddOrUpdateBackupData data = null;
try
{
var str = info.Request.Form["data"].Value;
if (string.IsNullOrWhiteSpace(str)) {
str = System.Threading.Tasks.Task.Run(async () =>
{
using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
return await sr.ReadToEndAsync();
}).GetAwaiter().GetResult();
}
data = Serializer.Deserialize<AddOrUpdateBackupData>(new StringReader(str));
if (data.Backup == null)
{
info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest);
return;
}
data.Backup.ID = null;
if (Duplicati.Library.Utility.Utility.ParseBool(info.Request.Form["temporary"].Value, false))
{
using(var tf = new Duplicati.Library.Utility.TempFile())
data.Backup.DBPath = tf;
data.Backup.Filters = data.Backup.Filters ?? new Duplicati.Server.Serialization.Interface.IFilter[0];
data.Backup.Settings = data.Backup.Settings ?? new Duplicati.Server.Serialization.Interface.ISetting[0];
FIXMEGlobal.DataConnection.RegisterTemporaryBackup(data.Backup);
info.OutputOK(new { status = "OK", ID = data.Backup.ID });
}
else
{
if (Library.Utility.Utility.ParseBool(info.Request.Form["existing_db"].Value, false))
{
data.Backup.DBPath = Library.Main.DatabaseLocator.GetDatabasePath(data.Backup.TargetURL, null, false, false);
if (string.IsNullOrWhiteSpace(data.Backup.DBPath))
throw new Exception("Unable to find remote db path?");
}
lock(FIXMEGlobal.DataConnection.m_lock)
{
if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase)))
{
info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict);
return;
}
var err = FIXMEGlobal.DataConnection.ValidateBackup(data.Backup, data.Schedule);
if (!string.IsNullOrWhiteSpace(err))
{
info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest);
return;
}
FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule);
}
info.OutputOK(new { status = "OK", ID = data.Backup.ID });
}
}
catch (Exception ex)
{
if (data == null)
info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
else
info.ReportClientError(string.Format("Unable to save schedule or backup object: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError);
}
}
public string Description { get { return "Return a list of current backups and their schedules"; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(AddOrUpdateBackupData[])),
new KeyValuePair<string, Type>(HttpServer.Method.Post, typeof(AddOrUpdateBackupData))
};
}
}
}
}
@@ -1,62 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.RestAPI;
using System;
using System.Linq;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class BugReport : IRESTMethodGET
{
public void GET(string key, RequestInfo info)
{
long id;
long.TryParse(key, out id);
var tf = FIXMEGlobal.DataConnection.GetTempFiles().FirstOrDefault(x => x.ID == id);
if (tf == null)
{
info.ReportClientError("Invalid or missing bugreport id", System.Net.HttpStatusCode.NotFound);
return;
}
if (!System.IO.File.Exists(tf.Path))
{
info.ReportClientError("File is missing", System.Net.HttpStatusCode.NotFound);
return;
}
var filename = "bugreport.zip";
using(var fs = System.IO.File.OpenRead(tf.Path))
{
info.Response.ContentLength = fs.Length;
info.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", filename));
info.Response.ContentType = "application/octet-stream";
info.BodyWriter.SetOK();
info.Response.SendHeaders();
fs.CopyTo(info.Response.Body);
info.Response.Send();
}
}
}
}
@@ -1,172 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
using System.Collections.Generic;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class Captcha : IRESTMethodGET, IRESTMethodPOST
{
private class CaptchaEntry
{
public readonly string Answer;
public readonly string Target;
public int Attempts;
public readonly DateTime Expires;
public CaptchaEntry(string answer, string target)
{
Answer = answer;
Target = target;
Attempts = 4;
Expires = DateTime.Now.AddMinutes(2);
}
}
private static readonly object m_lock = new object();
private static readonly Dictionary<string, CaptchaEntry> m_captchas = new Dictionary<string, CaptchaEntry>();
public static bool SolvedCaptcha(string token, string target, string answer)
{
lock(m_lock)
{
CaptchaEntry tp;
m_captchas.TryGetValue(token ?? string.Empty, out tp);
if (tp == null)
return false;
if (tp.Attempts > 0)
tp.Attempts--;
return tp.Attempts >= 0 && string.Equals(tp.Answer, answer, StringComparison.OrdinalIgnoreCase) && tp.Target == target && tp.Expires >= DateTime.Now;
}
}
public void GET(string key, RequestInfo info)
{
if (string.IsNullOrWhiteSpace(key))
{
info.ReportClientError("Missing token value", System.Net.HttpStatusCode.Unauthorized);
return;
}
else
{
string answer = null;
lock (m_lock)
{
CaptchaEntry tp;
m_captchas.TryGetValue(key, out tp);
if (tp != null && tp.Expires > DateTime.Now)
answer = tp.Answer;
}
if (string.IsNullOrWhiteSpace(answer))
{
info.ReportClientError("No such entry", System.Net.HttpStatusCode.NotFound);
return;
}
using (var bmp = CaptchaUtil.CreateCaptcha(answer))
using (var ms = new System.IO.MemoryStream())
{
info.Response.ContentType = "image/jpeg";
info.Response.ContentLength = ms.Length;
bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
ms.Position = 0;
info.Response.ContentType = "image/jpeg";
info.Response.ContentLength = ms.Length;
info.Response.SendHeaders();
ms.CopyTo(info.Response.Body);
info.Response.Send();
}
}
}
public void POST(string key, RequestInfo info)
{
if (string.IsNullOrWhiteSpace(key))
{
var target = info.Request.Param["target"].Value;
if (string.IsNullOrWhiteSpace(target))
{
info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest);
return;
}
var answer = CaptchaUtil.CreateRandomAnswer(minlength: 6, maxlength: 6);
var nonce = Guid.NewGuid().ToString();
string token;
using (var ms = new System.IO.MemoryStream())
{
var bytes = System.Text.Encoding.UTF8.GetBytes(answer + nonce);
ms.Write(bytes, 0, bytes.Length);
ms.Position = 0;
using(var hasher = Library.Utility.HashFactory.CreateHasher(Library.Utility.HashFactory.SHA256)){
token = Library.Utility.Utility.Base64PlainToBase64Url(Convert.ToBase64String(hasher.ComputeHash(ms)));
}
}
lock (m_lock)
{
var expired = m_captchas.Where(x => x.Value.Expires < DateTime.Now).Select(x => x.Key).ToArray();
foreach (var x in expired)
m_captchas.Remove(x);
if (m_captchas.Count > 3)
{
info.ReportClientError("Too many captchas, wait 2 minutes and try again", System.Net.HttpStatusCode.ServiceUnavailable);
return;
}
m_captchas[token] = new CaptchaEntry(answer, target);
}
info.OutputOK(new
{
token = token
});
}
else
{
var answer = info.Request.Param["answer"].Value;
var target = info.Request.Param["target"].Value;
if (string.IsNullOrWhiteSpace(answer))
{
info.ReportClientError("Missing answer parameter", System.Net.HttpStatusCode.BadRequest);
return;
}
if (string.IsNullOrWhiteSpace(target))
{
info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest);
return;
}
if (SolvedCaptcha(key, target, answer))
info.OutputOK();
else
info.ReportClientError("Incorrect", System.Net.HttpStatusCode.Forbidden);
}
}
}
}
@@ -1,79 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.RestAPI;
using System;
using System.Collections.Generic;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class Changelog : IRESTMethodGET, IRESTMethodDocumented
{
private class GetResponse
{
public string Status;
public string Version;
public string Changelog;
}
public void GET(string key, RequestInfo info)
{
var fromUpdate = info.Request.QueryString["from-update"].Value;
if (!Library.Utility.Utility.ParseBool(fromUpdate, false))
{
var path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "changelog.txt");
info.OutputOK(new GetResponse() {
Status = "OK",
Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(),
Changelog = System.IO.File.ReadAllText(path)
});
}
else
{
var updateInfo = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion;
if (updateInfo == null)
{
info.ReportClientError("No update found", System.Net.HttpStatusCode.NotFound);
}
else
{
info.OutputOK(new GetResponse() {
Status = "OK",
Version = updateInfo.Version,
Changelog = updateInfo.ChangeInfo
});
}
}
}
public string Description { get { return "Gets the current changelog"; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(GetResponse)),
};
}
}
}
}
@@ -1,293 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.using Duplicati.Library.RestAPI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Duplicati.Library.RestAPI;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class CommandLine : IRESTMethodGET, IRESTMethodPOST
{
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType<CommandLine>();
private class LogWriter : System.IO.TextWriter
{
private readonly ActiveRun m_target;
private readonly StringBuilder m_sb = new StringBuilder();
private int m_newlinechars = 0;
public LogWriter(ActiveRun target)
{
m_target = target;
}
public override Encoding Encoding { get { return System.Text.Encoding.UTF8; } }
public override void Write(char value)
{
lock(m_target.Lock)
{
m_sb.Append(value);
if (NewLine[m_newlinechars] == value)
{
m_newlinechars++;
if (m_newlinechars == NewLine.Length)
WriteLine(string.Empty);
}
else
m_newlinechars = 0;
}
}
public override void WriteLine(string value)
{
value = value ?? string.Empty;
lock(m_target.Lock)
{
m_target.LastAccess = DateTime.Now;
//Avoid writing the log if it does not exist
if (m_target.IsLogDisposed)
{
FIXMEGlobal.LogHandler.WriteMessage(new Library.Logging.LogEntry("Attempted to write message after closing: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", null));
return;
}
try
{
if (m_sb.Length != 0)
{
m_target.Log.Add(m_sb + value);
m_sb.Length = 0;
m_newlinechars = 0;
}
else
{
m_target.Log.Add(value);
}
}
catch (Exception ex)
{
// This can happen on a very unlucky race where IsLogDisposed is set right after the check
FIXMEGlobal.LogHandler.WriteMessage(new Library.Logging.LogEntry("Failed to forward commandline message: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", ex));
}
}
}
}
private class ActiveRun
{
public readonly string ID = Guid.NewGuid().ToString();
public DateTime LastAccess = DateTime.Now;
public readonly Library.Utility.FileBackedStringList Log = new Library.Utility.FileBackedStringList();
public Runner.IRunnerData Task;
public LogWriter Writer;
public readonly object Lock = new object();
public bool Finished = false;
public bool Started = false;
public bool IsLogDisposed = false;
public System.Threading.Thread Thread;
}
private readonly Dictionary<string, ActiveRun> m_activeItems = new Dictionary<string, ActiveRun>();
private System.Threading.Tasks.Task m_cleanupTask;
public void POST(string key, RequestInfo info)
{
if (string.IsNullOrWhiteSpace(key))
{
string[] args;
var str = System.Threading.Tasks.Task.Run(async () =>
{
using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
return await sr.ReadToEndAsync();
}).GetAwaiter().GetResult();
args = Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>(str);
var k = new ActiveRun();
k.Writer = new LogWriter(k);
m_activeItems[k.ID] = k;
StartCleanupTask();
k.Task = Runner.CreateCustomTask((sink) =>
{
try
{
k.Thread = System.Threading.Thread.CurrentThread;
k.Started = true;
var code = Duplicati.CommandLine.Program.RunCommandLine(k.Writer, k.Writer, c => {
k.Task.SetController(c);
c.AppendSink(sink);
}, args);
k.Writer.WriteLine("Return code: {0}", code);
}
catch (Exception ex)
{
var rx = ex;
if (rx is System.Reflection.TargetInvocationException)
rx = rx.InnerException;
if (rx is Library.Interface.UserInformationException)
k.Log.Add(rx.Message);
else
k.Log.Add(rx.ToString());
throw rx;
}
finally
{
k.Finished = true;
k.Thread = null;
}
});
FIXMEGlobal.WorkThread.AddTask(k.Task);
info.OutputOK(new
{
ID = k.ID
});
}
else
{
if (!key.EndsWith("/abort", StringComparison.OrdinalIgnoreCase))
{
info.ReportClientError("Only abort commands are allowed", System.Net.HttpStatusCode.BadRequest);
return;
}
key = key.Substring(0, key.Length - "/abort".Length);
if (string.IsNullOrWhiteSpace(key))
{
info.ReportClientError("No task key found", System.Net.HttpStatusCode.BadRequest);
return;
}
ActiveRun t;
if (!m_activeItems.TryGetValue(key, out t))
{
info.OutputError(code: System.Net.HttpStatusCode.NotFound);
return;
}
var tt = t.Task;
if (tt != null)
tt.Abort();
var tr = t.Thread;
if (tr != null)
tr.Interrupt();
info.OutputOK();
}
}
private void StartCleanupTask()
{
if (m_cleanupTask == null || m_cleanupTask.IsCompleted || m_cleanupTask.IsFaulted || m_cleanupTask.IsCanceled)
m_cleanupTask = RunCleanupAsync();
}
private async System.Threading.Tasks.Task RunCleanupAsync()
{
while (m_activeItems.Count > 0)
{
var oldest = m_activeItems.Values
.OrderBy(x => x.LastAccess)
.FirstOrDefault();
if (oldest != null)
{
// If the task has finished, we just wait a little to allow the UI to pick it up
var timeout = oldest.Finished ? TimeSpan.FromMinutes(5) : TimeSpan.FromDays(1);
if (DateTime.Now - oldest.LastAccess > timeout)
{
oldest.IsLogDisposed = true;
m_activeItems.Remove(oldest.ID);
oldest.Log.Dispose();
// Fix all expired, or stop running
continue;
}
}
await System.Threading.Tasks.Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false);
}
}
public void GET(string key, RequestInfo info)
{
if (string.IsNullOrWhiteSpace(key))
{
info.OutputOK(
Duplicati.CommandLine.Program.SupportedCommands
);
}
else
{
ActiveRun t;
if (!m_activeItems.TryGetValue(key, out t))
{
info.OutputError(code: System.Net.HttpStatusCode.NotFound);
return;
}
int pagesize;
int offset;
int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize);
int.TryParse(info.Request.QueryString["offset"].Value, out offset);
pagesize = Math.Max(10, Math.Min(500, pagesize));
offset = Math.Max(0, offset);
var items = new List<string>();
long count;
bool started;
bool finished;
lock(t.Lock)
{
t.LastAccess = DateTime.Now;
count = t.Log.Count;
offset = Math.Min((int)count, offset);
items.AddRange(t.Log.Skip(offset).Take(pagesize));
finished = t.Finished;
started = t.Started;
}
info.OutputOK(new
{
Pagesize = pagesize,
Offset = offset,
Count = count,
Items = items,
Finished = finished,
Started = started
});
}
}
}
}
@@ -1,277 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Duplicati.Library.Snapshots;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Common;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class Filesystem : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented
{
public void GET(string key, RequestInfo info)
{
var parts = (key ?? "").Split(new char[] { '/' });
var path = Duplicati.Library.Utility.Uri.UrlDecode((parts.Length == 2 ? parts.FirstOrDefault() : key ?? ""));
var command = parts.Length == 2 ? parts.Last() : null;
if (string.IsNullOrEmpty(path))
path = info.Request.QueryString["path"].Value;
Process(command, path, info);
}
private void Process(string command, string path, RequestInfo info)
{
if (string.IsNullOrEmpty(path))
{
info.ReportClientError("No path parameter was found", System.Net.HttpStatusCode.BadRequest);
return;
}
bool skipFiles = Library.Utility.Utility.ParseBool(info.Request.QueryString["onlyfolders"].Value, false);
bool showHidden = Library.Utility.Utility.ParseBool(info.Request.QueryString["showhidden"].Value, false);
string specialpath = null;
string specialtoken = null;
if (path.StartsWith("%", StringComparison.Ordinal))
{
var ix = path.IndexOf("%", 1, StringComparison.Ordinal);
if (ix > 0)
{
var tk = path.Substring(0, ix + 1);
var node = SpecialFolders.Nodes.FirstOrDefault(x => x.id.Equals(tk, StringComparison.OrdinalIgnoreCase));
if (node != null)
{
specialpath = node.resolvedpath;
specialtoken = node.id;
}
}
}
path = SpecialFolders.ExpandEnvironmentVariables(path);
if (Platform.IsClientPosix && !path.StartsWith("/", StringComparison.Ordinal))
{
info.ReportClientError("The path parameter must start with a forward-slash", System.Net.HttpStatusCode.BadRequest);
return;
}
if (!string.IsNullOrWhiteSpace(command))
{
if ("validate".Equals(command, StringComparison.OrdinalIgnoreCase))
{
try
{
if (System.IO.Path.IsPathRooted(path) && (System.IO.Directory.Exists(path) || System.IO.File.Exists(path)))
{
info.OutputOK();
return;
}
}
catch
{
}
info.ReportServerError("File or folder not found", System.Net.HttpStatusCode.NotFound);
return;
}
else
{
info.ReportClientError(string.Format("No such operation found: {0}", command), System.Net.HttpStatusCode.NotFound);
return;
}
}
try
{
if (path != "" && path != "/")
path = Util.AppendDirSeparator(path);
IEnumerable<Serializable.TreeNode> res;
if (!Platform.IsClientPosix && (path.Equals("/") || path.Equals("")))
{
res = DriveInfo.GetDrives()
.Where(di =>
(di.DriveType == DriveType.Fixed || di.DriveType == DriveType.Network || di.DriveType == DriveType.Removable)
&& di.IsReady // Only try to create TreeNode entries for drives who were ready 'now'
)
.Select(TryCreateTreeNodeForDrive) // This will try to create a TreeNode for selected drives
.Where(tn => tn != null); // This filters out such entries that could not be created
}
else
{
res = ListFolderAsNodes(path, skipFiles, showHidden);
}
if ((path.Equals("/") || path.Equals("")) && specialtoken == null)
{
// Prepend special folders
res = SpecialFolders.Nodes.Union(res);
}
if (specialtoken != null)
{
res = res.Select(x => {
x.resolvedpath = x.id;
x.id = specialtoken + x.id.Substring(specialpath.Length);
return x;
});
}
// We have to resolve the query before giving it to OutputOK
// If we do not do this, and the query throws an exception when OutputOK resolves it,
// the exception would not be handled properly
res = res.ToList();
info.OutputOK(res);
}
catch (Exception ex)
{
info.ReportClientError("Failed to process the path: " + ex.Message, System.Net.HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Try to create a new TreeNode instance for the given DriveInfo instance.
///
/// <remarks>
/// If an exception occurs during creation (most likely the device became unavailable), a null is returned instead.
/// </remarks>
/// </summary>
/// <param name="driveInfo">DriveInfo to try create a TreeNode for. Cannot be null.</param>
/// <returns>A new TreeNode instance on success; null if an exception occurred during creation.</returns>
private static Serializable.TreeNode TryCreateTreeNodeForDrive(DriveInfo driveInfo)
{
if (driveInfo == null) throw new ArgumentNullException(nameof(driveInfo));
try
{
// Try to create the TreeNode
// This may still fail as the drive might become unavailable in the meanwhile
return new Serializable.TreeNode
{
id = driveInfo.RootDirectory.FullName,
text =
(
string.IsNullOrWhiteSpace(driveInfo.VolumeLabel)
? driveInfo.RootDirectory.FullName.Replace('\\', ' ')
: driveInfo.VolumeLabel + " - " + driveInfo.RootDirectory.FullName.Replace('\\', ' ')
) + "(" + driveInfo.DriveType + ")",
iconCls = "x-tree-icon-drive"
};
}
catch
{
// Drive became unavailable in the meanwhile or another exception occurred
// Return a null as fall back
return null;
}
}
private static IEnumerable<Serializable.TreeNode> ListFolderAsNodes(string entrypath, bool skipFiles, bool showHidden)
{
//Helper function for finding out if a folder has sub elements
Func<string, bool> hasSubElements = (p) => skipFiles ? Directory.EnumerateDirectories(p).Any() : Directory.EnumerateFileSystemEntries(p).Any();
//Helper function for dealing with exceptions when accessing off-limits folders
Func<string, bool> isEmptyFolder = (p) =>
{
try { return !hasSubElements(p); }
catch { }
return true;
};
//Helper function for dealing with exceptions when accessing off-limits folders
Func<string, bool> canAccess = (p) =>
{
try { hasSubElements(p); return true; }
catch { }
return false;
};
foreach (var s in SystemIO.IO_OS.EnumerateFileSystemEntries(entrypath)
// Group directories first
.OrderByDescending(f => SystemIO.IO_OS.GetFileAttributes(f) & FileAttributes.Directory)
// Sort both groups (directories and files) alphabetically
.ThenBy(f => f))
{
Serializable.TreeNode tn = null;
try
{
var attr = SystemIO.IO_OS.GetFileAttributes(s);
var isSymlink = SystemIO.IO_OS.IsSymlink(s, attr);
var isFolder = (attr & FileAttributes.Directory) != 0;
var isFile = !isFolder;
var isHidden = (attr & FileAttributes.Hidden) != 0;
var accessible = isFile || canAccess(s);
var isLeaf = isFile || !accessible || isEmptyFolder(s);
var rawid = isFolder ? Util.AppendDirSeparator(s) : s;
if (skipFiles && !isFolder)
continue;
if (!showHidden && isHidden)
continue;
tn = new Serializable.TreeNode()
{
id = rawid,
text = SystemIO.IO_OS.PathGetFileName(s),
hidden = isHidden,
symlink = isSymlink,
iconCls = isFolder ? (accessible ? (isSymlink ? "x-tree-icon-symlink" : "x-tree-icon-parent") : "x-tree-icon-locked") : "x-tree-icon-leaf",
leaf = isLeaf
};
}
catch
{
}
if (tn != null)
yield return tn;
}
}
public void POST(string key, RequestInfo info)
{
Process(key, info.Request.Form["path"].Value, info);
}
public string Description { get { return "Enumerates the server filesystem"; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(string[])),
};
}
}
}
}
@@ -1,104 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class Help : IRESTMethodGET
{
public void GET(string key, RequestInfo info)
{
var sb = new StringBuilder();
if (string.IsNullOrWhiteSpace(key))
{
foreach(var m in RESTHandler.Modules.Keys.OrderBy(x => x))
{
var mod = RESTHandler.Modules[m];
if (mod == this)
continue;
var desc = mod.GetType().Name;
if (mod is IRESTMethodDocumented documented)
desc = documented.Description;
sb.AppendFormat(ITEM_TEMPLATE, RESTHandler.API_URI_PATH, m, mod.GetType().Name, desc);
}
var data = Encoding.UTF8.GetBytes(string.Format(TEMPLATE, "API Information", "", sb));
info.Response.ContentType = "text/html";
info.Response.ContentLength = data.Length;
info.Response.Body.Write(data, 0, data.Length);
info.Response.Send();
}
else
{
IRESTMethod m;
RESTHandler.Modules.TryGetValue(key, out m);
if (m == null)
{
info.Response.Status = System.Net.HttpStatusCode.NotFound;
info.Response.Reason = "Module not found";
}
else
{
var desc = "";
if (m is IRESTMethodDocumented doc)
{
desc = doc.Description;
foreach(var t in doc.Types)
sb.AppendFormat(METHOD_TEMPLATE, t.Key, JsonConvert.SerializeObject(t.Value)); //TODO: Format the type
}
var data = Encoding.UTF8.GetBytes(string.Format(TEMPLATE, m.GetType().Name, desc, sb));
info.Response.ContentType = "text/html";
info.Response.ContentLength = data.Length;
info.Response.Body.Write(data, 0, data.Length);
info.Response.Send();
}
}
}
private const string TEMPLATE = @"
<html><head><title>{0}</title></head>
<body>
<h1>{0}</h1>
<p>{1}</p>
<ul>
{2}
</ul>
</body>
";
private const string ITEM_TEMPLATE = @"
<li><a href=""{0}/help/{1}"">{2}</a>: {3}</li>
";
private const string METHOD_TEMPLATE = @"
<b>{0}:</b><br><code>{1}</code><br>
";
}
}
@@ -1,93 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using Duplicati.Library.Interface;
using System.Linq;
using System.Security.Principal;
using Duplicati.Library.Snapshots;
using Duplicati.Library.Common;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class HyperV : IRESTMethodGET, IRESTMethodDocumented
{
public void GET(string key, RequestInfo info)
{
// Early exit in case we are non-windows to prevent attempting to load Windows-only components
if (Platform.IsClientWindows)
RealGET(key, info);
else
info.OutputOK(new string[0]);
}
// Make sure the JIT does not attempt to inline this call and thus load
// referenced types from System.Management here
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
private void RealGET(string key, RequestInfo info)
{
var hypervUtility = new HyperVUtility();
if (!hypervUtility.IsHyperVInstalled || !new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
{
info.OutputOK(new string[0]);
return;
}
try
{
if (string.IsNullOrEmpty(key))
{
hypervUtility.QueryHyperVGuestsInfo();
info.OutputOK(hypervUtility.Guests.Select(x => new { id = x.ID, name = x.Name }).ToList());
}
else
{
hypervUtility.QueryHyperVGuestsInfo(true);
var foundVMs = hypervUtility.Guests.FindAll(x => x.ID.Equals(new Guid(key)));
if (foundVMs.Count == 1)
info.OutputOK(foundVMs[0].DataPaths.Select(x => new { text = x, id = x, cls = "folder", iconCls = "x-tree-icon-leaf", check = "false", leaf = "true" }).ToList());
else
info.ReportClientError(string.Format("Cannot find VM with ID {0}.", key), System.Net.HttpStatusCode.NotFound);
}
}
catch (Exception ex)
{
info.ReportServerError("Failed to enumerate Hyper-V virtual machines: " + ex.Message);
}
}
public string Description { get { return "Return a list of Hyper-V virtual machines"; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(ICommandLineArgument[]))
};
}
}
}
}
@@ -1,60 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
namespace Duplicati.Server.WebServer.RESTMethods
{
public interface IRESTMethod
{
}
public interface IRESTMethodDocumented
{
string Description { get; }
IEnumerable<KeyValuePair<string, Type>> Types { get; }
}
public interface IRESTMethodGET : IRESTMethod
{
void GET(string key, RequestInfo info);
}
public interface IRESTMethodPUT : IRESTMethod
{
void PUT(string key, RequestInfo info);
}
public interface IRESTMethodPOST : IRESTMethod
{
void POST(string key, RequestInfo info);
}
public interface IRESTMethodDELETE : IRESTMethod
{
void DELETE(string key, RequestInfo info);
}
public interface IRESTMethodPATCH : IRESTMethod
{
void PATCH(string key, RequestInfo info);
}
}
@@ -1,44 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.IO;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class Licenses : IRESTMethodGET
{
public void GET(string key, RequestInfo info)
{
var exefolder = System.IO.Path.GetDirectoryName(Duplicati.Library.Utility.Utility.getEntryAssembly().Location);
var path = System.IO.Path.Combine(exefolder, "licenses");
if (Duplicati.Library.Common.Platform.IsClientOSX && !Directory.Exists(path))
{
// Go up one, as the licenses cannot be in the binary folder in MacOS Packages
exefolder = Path.GetDirectoryName(exefolder);
var test = Path.Combine(exefolder, "Licenses");
if (Directory.Exists(test))
path = test;
}
info.OutputOK(Duplicati.License.LicenseReader.ReadLicenses(path));
}
}
}
@@ -1,122 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using Duplicati.Library.RestAPI;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class LogData : IRESTMethodGET, IRESTMethodDocumented
{
public void GET(string key, RequestInfo info)
{
if ("poll".Equals(key, StringComparison.OrdinalIgnoreCase))
{
var input = info.Request.QueryString;
var level_str = input["level"].Value ?? "";
var id_str = input["id"].Value ?? "";
int pagesize;
if (!int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize))
pagesize = 100;
pagesize = Math.Max(1, Math.Min(500, pagesize));
Library.Logging.LogMessageType level;
long id;
long.TryParse(id_str, out id);
Enum.TryParse(level_str, true, out level);
info.OutputOK(FIXMEGlobal.LogHandler.AfterID(id, level, pagesize));
}
else
{
List<Dictionary<string, object>> res = null;
FIXMEGlobal.DataConnection.ExecuteWithCommand(x =>
{
res = DumpTable(x, "ErrorLog", "Timestamp", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value);
});
info.OutputOK(res);
}
}
public static List<Dictionary<string, object>> DumpTable(System.Data.IDbCommand cmd, string tablename, string pagingfield, string offset_str, string pagesize_str)
{
var result = new List<Dictionary<string, object>>();
long pagesize;
if (!long.TryParse(pagesize_str, out pagesize))
pagesize = 100;
pagesize = Math.Max(10, Math.Min(500, pagesize));
cmd.CommandText = "SELECT * FROM \"" + tablename + "\"";
long offset = 0;
if (!string.IsNullOrWhiteSpace(offset_str) && long.TryParse(offset_str, out offset) && !string.IsNullOrEmpty(pagingfield))
{
var p = cmd.CreateParameter();
p.Value = offset;
cmd.Parameters.Add(p);
cmd.CommandText += " WHERE \"" + pagingfield + "\" < ?";
}
if (!string.IsNullOrEmpty(pagingfield))
cmd.CommandText += " ORDER BY \"" + pagingfield + "\" DESC";
cmd.CommandText += " LIMIT " + pagesize.ToString();
using(var rd = cmd.ExecuteReader())
{
var names = new List<string>();
for(var i = 0; i < rd.FieldCount; i++)
names.Add(rd.GetName(i));
while (rd.Read())
{
var dict = new Dictionary<string, object>();
for(int i = 0; i < names.Count; i++)
dict[names[i]] = rd.GetValue(i);
result.Add(dict);
}
}
return result;
}
public string Description { get { return "Retrieves system log data"; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(Dictionary<string, string>[])),
};
}
}
}
}
@@ -1,91 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using Duplicati.Library.Interface;
using System.Linq;
using System.Security.Principal;
using Duplicati.Library.Snapshots;
using Duplicati.Library.Common;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class MSSQL : IRESTMethodGET, IRESTMethodDocumented
{
public void GET(string key, RequestInfo info)
{
// Early exit in case we are non-windows to prevent attempting to load Windows-only components
if (Platform.IsClientWindows)
RealGET(key, info);
else
info.OutputOK(new string[0]);
}
// Make sure the JIT does not attempt to inline this call and thus load
// referenced types from System.Management here
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
private void RealGET(string key, RequestInfo info)
{
var mssqlUtility = new MSSQLUtility();
if (!mssqlUtility.IsMSSQLInstalled || !new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
{
info.OutputOK(new string[0]);
return;
}
try
{
mssqlUtility.QueryDBsInfo();
if (string.IsNullOrEmpty(key))
info.OutputOK(mssqlUtility.DBs.Select(x => new { id = x.ID, name = x.Name }).ToList());
else
{
var foundDBs = mssqlUtility.DBs.FindAll(x => x.ID.Equals(key, StringComparison.OrdinalIgnoreCase));
if (foundDBs.Count == 1)
info.OutputOK(foundDBs[0].DataPaths.Select(x => new { text = x, id = x, cls = "folder", iconCls = "x-tree-icon-leaf", check = "false", leaf = "true" }).ToList());
else
info.ReportClientError(string.Format("Cannot find DB with ID {0}.", key), System.Net.HttpStatusCode.NotFound);
}
}
catch (Exception ex)
{
info.ReportServerError("Failed to enumerate Microsoft SQL Server databases: " + ex.Message);
}
}
public string Description { get { return "Return a list of Microsoft SQL Server databases"; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(ICommandLineArgument[]))
};
}
}
}
}
@@ -1,65 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.RestAPI;
using System;
using System.Linq;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class Notification : IRESTMethodGET, IRESTMethodDELETE
{
public void GET(string key, RequestInfo info)
{
long id;
if (!long.TryParse(key, out id))
{
info.ReportClientError("Invalid ID", System.Net.HttpStatusCode.BadRequest);
return;
}
var el = FIXMEGlobal.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id);
if (el == null)
info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound);
else
info.OutputOK(el);
}
public void DELETE(string key, RequestInfo info)
{
long id;
if (!long.TryParse(key, out id))
{
info.ReportClientError("Invalid ID", System.Net.HttpStatusCode.BadRequest);
return;
}
var el = FIXMEGlobal.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id);
if (el == null)
info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound);
else
{
FIXMEGlobal.DataConnection.DismissNotification(id);
info.OutputOK();
}
}
}
}
@@ -1,50 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.RestAPI;
using System;
using System.Collections.Generic;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class ProgressState : IRESTMethodGET, IRESTMethodDocumented
{
public void GET(string key, RequestInfo info)
{
if (FIXMEGlobal.GenerateProgressState == null)
info.ReportClientError("No active backup", System.Net.HttpStatusCode.NotFound);
else
info.OutputOK(FIXMEGlobal.GenerateProgressState());
}
public string Description { get { return "Return the progress of the currently running operation."; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(Serialization.Interface.IProgressEventData))
};
}
}
}
}
@@ -1,254 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Duplicati.Library.Interface;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class RemoteOperation : IRESTMethodGET, IRESTMethodPOST
{
private void LocateDbUri(string uri, RequestInfo info)
{
var path = Library.Main.DatabaseLocator.GetDatabasePath(uri, null, false, false);
info.OutputOK(new {
Exists = !string.IsNullOrWhiteSpace(path),
Path = path
});
}
private Dictionary<string, string> ParseUrlOptions(Library.Utility.Uri uri)
{
var qp = uri.QueryParameters;
var opts = Runner.GetCommonOptions();
foreach (var k in qp.Keys.Cast<string>())
opts[k] = qp[k];
return opts;
}
private IEnumerable<IGenericModule> ConfigureModules(IDictionary<string, string> opts)
{
// TODO: This works because the generic modules are implemented
// with pre .NetCore logic, using static methods
// The modules are created to allow multipe dispose,
// which violates the .Net patterns
var modules = (from n in Library.DynamicLoader.GenericLoader.Modules
where n is Library.Interface.IConnectionModule
select n).ToArray();
foreach (var n in modules)
n.Configure(opts);
return modules;
}
private class TupleDisposeWrapper : IDisposable
{
public IBackend Backend { get; set; }
public IEnumerable<IGenericModule> Modules { get; set; }
public void Dispose()
{
Backend.Dispose();
DisposeModules();
}
public void DisposeModules()
{
foreach (var n in Modules)
if (n is IDisposable disposable)
disposable.Dispose();
}
}
private TupleDisposeWrapper GetBackend(string url)
{
var uri = new Library.Utility.Uri(url);
var opts = ParseUrlOptions(uri);
var modules = ConfigureModules(opts);
var backend = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(url, new Dictionary<string, string>());
return new TupleDisposeWrapper() { Backend = backend, Modules = modules };
}
private void CreateFolder(string uri, RequestInfo info)
{
using(var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary<string, string>()))
b.CreateFolder();
info.OutputOK();
}
private void UploadFile(string uri, RequestInfo info)
{
var data = info.Request.QueryString["data"].Value;
var remotename = info.Request.QueryString["filename"].Value;
using(var ms = new System.IO.MemoryStream())
using(var b = GetBackend(uri))
{
using(var tf = new Library.Utility.TempFile())
{
System.IO.File.WriteAllText(tf, data);
b.Backend.PutAsync(remotename, tf, CancellationToken.None).Wait();
}
}
info.OutputOK();
}
private void ListFolder(string uri, RequestInfo info)
{
using(var b = GetBackend(uri))
info.OutputOK(b.Backend.List());
}
private void TestConnection(string url, RequestInfo info)
{
bool autoCreate = info.Request.Param.Contains("autocreate")
? Library.Utility.Utility.ParseBool(info.Request.Param["autocreate"].Value, false)
: false;
TupleDisposeWrapper wrapper = null;
try
{
wrapper = GetBackend(url);
using (var b = wrapper.Backend)
{
try { b.Test(); }
catch (FolderMissingException)
{
if (!autoCreate)
throw;
b.CreateFolder();
b.Test();
}
info.OutputOK();
}
}
catch (Duplicati.Library.Interface.FolderMissingException)
{
if (!autoCreate) {
info.ReportServerError("missing-folder");
} else {
info.ReportServerError("error-creating-folder");
}
}
catch (Duplicati.Library.Utility.SslCertificateValidator.InvalidCertificateException icex)
{
if (string.IsNullOrWhiteSpace(icex.Certificate))
info.ReportServerError(icex.Message);
else
info.ReportServerError("incorrect-cert:" + icex.Certificate);
}
catch (Duplicati.Library.Utility.HostKeyException hex)
{
if (string.IsNullOrWhiteSpace(hex.ReportedHostKey))
info.ReportServerError(hex.Message);
else
{
info.ReportServerError(string.Format(
@"incorrect-host-key:""{0}"", accepted-host-key:""{1}""",
hex.ReportedHostKey,
hex.AcceptedHostKey
));
}
}
finally
{
if (wrapper != null)
wrapper.DisposeModules();
}
}
public void GET(string key, RequestInfo info)
{
var parts = (key ?? "").Split(new char[] { '/' }, 2);
if (parts.Length <= 1)
{
info.ReportClientError("No url or operation supplied", System.Net.HttpStatusCode.BadRequest);
return;
}
var url = Library.Utility.Uri.UrlDecode(parts.First());
var operation = parts.Last().ToLowerInvariant();
switch (operation)
{
case "dbpath":
LocateDbUri(url, info);
return;
case "list":
ListFolder(url, info);
return;
case "create":
CreateFolder(url, info);
return;
case "test":
TestConnection(url, info);
return;
default:
info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest);
return;
}
}
public void POST(string key, RequestInfo info)
{
string url;
using(var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
url = sr.ReadToEnd();
switch (key)
{
case "dbpath":
LocateDbUri(url, info);
return;
case "list":
ListFolder(url, info);
return;
case "create":
CreateFolder(url, info);
return;
case "put":
UploadFile(url, info);
return;
case "test":
TestConnection(url, info);
return;
default:
info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest);
return;
}
}
}
}
@@ -1,117 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class RequestInfo : IDisposable
{
public HttpServer.IHttpRequest Request { get; private set; }
public HttpServer.IHttpResponse Response { get; private set; }
public HttpServer.Sessions.IHttpSession Session { get; private set; }
public BodyWriter BodyWriter { get; private set; }
public RequestInfo(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
{
Request = request;
Response = response;
Session = session;
BodyWriter = new BodyWriter(response, request);
}
public void ReportServerError(string message, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.InternalServerError)
{
Response.Status = code;
Response.Reason = message;
BodyWriter.WriteJsonObject(new { Error = message });
}
public void ReportClientError(string message, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.BadRequest)
{
ReportServerError(message, code);
}
public bool LongPollCheck(EventPollNotify poller, ref long id, out bool isError)
{
HttpServer.HttpInput input = String.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? Request.Form : Request.QueryString;
if (Library.Utility.Utility.ParseBool(input["longpoll"].Value, false))
{
long lastEventId;
if (!long.TryParse(input["lasteventid"].Value, out lastEventId))
{
ReportClientError("When activating long poll, the request must include the last event id", System.Net.HttpStatusCode.BadRequest);
isError = true;
return false;
}
TimeSpan ts;
try { ts = Library.Utility.Timeparser.ParseTimeSpan(input["duration"].Value); }
catch (Exception ex)
{
ReportClientError("Invalid duration: " + ex.Message, System.Net.HttpStatusCode.BadRequest);
isError = true;
return false;
}
if (ts <= TimeSpan.FromSeconds(10) || ts.TotalMilliseconds > int.MaxValue)
{
ReportClientError("Invalid duration, must be at least 10 seconds, and less than " + int.MaxValue + " milliseconds", System.Net.HttpStatusCode.BadRequest);
isError = true;
return false;
}
isError = false;
id = poller.Wait(lastEventId, (int)ts.TotalMilliseconds);
return true;
}
isError = false;
return false;
}
public void OutputOK(object item = null)
{
BodyWriter.OutputOK(item);
}
public void OutputError(object item = null, System.Net.HttpStatusCode code = System.Net.HttpStatusCode.InternalServerError, string reason = null)
{
Response.Status = code;
Response.Reason = reason ?? "Error";
if(item == null && reason != null)
{
item = new { Error = reason };
}
BodyWriter.WriteJsonObject(item);
}
public void Dispose()
{
if (BodyWriter != null)
{
var bw = BodyWriter;
BodyWriter = null;
bw.Dispose();
}
}
}
}
@@ -1,116 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.RestAPI;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class ServerSetting : IRESTMethodGET, IRESTMethodPUT, IRESTMethodDocumented
{
public void GET(string key, RequestInfo info)
{
if (string.IsNullOrWhiteSpace(key))
{
info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Key is missing");
return;
}
if (key.Equals("server-ssl-certificate", StringComparison.OrdinalIgnoreCase) || key.Equals("ServerSSLCertificate", StringComparison.OrdinalIgnoreCase))
{
info.OutputOK(FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate == null ? "False" : "True");
return;
}
if (key.StartsWith("--", StringComparison.Ordinal))
{
var prop = FIXMEGlobal.DataConnection.Settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase));
info.OutputOK(prop == null ? null : prop.Value);
}
else
{
var prop = typeof(Database.ServerSettings).GetProperty(key);
if (prop == null)
info.OutputError(null, System.Net.HttpStatusCode.NotFound, "Not found");
else
info.OutputOK(prop.GetValue(FIXMEGlobal.DataConnection.ApplicationSettings));
}
}
public void PUT(string key, RequestInfo info)
{
if (string.IsNullOrWhiteSpace(key))
{
info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Key is missing");
return;
}
if (key.Equals("server-ssl-certificate", StringComparison.OrdinalIgnoreCase) || key.Equals("ServerSSLCertificate", StringComparison.OrdinalIgnoreCase))
{
info.OutputError(null, System.Net.HttpStatusCode.BadRequest, "Can only update SSL certificate from commandline");
return;
}
if (key.StartsWith("--", StringComparison.Ordinal))
{
var settings = FIXMEGlobal.DataConnection.Settings.ToList();
var prop = settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase));
if (prop == null)
settings.Add(prop = new Database.Setting() { Name = key, Value = info.Request.Form["data"].Value });
else
prop.Value = info.Request.Form["data"].Value;
FIXMEGlobal.DataConnection.Settings = settings.ToArray();
info.OutputOK(prop == null ? null : prop.Value);
}
else
{
var prop = typeof(Database.ServerSettings).GetProperty(key);
if (prop == null)
info.OutputError(null, System.Net.HttpStatusCode.NotFound, "Not found");
else
{
var dict = new Dictionary<string, string>();
dict[key] = info.Request.Form["data"].Value;
FIXMEGlobal.DataConnection.ApplicationSettings.UpdateSettings(dict, false);
info.OutputOK();
}
}
}
public string Description { get { return "Return a list of settings for the server"; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(string)),
new KeyValuePair<string, Type>(HttpServer.Method.Put, typeof(string))
};
}
}
}
}
@@ -1,136 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
using System.Collections.Generic;
using Duplicati.Server.Database;
using System.IO;
using Duplicati.Server.Serialization;
using Duplicati.Library.RestAPI;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class ServerSettings : IRESTMethodGET, IRESTMethodPATCH, IRESTMethodDocumented
{
public void GET(string key, RequestInfo info)
{
// Join server settings and global settings
var adv_props =
FIXMEGlobal.DataConnection.GetSettings(Database.Connection.SERVER_SETTINGS_ID)
.Where(x => !string.IsNullOrWhiteSpace(x.Name))
.Union(
FIXMEGlobal.DataConnection.Settings
.Where(x => !string.IsNullOrWhiteSpace(x.Name) && x.Name.StartsWith("--", StringComparison.Ordinal))
);
var dict = new Dictionary<string, string>();
foreach (var n in adv_props)
dict[n.Name] = n.Value;
string sslcert;
dict.TryGetValue("server-ssl-certificate", out sslcert);
dict["server-ssl-certificate"] = (!string.IsNullOrWhiteSpace(sslcert)).ToString();
info.OutputOK(dict);
}
public void PATCH(string key, RequestInfo info)
{
string str = info.Request.Form["data"].Value;
if (string.IsNullOrWhiteSpace(str))
str = System.Threading.Tasks.Task.Run(async () =>
{
using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
return await sr.ReadToEndAsync();
}).GetAwaiter().GetResult();
if (string.IsNullOrWhiteSpace(str))
{
info.ReportClientError("Missing data object", System.Net.HttpStatusCode.BadRequest);
return;
}
Dictionary<string, string> data = null;
try
{
data = Serializer.Deserialize<Dictionary<string, string>>(new StringReader(str));
if (data == null)
{
info.ReportClientError("Data object had no entry", System.Net.HttpStatusCode.BadRequest);
return;
}
// Split into server settings and global settings
var serversettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key)).ToDictionary(x => x.Key, x => x.Key.StartsWith("--", StringComparison.Ordinal) ? null : x.Value);
var globalsettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Key.StartsWith("--", StringComparison.Ordinal));
serversettings.Remove("server-ssl-certificate");
serversettings.Remove("ServerSSLCertificate");
if (serversettings.Any())
FIXMEGlobal.DataConnection.ApplicationSettings.UpdateSettings(serversettings, false);
if (globalsettings.Any())
{
// Update based on inputs
var existing = FIXMEGlobal.DataConnection.Settings.ToDictionary(x => x.Name, x => x);
foreach (var g in globalsettings)
if (g.Value == null)
existing.Remove(g.Key);
else
{
if (existing.ContainsKey(g.Key))
existing[g.Key].Value = g.Value;
else
existing[g.Key] = new Setting() { Name = g.Key, Value = g.Value };
}
FIXMEGlobal.DataConnection.Settings = existing.Select(x => x.Value).ToArray();
}
info.OutputOK();
}
catch (Exception ex)
{
if (data == null)
info.ReportClientError(string.Format("Unable to parse data object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
else
info.ReportClientError(string.Format("Unable to save settings: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError);
}
}
public string Description { get { return "Return a list of settings for the server"; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(Database.ServerSettings))
};
}
}
}
}
@@ -1,104 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.RestAPI;
using System;
using System.Collections.Generic;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class ServerState : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented
{
public void GET(string key, RequestInfo info)
{
bool isError;
long id = 0;
long.TryParse(key, out id);
if (info.LongPollCheck(FIXMEGlobal.StatusEventNotifyer, ref id, out isError))
{
//Make sure we do not report a higher number than the eventnotifier says
var st = new Serializable.ServerStatus();
st.LastEventID = id;
info.OutputOK(st);
}
else if (!isError)
{
info.OutputOK(new Serializable.ServerStatus());
}
}
public void POST(string key, RequestInfo info)
{
var input = info.Request.Form;
switch ((key ?? "").ToLowerInvariant())
{
case "pause":
if (input.Contains("duration") && !string.IsNullOrWhiteSpace(input["duration"].Value))
{
TimeSpan ts;
try
{
ts = Library.Utility.Timeparser.ParseTimeSpan(input["duration"].Value);
}
catch (Exception ex)
{
info.ReportClientError(ex.Message, System.Net.HttpStatusCode.BadRequest);
return;
}
if (ts.TotalMilliseconds > 0)
FIXMEGlobal.LiveControl.Pause(ts);
else
FIXMEGlobal.LiveControl.Pause();
}
else
{
FIXMEGlobal.LiveControl.Pause();
}
info.OutputOK();
return;
case "resume":
FIXMEGlobal.LiveControl.Resume();
info.OutputOK();
return;
default:
info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound);
return;
}
}
public string Description { get { return "Return the state of the server. This method can be long-polled."; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(Serializable.ServerStatus)),
new KeyValuePair<string, Type>(HttpServer.Method.Post, typeof(Serializable.ServerStatus))
};
}
}
}
}
@@ -1,109 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
using System.Collections.Generic;
using Duplicati.Library.Interface;
using Duplicati.Library.Common;
using Duplicati.Library.RestAPI;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class SystemInfo : IRESTMethodGET, IRESTMethodDocumented
{
public string Description { get { return "Gets various system properties"; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, SystemData(null).GetType())
};
}
}
public void GET(string key, RequestInfo info)
{
info.BodyWriter.OutputOK(SystemData(info));
}
private static object SystemData(RequestInfo info)
{
var browserlanguage = RESTHandler.ParseDefaultRequestCulture(info) ?? System.Globalization.CultureInfo.InvariantCulture;
return new
{
APIVersion = 1,
PasswordPlaceholder = Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER,
ServerVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(),
ServerVersionName = Duplicati.License.VersionNumbers.Version,
ServerVersionType = Duplicati.Library.AutoUpdater.UpdaterManager.SelfVersion.ReleaseType,
StartedBy = FIXMEGlobal.Origin,
DefaultUpdateChannel = Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel,
DefaultUsageReportLevel = Duplicati.Library.UsageReporter.Reporter.DefaultReportLevel,
ServerTime = DateTime.Now,
OSType = Platform.IsClientPosix ? (Platform.IsClientOSX ? "OSX" : "Linux") : "Windows",
DirectorySeparator = System.IO.Path.DirectorySeparatorChar,
PathSeparator = System.IO.Path.PathSeparator,
CaseSensitiveFilesystem = Duplicati.Library.Utility.Utility.IsFSCaseSensitive,
MachineName = System.Environment.MachineName,
PackageTypeId = Duplicati.Library.AutoUpdater.UpdaterManager.PackageTypeId,
UserName = System.Environment.UserName,
NewLine = System.Environment.NewLine,
CLRVersion = System.Environment.Version.ToString(),
CLROSInfo = new
{
Platform = System.Environment.OSVersion.Platform.ToString(),
ServicePack = System.Environment.OSVersion.ServicePack,
Version = System.Environment.OSVersion.Version.ToString(),
VersionString = System.Environment.OSVersion.VersionString
},
Options = Serializable.ServerSettings.Options,
CompressionModules = Serializable.ServerSettings.CompressionModules,
EncryptionModules = Serializable.ServerSettings.EncryptionModules,
BackendModules = Serializable.ServerSettings.BackendModules,
GenericModules = Serializable.ServerSettings.GenericModules,
WebModules = Serializable.ServerSettings.WebModules,
ConnectionModules = Serializable.ServerSettings.ConnectionModules,
ServerModules = Serializable.ServerSettings.ServerModules,
UsingAlternateUpdateURLs = Duplicati.Library.AutoUpdater.AutoUpdateSettings.UsesAlternateURLs,
LogLevels = Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType)),
SpecialFolders = from n in SpecialFolders.Nodes select new { ID = n.id, Path = n.resolvedpath },
BrowserLocale = new
{
Code = browserlanguage.Name,
EnglishName = browserlanguage.EnglishName,
DisplayName = browserlanguage.NativeName
},
SupportedLocales =
Library.Localization.LocalizationService.SupportedCultures
.Select(x => new {
Code = x,
EnglishName = new System.Globalization.CultureInfo(x).EnglishName,
DisplayName = new System.Globalization.CultureInfo(x).NativeName
}
),
BrowserLocaleSupported = Library.Localization.LocalizationService.isCultureSupported(browserlanguage)
};
}
}
}
@@ -1,48 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using Duplicati.Library.Interface;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class SystemWideSettings : IRESTMethodGET, IRESTMethodDocumented
{
public void GET(string key, RequestInfo info)
{
info.OutputOK(new Duplicati.Library.Main.Options(new Dictionary<string, string>()).SupportedCommands);
}
public string Description { get { return "Return a list of settings that can be applied to all backups on a system-wide basis"; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(ICommandLineArgument[]))
};
}
}
}
}
@@ -1,59 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.RestAPI;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class Tags : IRESTMethodGET, IRESTMethodDocumented
{
public void GET(string key, RequestInfo info)
{
var r =
from n in
Serializable.ServerSettings.CompressionModules
.Union(Serializable.ServerSettings.EncryptionModules)
.Union(Serializable.ServerSettings.BackendModules)
.Union(Serializable.ServerSettings.GenericModules)
select n.Key.ToLower(CultureInfo.InvariantCulture);
// Append all known tags
r = r.Union(from n in FIXMEGlobal.DataConnection.Backups select n.Tags into p from x in p select x.ToLower(CultureInfo.InvariantCulture));
info.OutputOK(r);
}
public string Description { get { return "Gets the list of tags"; } }
public IEnumerable<KeyValuePair<string, Type>> Types
{
get
{
return new KeyValuePair<string, Type>[] {
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(string[])),
};
}
}
}
}
@@ -1,113 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
using System.Collections.Generic;
using Duplicati.Library.RestAPI;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class Task : IRESTMethodGET, IRESTMethodPOST
{
public void GET(string key, RequestInfo info)
{
var parts = (key ?? "").Split(new char[] { '/' }, 2);
long taskid;
if (long.TryParse(parts.FirstOrDefault(), out taskid))
{
var task = FIXMEGlobal.WorkThread.CurrentTask;
var tasks = FIXMEGlobal.WorkThread.CurrentTasks;
if (task != null && task.TaskID == taskid)
{
info.OutputOK(new { Status = "Running" });
return;
}
if (tasks.FirstOrDefault(x => x.TaskID == taskid) == null)
{
KeyValuePair<long, Exception>[] matches;
lock(FIXMEGlobal.MainLock)
matches = FIXMEGlobal.TaskResultCache.Where(x => x.Key == taskid).ToArray();
if (matches.Length == 0)
info.ReportClientError("No such task found", System.Net.HttpStatusCode.NotFound);
else
info.OutputOK(new {
Status = matches[0].Value == null ? "Completed" : "Failed",
ErrorMessage = matches[0].Value == null ? null : matches[0].Value.Message,
Exception = matches[0].Value == null ? null : matches[0].Value.ToString()
});
}
else
{
info.OutputOK(new { Status = "Waiting" });
}
}
else
{
info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest);
}
}
public void POST(string key, RequestInfo info)
{
var parts = (key ?? "").Split(new char[] { '/' }, 2);
long taskid;
if (parts.Length == 2 && long.TryParse(parts.First(), out taskid))
{
var task = FIXMEGlobal.WorkThread.CurrentTask;
var tasks = FIXMEGlobal.WorkThread.CurrentTasks;
if (task != null)
tasks.Insert(0, task);
task = tasks.FirstOrDefault(x => x.TaskID == taskid);
if (task == null)
{
info.ReportClientError("No such task", System.Net.HttpStatusCode.NotFound);
return;
}
switch (parts.Last().ToLowerInvariant())
{
case "stopaftercurrentfile":
task.Stop(allowCurrentFileToFinish: true);
info.OutputOK();
return;
case "stopnow":
task.Stop(allowCurrentFileToFinish: false);
info.OutputOK();
return;
case "abort":
task.Abort();
info.OutputOK();
return;
}
}
info.ReportClientError("Invalid or missing task id", System.Net.HttpStatusCode.NotFound);
}
}
}
@@ -1,82 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using Duplicati.Server.Serialization;
using System.IO;
using Duplicati.Library.RestAPI;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class UISettings : IRESTMethodGET, IRESTMethodPOST, IRESTMethodPATCH
{
public void GET(string key, RequestInfo info)
{
if (string.IsNullOrWhiteSpace(key))
{
info.OutputOK(FIXMEGlobal.DataConnection.GetUISettingsSchemes());
}
else
{
info.OutputOK(FIXMEGlobal.DataConnection.GetUISettings(key));
}
}
public void POST(string key, RequestInfo info)
{
PATCH(key, info);
}
public void PATCH(string key, RequestInfo info)
{
if (string.IsNullOrWhiteSpace(key))
{
info.ReportClientError("Scheme is missing", System.Net.HttpStatusCode.BadRequest);
return;
}
IDictionary<string, string> data;
try
{
data = Serializer.Deserialize<Dictionary<string, string>>(new StreamReader(info.Request.Body));
}
catch (Exception ex)
{
info.ReportClientError(string.Format("Unable to parse settings object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
return;
}
if (data == null)
{
info.ReportClientError("Unable to parse settings object", System.Net.HttpStatusCode.BadRequest);
return;
}
if (info.Request.Method == "POST")
FIXMEGlobal.DataConnection.SetUISettings(key, data);
else
FIXMEGlobal.DataConnection.UpdateUISettings(key, data);
info.OutputOK();
}
}
}
@@ -1,45 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class WebModule : IRESTMethodPOST
{
public void POST(string key, RequestInfo info)
{
var m = Duplicati.Library.DynamicLoader.WebLoader.Modules.FirstOrDefault(x => x.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
if (m == null)
{
info.ReportClientError(string.Format("No such command {0}", key), System.Net.HttpStatusCode.NotFound);
return;
}
info.OutputOK(new {
Status = "OK",
Result = m.Execute(info.Request.Form.Where(x => !x.Name.Equals("command", StringComparison.OrdinalIgnoreCase)
).ToDictionary(x => x.Name, x => x.Value))
});
}
}
}
+9 -3
View File
@@ -690,7 +690,7 @@ namespace Duplicati.Server
if (ex is UserInformationException exception)
messageid = exception.HelpID;
FIXMEGlobal.IncrementLastDataUpdateID();
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
FIXMEGlobal.DataConnection.RegisterNotification(
NotificationType.Error,
backup.IsTemporary ?
@@ -856,6 +856,12 @@ namespace Duplicati.Server
? string.Format("Got {0} warning(s)", result.Warnings.Count())
: string.Format("Got {0} error(s)", result.Errors.Count());
// If there is only one error or warning, show the message
if (result.ParsedResult == ParsedResultType.Warning && result.Warnings.Count() == 1)
message = $"Warning: {result.Warnings.Single()}";
else if (result.ParsedResult == ParsedResultType.Error && result.Errors.Count() == 1)
message = $"Error: {result.Errors.Single()}";
FIXMEGlobal.DataConnection.RegisterNotification(
type,
title,
@@ -873,7 +879,7 @@ namespace Duplicati.Server
if (!backup.IsTemporary)
FIXMEGlobal.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null);
FIXMEGlobal.IncrementLastDataUpdateID();
FIXMEGlobal.NotificationUpdateService.IncrementLastDataUpdateId();
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
}
@@ -942,7 +948,7 @@ namespace Duplicati.Server
return filter;
}
internal static Dictionary<string, string> GetCommonOptions()
public static Dictionary<string, string> GetCommonOptions()
{
return
(from n in FIXMEGlobal.DataConnection.Settings
+107 -68
View File
@@ -1,25 +1,26 @@
#region Disclaimer / License
// Copyright (C) 2015, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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.
// 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:
//
// 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Duplicati.Server.Serialization.Interface;
#endregion
using System;
using System.Collections.Generic;
using System.Text;
@@ -28,6 +29,9 @@ using System.Threading;
using Duplicati.Library.Utility;
using Duplicati.Library.RestAPI;
// TODO: Rewrite this class.
// It should just signal what new backups to run, and not mix with the worker thread.
namespace Duplicati.Server
{
/// <summary>
@@ -40,19 +44,23 @@ namespace Duplicati.Server
/// <summary>
/// The thread that runs the scheduler
/// </summary>
private readonly Thread m_thread;
private Thread m_thread;
/// <summary>
/// A termination flag
/// </summary>
private volatile bool m_terminate;
/// <summary>
/// The worker thread that is invoked to do work
/// </summary>
private readonly WorkerThread<Runner.IRunnerData> m_worker;
private WorkerThread<Runner.IRunnerData> m_worker;
/// <summary>
/// The wait event
/// </summary>
private readonly AutoResetEvent m_event;
private AutoResetEvent m_event;
/// <summary>
/// The data synchronization lock
/// </summary>
@@ -67,17 +75,24 @@ namespace Duplicati.Server
/// The currently scheduled items
/// </summary>
private KeyValuePair<DateTime, ISchedule>[] m_schedule;
/// <summary>
/// List of update tasks, used to set the timestamp on the schedule once completed
/// </summary>
private readonly Dictionary<Server.Runner.IRunnerData, Tuple<ISchedule, DateTime, DateTime>> m_updateTasks;
private Dictionary<Server.Runner.IRunnerData, Tuple<ISchedule, DateTime, DateTime>> m_updateTasks;
/// <summary>
/// Constructs a new scheduler
/// </summary>
public Scheduler()
{
}
/// <summary>
/// Initializes scheduler
/// </summary>
/// <param name="worker">The worker thread</param>
public Scheduler(WorkerThread<Server.Runner.IRunnerData> worker)
public void Init(WorkerThread<Runner.IRunnerData> worker)
{
m_thread = new Thread(new ThreadStart(Runner));
m_worker = worker;
@@ -92,6 +107,13 @@ namespace Duplicati.Server
m_thread.Start();
}
public IList<Tuple<long, string>> GetSchedulerQueueIds()
{
return (from n in WorkerQueue
where n.Backup != null
select new Tuple<long, string>(n.TaskID, n.Backup.ID)).ToList();
}
/// <summary>
/// Forces the scheduler to re-evaluate the order.
/// Call this method if something changes
@@ -104,13 +126,13 @@ namespace Duplicati.Server
/// <summary>
/// A snapshot copy of the current schedule list
/// </summary>
public List<KeyValuePair<DateTime, ISchedule>> Schedule
{
get
public List<KeyValuePair<DateTime, ISchedule>> Schedule
{
get
{
lock (m_lock)
return m_schedule.ToList();
}
}
}
/// <summary>
@@ -118,10 +140,7 @@ namespace Duplicati.Server
/// </summary>
public List<Runner.IRunnerData> WorkerQueue
{
get
{
return (from t in m_worker.CurrentTasks where t != null select t).ToList();
}
get { return (from t in m_worker.CurrentTasks where t != null select t).ToList(); }
}
/// <summary>
@@ -135,8 +154,13 @@ namespace Duplicati.Server
if (wait)
{
try { m_thread.Join(); }
catch { }
try
{
m_thread.Join();
}
catch
{
}
}
}
@@ -148,7 +172,8 @@ namespace Duplicati.Server
/// <param name="repetition">The repetition interval</param>
/// <param name="allowedDays">The days the backup is allowed to run</param>
/// <returns>The next valid date, or throws an exception if no such date can be found</returns>
public static DateTime GetNextValidTime(DateTime basetime, DateTime firstdate, string repetition, DayOfWeek[] allowedDays)
public static DateTime GetNextValidTime(DateTime basetime, DateTime firstdate, string repetition,
DayOfWeek[] allowedDays)
{
var res = basetime;
@@ -195,26 +220,25 @@ namespace Duplicati.Server
throw new Exception(Strings.Scheduler.InvalidTimeSetupError(basetime, repetition, sb.ToString()));
}
return res;
}
private void OnCompleted(WorkerThread<Runner.IRunnerData> worker, Runner.IRunnerData task)
{
Tuple<ISchedule, DateTime, DateTime> t = null;
lock(m_lock)
lock (m_lock)
{
if (task != null && m_updateTasks.TryGetValue(task, out t))
m_updateTasks.Remove(task);
}
if (t != null)
{
t.Item1.Time = t.Item2;
t.Item1.LastRun = t.Item3;
FIXMEGlobal.DataConnection.AddOrUpdateSchedule(t.Item1);
}
}
private void OnStartingWork(WorkerThread<Runner.IRunnerData> worker, Runner.IRunnerData task)
@@ -223,8 +247,8 @@ namespace Duplicati.Server
{
return;
}
lock(m_lock)
lock (m_lock)
{
if (m_updateTasks.TryGetValue(task, out Tuple<ISchedule, DateTime, DateTime> scheduleInfo))
{
@@ -245,10 +269,10 @@ namespace Duplicati.Server
{
//TODO: As this is executed repeatedly we should cache it
// to avoid frequent db lookups
//Determine schedule list
var lst = FIXMEGlobal.DataConnection.Schedules;
foreach(var sc in lst)
foreach (var sc in lst)
{
if (!string.IsNullOrEmpty(sc.Repeat))
{
@@ -267,7 +291,7 @@ namespace Duplicati.Server
{
start = startkey.Value;
}
try
{
// Recover from timedrift issues by overriding the dates if the last run date is in the future.
@@ -276,11 +300,13 @@ namespace Duplicati.Server
start = DateTime.UtcNow;
last = DateTime.UtcNow;
}
start = GetNextValidTime(start, last, sc.Repeat, sc.AllowedDays);
}
catch (Exception ex)
{
FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex);
FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date",
ex);
}
//If time is exceeded, run it now
@@ -288,15 +314,17 @@ namespace Duplicati.Server
{
var jobsToRun = new List<Server.Runner.IRunnerData>();
//TODO: Cache this to avoid frequent lookups
foreach(var id in FIXMEGlobal.DataConnection.GetBackupIDsForTags(sc.Tags).Distinct().Select(x => x.ToString()))
foreach (var id in FIXMEGlobal.DataConnection.GetBackupIDsForTags(sc.Tags).Distinct()
.Select(x => x.ToString()))
{
//See if it is already queued
var tmplst = from n in m_worker.CurrentTasks
where n.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup
select n.Backup;
where n.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup
select n.Backup;
var tastTemp = m_worker.CurrentTask;
if (tastTemp != null && tastTemp.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup)
tmplst = tmplst.Union(new [] { tastTemp.Backup });
if (tastTemp != null && tastTemp.Operation ==
Duplicati.Server.Serialization.DuplicatiOperation.Backup)
tmplst = tmplst.Union(new[] { tastTemp.Backup });
//If it is not already in queue, put it there
if (!tmplst.Any(x => x.ID == id))
@@ -306,13 +334,18 @@ namespace Duplicati.Server
{
Dictionary<string, string> options = Duplicati.Server.Runner.GetCommonOptions();
Duplicati.Server.Runner.ApplyOptions(entry, options);
if ((new Duplicati.Library.Main.Options(options)).DisableOnBattery && (Duplicati.Library.Utility.Power.PowerSupply.GetSource() == Duplicati.Library.Utility.Power.PowerSupply.Source.Battery))
if ((new Duplicati.Library.Main.Options(options)).DisableOnBattery &&
(Duplicati.Library.Utility.Power.PowerSupply.GetSource() ==
Duplicati.Library.Utility.Power.PowerSupply.Source.Battery))
{
Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "BackupDisabledOnBattery", "Scheduled backup disabled while on battery power.");
Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG,
"BackupDisabledOnBattery",
"Scheduled backup disabled while on battery power.");
}
else
{
jobsToRun.Add(Server.Runner.CreateTask(Duplicati.Server.Serialization.DuplicatiOperation.Backup, entry));
jobsToRun.Add(Server.Runner.CreateTask(
Duplicati.Server.Serialization.DuplicatiOperation.Backup, entry));
}
}
}
@@ -321,27 +354,32 @@ namespace Duplicati.Server
// Calculate next time, by finding the first entry later than now
try
{
start = GetNextValidTime(start, new DateTime(Math.Max(DateTime.UtcNow.AddSeconds(1).Ticks, start.AddSeconds(1).Ticks), DateTimeKind.Utc), sc.Repeat, sc.AllowedDays);
start = GetNextValidTime(start,
new DateTime(
Math.Max(DateTime.UtcNow.AddSeconds(1).Ticks, start.AddSeconds(1).Ticks),
DateTimeKind.Utc), sc.Repeat, sc.AllowedDays);
}
catch(Exception ex)
catch (Exception ex)
{
FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex);
FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(),
"Scheduler failed to find next date", ex);
continue;
}
Server.Runner.IRunnerData lastJob = jobsToRun.LastOrDefault();
if (lastJob != null)
{
lock (m_lock)
{
// The actual last run time will be updated when the StartingWork event is raised.
m_updateTasks[lastJob] = new Tuple<ISchedule, DateTime, DateTime>(sc, start, DateTime.UtcNow);
m_updateTasks[lastJob] =
new Tuple<ISchedule, DateTime, DateTime>(sc, start, DateTime.UtcNow);
}
}
foreach (var job in jobsToRun)
m_worker.AddTask(job);
if (start < DateTime.UtcNow)
{
//TODO: Report this somehow
@@ -349,23 +387,24 @@ namespace Duplicati.Server
}
}
scheduled[sc.ID] = new KeyValuePair<long,DateTime>(scticks, start);
scheduled[sc.ID] = new KeyValuePair<long, DateTime>(scticks, start);
}
}
var existing = lst.ToDictionary(x => x.ID);
//Sort them, lock as we assign the m_schedule variable
lock(m_lock)
lock (m_lock)
m_schedule = (from n in scheduled
where existing.ContainsKey(n.Key)
orderby n.Value.Value
select new KeyValuePair<DateTime, ISchedule>(n.Value.Value, existing[n.Key])).ToArray();
where existing.ContainsKey(n.Key)
orderby n.Value.Value
select new KeyValuePair<DateTime, ISchedule>(n.Value.Value, existing[n.Key])).ToArray();
// Remove unused entries
foreach(var c in (from n in scheduled where !existing.ContainsKey(n.Key) select n.Key).ToArray())
foreach (var c in (from n in scheduled where !existing.ContainsKey(n.Key) select n.Key).ToArray())
scheduled.Remove(c);
//Raise event if needed
// TODO: This triggers a new data event and a reconnect with long-poll
if (NewSchedule != null)
NewSchedule(this, null);
@@ -384,6 +423,7 @@ namespace Duplicati.Server
}
else
{
// TODO: This should be handled with events, instead of one wakeup per minute
//No tasks, check back later
waittime = 60 * 1000;
}
@@ -406,8 +446,7 @@ namespace Duplicati.Server
if (allowedDays == null || allowedDays.Length == 0)
return true;
else
return Array.IndexOf<DayOfWeek>(allowedDays, localTime.DayOfWeek) >= 0;
return Array.IndexOf<DayOfWeek>(allowedDays, localTime.DayOfWeek) >= 0;
}
}
}
}
@@ -29,21 +29,21 @@ namespace Duplicati.Server.Serializable
/// <summary>
/// This class collects all reportable status properties into a single class that can be exported as JSON
/// </summary>
public class ServerStatus : Duplicati.Server.Serialization.Interface.IServerStatus
public class ServerStatus(LiveControls liveControls) : Duplicati.Server.Serialization.Interface.IServerStatus
{
public LiveControlState ProgramState
{
get { return EnumConverter.Convert<LiveControlState>(FIXMEGlobal.LiveControl.State); }
get { return EnumConverter.Convert<LiveControlState>(liveControls.State); }
}
public string UpdatedVersion
{
get
{
public string UpdatedVersion
{
get
{
var u = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion;
if (u == null)
return null;
Version v;
if (!Version.TryParse(u.Version, out v))
return null;
@@ -51,7 +51,7 @@ namespace Duplicati.Server.Serializable
if (v <= System.Reflection.Assembly.GetExecutingAssembly().GetName().Version)
return null;
return u.Displayname;
return u.Displayname;
}
}
@@ -64,8 +64,8 @@ namespace Duplicati.Server.Serializable
public Tuple<long, string> ActiveTask
{
get
{
get
{
var t = FIXMEGlobal.WorkThread.CurrentTask;
if (t == null)
return null;
@@ -85,18 +85,18 @@ namespace Duplicati.Server.Serializable
{
return (
from n in FIXMEGlobal.Scheduler.Schedule
let backupid = (from t in n.Value.Tags
where t != null && t.StartsWith("ID=", StringComparison.Ordinal)
select t.Substring("ID=".Length)).FirstOrDefault()
where !string.IsNullOrWhiteSpace(backupid)
select new Tuple<string, DateTime>(backupid, n.Key)
let backupid = (from t in n.Value.Tags
where t != null && t.StartsWith("ID=", StringComparison.Ordinal)
select t.Substring("ID=".Length)).FirstOrDefault()
where !string.IsNullOrWhiteSpace(backupid)
select new Tuple<string, DateTime>(backupid, n.Key)
).ToList();
}
}
public bool HasWarning { get { return FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning; } }
public bool HasError { get { return FIXMEGlobal.DataConnection.ApplicationSettings.UnackedError; } }
public SuggestedStatusIcon SuggestedStatusIcon
{
get
@@ -105,12 +105,12 @@ namespace Duplicati.Server.Serializable
{
if (this.ProgramState == LiveControlState.Paused)
return SuggestedStatusIcon.Paused;
if (this.HasError)
return SuggestedStatusIcon.ReadyError;
if (this.HasWarning)
return SuggestedStatusIcon.ReadyWarning;
return SuggestedStatusIcon.Ready;
}
else
@@ -123,26 +123,25 @@ namespace Duplicati.Server.Serializable
}
}
public DateTime EstimatedPauseEnd
public DateTime EstimatedPauseEnd
{
get
{
return FIXMEGlobal.LiveControl.EstimatedPauseEnd;
get
{
return liveControls.EstimatedPauseEnd;
}
}
private long m_lastEventID = FIXMEGlobal.StatusEventNotifyer.EventNo;
public long LastEventID
{
public long LastEventID
{
get { return m_lastEventID; }
set { m_lastEventID = value; }
}
public long LastDataUpdateID { get { return FIXMEGlobal.PeekLastDataUpdateID(); } }
public long LastNotificationUpdateID { get { return FIXMEGlobal.PeekLastNotificationUpdateID(); } }
public long LastDataUpdateID => FIXMEGlobal.NotificationUpdateService.LastDataUpdateId;
public long LastNotificationUpdateID => FIXMEGlobal.NotificationUpdateService.LastNotificationUpdateId;
}
}
@@ -1,60 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Duplicati.Server.Serializable
{
/// <summary>
/// Implementation of a ExtJS treenode-like class for easy JSON export
/// </summary>
public class TreeNode
{
/// <summary>
/// The text displayed for the node
/// </summary>
public string text { get; set; }
/// <summary>
/// The node id
/// </summary>
public string id { get; set; }
/// <summary>
/// The class applied to the node
/// </summary>
public string cls { get; set; }
/// <summary>
/// The class applied to the icon
/// </summary>
public string iconCls { get; set; }
/// <summary>
/// True if the element should be checked
/// </summary>
public bool check { get; set; }
/// <summary>
/// True if the element is a leaf node
/// </summary>
public bool leaf { get; set; }
/// <summary>
/// Gets or sets the current path, if the item is a symbolic path
/// </summary>
public string resolvedpath { get; set; }
/// <summary>
/// True if the element is hidden
/// </summary>
public bool hidden { get; set; }
/// <summary>
/// True if the element is a symlink
/// </summary>
public bool symlink { get; set; }
/// <summary>
/// Constructs a new TreeNode
/// </summary>
public TreeNode()
{
this.cls = "folder";
this.iconCls = "x-tree-icon-parent";
this.check = false;
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Duplicati.Server.Serializable
{
/// <summary>
/// Implementation of a ExtJS treenode-like class for easy JSON export
/// </summary>
public class TreeNode
{
/// <summary>
/// The text displayed for the node
/// </summary>
public string text { get; set; }
/// <summary>
/// The node id
/// </summary>
public string id { get; set; }
/// <summary>
/// The class applied to the node
/// </summary>
public string cls { get; set; }
/// <summary>
/// The class applied to the icon
/// </summary>
public string iconCls { get; set; }
/// <summary>
/// True if the element should be checked
/// </summary>
public bool check { get; set; }
/// <summary>
/// True if the element is a leaf node
/// </summary>
public bool leaf { get; set; }
/// <summary>
/// Gets or sets the current path, if the item is a symbolic path
/// </summary>
public string resolvedpath { get; set; }
/// <summary>
/// True if the element is hidden
/// </summary>
public bool hidden { get; set; }
/// <summary>
/// True if the element has the system file attribute
/// </summary>
public bool systemFile { get; set; }
/// <summary>
/// True if the element is marked as temporary
/// </summary>
public bool temporary { get; set; }
/// <summary>
/// True if the element is a symlink
/// </summary>
public bool symlink { get; set; }
/// <summary>
/// Size of the file. -1 if directory or inaccessible
/// </summary>
public long fileSize { get; set; }
/// <summary>
/// Constructs a new TreeNode
/// </summary>
public TreeNode()
{
this.cls = "folder";
this.iconCls = "x-tree-icon-parent";
this.check = false;
}
}
}
+30 -26
View File
@@ -1,20 +1,24 @@
// Copyright (C) 2015, The Duplicati Team
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
// http://www.duplicati.com, info@duplicati.com
//
// 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
using System;
using System.Linq;
using System.Collections.Generic;
@@ -31,7 +35,7 @@ namespace Duplicati.Server
public static string ExpandEnvironmentVariables(string path)
{
foreach(var n in Nodes)
foreach (var n in Nodes)
if (path.StartsWith(n.id, StringComparison.Ordinal))
path = path.Replace(n.id, n.resolvedpath);
return Environment.ExpandEnvironmentVariables(path);
@@ -54,21 +58,21 @@ namespace Duplicati.Server
});
}
public static string TranslateToPath(string str)
public static string TranslateToPath(string str)
{
string res;
if (PathMap.TryGetValue(str, out res))
return res;
return null;
}
public static string TranslateToDisplayString(string str)
public static string TranslateToDisplayString(string str)
{
string res;
if (DisplayMap.TryGetValue(str, out res))
return res;
return null;
}
@@ -109,12 +113,12 @@ namespace Duplicati.Server
{
}
}
static SpecialFolders()
{
var lst = new List<Serializable.TreeNode>();
if (Platform.IsClientWindows)
if (OperatingSystem.IsWindows())
{
TryAdd(lst, Environment.SpecialFolder.MyDocuments, "%MY_DOCUMENTS%", "My Documents");
TryAdd(lst, Environment.SpecialFolder.MyMusic, "%MY_MUSIC%", "My Music");
@@ -147,7 +151,7 @@ namespace Duplicati.Server
Nodes = lst.ToArray();
}
internal static Dictionary<string, string> GetSourceNames(Serialization.Interface.IBackup backup)
public static Dictionary<string, string> GetSourceNames(Serialization.Interface.IBackup backup)
{
if (backup.Sources == null || backup.Sources.Length == 0)
return new Dictionary<string, string>();
@@ -181,7 +185,7 @@ namespace Duplicati.Server
// Handle duplicates
var result = new Dictionary<string, string>();
foreach(var x in sources)
foreach (var x in sources)
result[x.Key] = x.Value;
return result;
+9 -5
View File
@@ -2,8 +2,10 @@ using System.Collections.Generic;
using Duplicati.Library.Localization.Short;
using System.Linq;
namespace Duplicati.Server.Strings {
public static class Program {
namespace Duplicati.Server.Strings
{
public static class Program
{
public static string AnotherInstanceDetected { get { return LC.L(@"Another instance is running, and was notified"); } }
public static string DatabaseOpenError(string message) { return LC.L(@"Failed to create, open or upgrade the database.
Error message: {0}", message); }
@@ -42,11 +44,13 @@ Error message: {0}", error); }
public static string ServerencryptionkeyLong(string envname, string decryptionoption) { return LC.L(@"This option sets the encryption key used to scramble the local settings database. This option can also be set with the environment variable {0}. Use the option --{1} to disable the database scrambling.", envname, decryptionoption); }
public static string TempdirShort { get { return LC.L(@"Temporary storage folder"); } }
public static string TempdirLong { get { return LC.L(@"This option can be used to supply an alternative folder for temporary storage. By default the system default temporary folder is used. Note that also SQLite will put temporary files in this temporary folder."); } }
}
internal static class Scheduler {
public static string WebserverResetJwtConfigDescription { get { return LC.L(@"Resets the JWT configuration, invalidating any issued login tokens"); } }
}
internal static class Scheduler
{
public static string InvalidTimeSetupError(System.DateTime startdate, string interval, string alloweddays) { return LC.L(@"Unable to find a valid date, given the start date {0}, the repetition interval {1} and the allowed days {2}", startdate, interval, alloweddays); }
}
internal static class Server
public static class Server
{
public static string DefectSSLCertInDatabase { get { return @"Unable to create SSL certificate using data from database. Starting without SSL."; } }
public static string StartedServer(string ip, int port) { return LC.L(@"Server has started and is listening on {0}, port {1}", ip, port); }
+211 -205
View File
@@ -1,205 +1,211 @@
// Copyright (C) 2015, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
//
// 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
using System;
using System.Linq;
using System.Threading;
using Duplicati.Library.RestAPI;
using Duplicati.Server.Serialization;
namespace Duplicati.Server
{
/// <summary>
/// The thread that checks on the update server if new versions are available
/// </summary>
public class UpdatePollThread
{
private readonly Thread m_thread;
private volatile bool m_terminated = false;
private volatile bool m_forceCheck = false;
private readonly object m_lock = new object();
private readonly AutoResetEvent m_waitSignal;
private double m_downloadProgress;
public bool IsUpdateRequested { get; private set; } = false;
public UpdatePollerStates ThreadState { get; private set; }
public double DownloadProgess
{
get { return m_downloadProgress; }
private set
{
var oldv = m_downloadProgress;
m_downloadProgress = value;
if ((int)(oldv * 100) != (int)(value * 100))
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
}
}
public UpdatePollThread()
{
m_waitSignal = new AutoResetEvent(false);
ThreadState = UpdatePollerStates.Waiting;
m_thread = new Thread(Run);
m_thread.IsBackground = true;
m_thread.Name = "UpdatePollThread";
m_thread.Start();
}
public void CheckNow()
{
lock (m_lock)
{
m_forceCheck = true;
m_waitSignal.Set();
}
}
public void Terminate()
{
lock (m_lock)
{
m_terminated = true;
m_waitSignal.Set();
}
}
public void Reschedule()
{
m_waitSignal.Set();
}
private void Run()
{
// Wait on startup
m_waitSignal.WaitOne(TimeSpan.FromMinutes(1), true);
while (!m_terminated)
{
var nextCheck = FIXMEGlobal.DataConnection.ApplicationSettings.NextUpdateCheck;
var maxcheck = TimeSpan.FromDays(7);
try
{
maxcheck = Library.Utility.Timeparser.ParseTimeSpan(FIXMEGlobal.DataConnection.ApplicationSettings.UpdateCheckInterval);
}
catch
{
}
// If we have some weirdness, just check now
if (nextCheck - DateTime.UtcNow > maxcheck)
nextCheck = DateTime.UtcNow - TimeSpan.FromSeconds(1);
if (nextCheck < DateTime.UtcNow || m_forceCheck)
{
lock (m_lock)
m_forceCheck = false;
ThreadState = UpdatePollerStates.Checking;
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
DateTime started = DateTime.UtcNow;
FIXMEGlobal.DataConnection.ApplicationSettings.LastUpdateCheck = started;
nextCheck = FIXMEGlobal.DataConnection.ApplicationSettings.NextUpdateCheck;
Library.AutoUpdater.ReleaseType rt;
if (!Enum.TryParse<Library.AutoUpdater.ReleaseType>(FIXMEGlobal.DataConnection.ApplicationSettings.UpdateChannel, true, out rt))
rt = Duplicati.Library.AutoUpdater.ReleaseType.Unknown;
// Choose the default channel in case we have unknown
rt = rt == Duplicati.Library.AutoUpdater.ReleaseType.Unknown ? Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel : rt;
try
{
var update = Duplicati.Library.AutoUpdater.UpdaterManager.CheckForUpdate(rt);
if (update != null)
FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion = update;
}
catch
{
}
// It could be that we have registered an update from a more unstable channel,
// but the user has switched to a more stable channel.
// In that case we discard the old update to avoid offering it.
if (FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion != null)
{
Library.AutoUpdater.ReleaseType updatert;
var updatertstring = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion.ReleaseType;
if (string.Equals(updatertstring, "preview", StringComparison.OrdinalIgnoreCase))
updatertstring = Library.AutoUpdater.ReleaseType.Experimental.ToString();
if (!Enum.TryParse<Library.AutoUpdater.ReleaseType>(updatertstring, true, out updatert))
updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly;
if (updatert == Duplicati.Library.AutoUpdater.ReleaseType.Unknown)
updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly;
if (updatert > rt)
FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion = null;
}
var updatedinfo = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion;
if (updatedinfo != null && Duplicati.Library.AutoUpdater.UpdaterManager.TryParseVersion(updatedinfo.Version) > System.Reflection.Assembly.GetExecutingAssembly().GetName().Version)
{
var package = updatedinfo.FindPackage();
FIXMEGlobal.DataConnection.RegisterNotification(
NotificationType.Information,
"Found update",
updatedinfo.Displayname,
null,
null,
"update:new",
null,
"NewUpdateFound",
null,
(self, all) =>
{
return all.FirstOrDefault(x => x.Action == "update:new") ?? self;
}
);
}
}
DownloadProgess = 0;
if (ThreadState != UpdatePollerStates.Waiting)
{
ThreadState = UpdatePollerStates.Waiting;
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
}
var waitTime = nextCheck - DateTime.UtcNow;
// Guard against spin-loop
if (waitTime.TotalSeconds < 5)
waitTime = TimeSpan.FromSeconds(5);
// Guard against year-long waits
// A re-check does not cause an update check
if (waitTime.TotalDays > 1)
waitTime = TimeSpan.FromDays(1);
m_waitSignal.WaitOne(waitTime, true);
}
}
}
}
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
using System.Threading;
using Duplicati.Server.Database;
using Duplicati.Server.Serialization;
namespace Duplicati.Server
{
/// <summary>
/// The thread that checks on the update server if new versions are available
/// </summary>
public class UpdatePollThread(Connection connection, EventPollNotify eventPollNotify)
{
private Thread m_thread;
private volatile bool m_terminated = false;
private volatile bool m_forceCheck = false;
private readonly object m_lock = new object();
private AutoResetEvent m_waitSignal;
private double m_downloadProgress;
public bool IsUpdateRequested { get; private set; } = false;
public UpdatePollerStates ThreadState { get; private set; }
public double DownloadProgess
{
get { return m_downloadProgress; }
private set
{
var oldv = m_downloadProgress;
m_downloadProgress = value;
if ((int)(oldv * 100) != (int)(value * 100))
eventPollNotify.SignalNewEvent();
}
}
public void Init()
{
m_waitSignal = new AutoResetEvent(false);
ThreadState = UpdatePollerStates.Waiting;
m_thread = new Thread(Run)
{
IsBackground = true,
Name = "UpdatePollThread"
};
m_thread.Start();
}
public void CheckNow()
{
lock (m_lock)
{
m_forceCheck = true;
m_waitSignal.Set();
}
}
public void Terminate()
{
lock (m_lock)
{
m_terminated = true;
m_waitSignal.Set();
}
}
public void Reschedule()
{
m_waitSignal.Set();
}
private void Run()
{
// Wait on startup
m_waitSignal.WaitOne(TimeSpan.FromMinutes(1), true);
while (!m_terminated)
{
var nextCheck = connection.ApplicationSettings.NextUpdateCheck;
var maxcheck = TimeSpan.FromDays(7);
try
{
maxcheck = Library.Utility.Timeparser.ParseTimeSpan(connection.ApplicationSettings.UpdateCheckInterval);
}
catch
{
}
// If we have some weirdness, just check now
if (nextCheck - DateTime.UtcNow > maxcheck)
nextCheck = DateTime.UtcNow - TimeSpan.FromSeconds(1);
if (nextCheck < DateTime.UtcNow || m_forceCheck)
{
lock (m_lock)
m_forceCheck = false;
ThreadState = UpdatePollerStates.Checking;
eventPollNotify.SignalNewEvent();
DateTime started = DateTime.UtcNow;
connection.ApplicationSettings.LastUpdateCheck = started;
nextCheck = connection.ApplicationSettings.NextUpdateCheck;
Library.AutoUpdater.ReleaseType rt;
if (!Enum.TryParse<Library.AutoUpdater.ReleaseType>(connection.ApplicationSettings.UpdateChannel, true, out rt))
rt = Duplicati.Library.AutoUpdater.ReleaseType.Unknown;
// Choose the default channel in case we have unknown
rt = rt == Duplicati.Library.AutoUpdater.ReleaseType.Unknown ? Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel : rt;
try
{
var update = Duplicati.Library.AutoUpdater.UpdaterManager.CheckForUpdate(rt);
if (update != null)
connection.ApplicationSettings.UpdatedVersion = update;
}
catch
{
}
// It could be that we have registered an update from a more unstable channel,
// but the user has switched to a more stable channel.
// In that case we discard the old update to avoid offering it.
if (connection.ApplicationSettings.UpdatedVersion != null)
{
Library.AutoUpdater.ReleaseType updatert;
var updatertstring = connection.ApplicationSettings.UpdatedVersion.ReleaseType;
if (string.Equals(updatertstring, "preview", StringComparison.OrdinalIgnoreCase))
updatertstring = Library.AutoUpdater.ReleaseType.Experimental.ToString();
if (!Enum.TryParse<Library.AutoUpdater.ReleaseType>(updatertstring, true, out updatert))
updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly;
if (updatert == Duplicati.Library.AutoUpdater.ReleaseType.Unknown)
updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly;
if (updatert > rt)
connection.ApplicationSettings.UpdatedVersion = null;
}
var updatedinfo = connection.ApplicationSettings.UpdatedVersion;
if (updatedinfo != null && Duplicati.Library.AutoUpdater.UpdaterManager.TryParseVersion(updatedinfo.Version) > System.Reflection.Assembly.GetExecutingAssembly().GetName().Version)
{
var package = updatedinfo.FindPackage();
connection.RegisterNotification(
NotificationType.Information,
"Found update",
updatedinfo.Displayname,
null,
null,
"update:new",
null,
"NewUpdateFound",
null,
(self, all) =>
{
return all.FirstOrDefault(x => x.Action == "update:new") ?? self;
}
);
}
}
DownloadProgess = 0;
if (ThreadState != UpdatePollerStates.Waiting)
{
ThreadState = UpdatePollerStates.Waiting;
eventPollNotify.SignalNewEvent();
}
var waitTime = nextCheck - DateTime.UtcNow;
// Guard against spin-loop
if (waitTime.TotalSeconds < 5)
waitTime = TimeSpan.FromSeconds(5);
// Guard against year-long waits
// A re-check does not cause an update check
if (waitTime.TotalDays > 1)
waitTime = TimeSpan.FromDays(1);
m_waitSignal.WaitOne(waitTime, true);
}
}
}
}
@@ -1,342 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.Linq;
using HttpServer;
using HttpServer.HttpModules;
using System.Collections.Generic;
using Duplicati.Library.RestAPI;
namespace Duplicati.Server.WebServer
{
public class AuthenticationHandler : HttpModule
{
private const string AUTH_COOKIE_NAME = "session-auth";
private const string NONCE_COOKIE_NAME = "session-nonce";
private const string XSRF_COOKIE_NAME = "xsrf-token";
private const string XSRF_HEADER_NAME = "X-XSRF-Token";
private const string TRAYICONPASSWORDSOURCE_HEADER = "X-TrayIcon-PasswordSource";
public const string LOGIN_SCRIPT_URI = "/login.cgi";
public const string LOGOUT_SCRIPT_URI = "/logout.cgi";
public const string CAPTCHA_IMAGE_URI = RESTHandler.API_URI_PATH + "/captcha/";
private const int XSRF_TIMEOUT_MINUTES = 10;
private const int AUTH_TIMEOUT_MINUTES = 10;
private readonly ConcurrentDictionary<string, DateTime> m_activeTokens = new ConcurrentDictionary<string, DateTime>();
private readonly ConcurrentDictionary<string, Tuple<DateTime, string>> m_activeNonces = new ConcurrentDictionary<string, Tuple<DateTime, string>>();
private readonly ConcurrentDictionary<string, DateTime> m_activexsrf = new ConcurrentDictionary<string, DateTime>();
readonly System.Security.Cryptography.RandomNumberGenerator m_prng = System.Security.Cryptography.RNGCryptoServiceProvider.Create();
private string FindXSRFToken(HttpServer.IHttpRequest request)
{
string xsrftoken = request.Headers[XSRF_HEADER_NAME] ?? "";
if (string.IsNullOrWhiteSpace(xsrftoken))
{
var xsrfq = request.Form[XSRF_HEADER_NAME] ?? request.Form[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)];
xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value;
}
if (string.IsNullOrWhiteSpace(xsrftoken))
{
var xsrfq = request.QueryString[XSRF_HEADER_NAME] ?? request.QueryString[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)];
xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value;
}
return xsrftoken;
}
private bool AddXSRFTokenToRespone(HttpServer.IHttpResponse response)
{
if (m_activexsrf.Count > 500)
return false;
var buf = new byte[32];
var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
m_prng.GetBytes(buf);
var token = Convert.ToBase64String(buf);
m_activexsrf.AddOrUpdate(token, key => expires, (key, existingExpires) =>
{
// Simulate the original behavior => if the random token, against all odds, is already used
// we throw an ArgumentException
throw new ArgumentException("An element with the same key already exists in the dictionary.");
});
response.Cookies.Add(new HttpServer.ResponseCookie(XSRF_COOKIE_NAME, token, expires));
return true;
}
private string FindAuthCookie(HttpServer.IHttpRequest request)
{
var authcookie = request.Cookies[AUTH_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(AUTH_COOKIE_NAME)];
var authform = request.Form["auth-token"] ?? request.Form[Library.Utility.Uri.UrlEncode("auth-token")];
var authquery = request.QueryString["auth-token"] ?? request.QueryString[Library.Utility.Uri.UrlEncode("auth-token")];
var auth_token = string.IsNullOrWhiteSpace(authcookie?.Value) ? null : authcookie.Value;
if (!string.IsNullOrWhiteSpace(authquery?.Value))
auth_token = authquery.Value;
if (!string.IsNullOrWhiteSpace(authform?.Value))
auth_token = authform.Value;
return auth_token;
}
private bool HasXSRFCookie(HttpServer.IHttpRequest request)
{
// Clean up expired XSRF cookies
foreach (var k in (from n in m_activexsrf where DateTime.UtcNow > n.Value select n.Key))
m_activexsrf.TryRemove(k, out _);
var xsrfcookie = request.Cookies[XSRF_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(XSRF_COOKIE_NAME)];
var value = xsrfcookie == null ? null : xsrfcookie.Value;
if (string.IsNullOrWhiteSpace(value))
return false;
if (m_activexsrf.ContainsKey(value))
{
m_activexsrf[value] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
return true;
}
else if (m_activexsrf.ContainsKey(Library.Utility.Uri.UrlDecode(value)))
{
m_activexsrf[Library.Utility.Uri.UrlDecode(value)] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
return true;
}
return false;
}
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
{
HttpServer.HttpInput input = String.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? request.Form : request.QueryString;
var auth_token = FindAuthCookie(request);
var xsrf_token = FindXSRFToken(request);
if (!HasXSRFCookie(request))
{
var cookieAdded = AddXSRFTokenToRespone(response);
if (!cookieAdded)
{
response.Status = System.Net.HttpStatusCode.ServiceUnavailable;
response.Reason = "Too Many Concurrent Request, try again later";
return true;
}
}
if (LOGOUT_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
{
if (!string.IsNullOrWhiteSpace(auth_token))
{
// Remove the active auth token
m_activeTokens.TryRemove(auth_token, out _);
}
response.Status = System.Net.HttpStatusCode.NoContent;
response.Reason = "OK";
return true;
}
else if (LOGIN_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
{
// Remove expired nonces
foreach(var k in (from n in m_activeNonces where DateTime.UtcNow > n.Value.Item1 select n.Key))
m_activeNonces.TryRemove(k, out _);
if (input["get-nonce"] != null && !string.IsNullOrWhiteSpace(input["get-nonce"].Value))
{
if (m_activeNonces.Count > 50)
{
response.Status = System.Net.HttpStatusCode.ServiceUnavailable;
response.Reason = "Too many active login attempts";
return true;
}
var password = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPassword;
if (request.Headers[TRAYICONPASSWORDSOURCE_HEADER] == "database")
password = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPasswordTrayIconHash;
var buf = new byte[32];
var expires = DateTime.UtcNow.AddMinutes(AUTH_TIMEOUT_MINUTES);
m_prng.GetBytes(buf);
var nonce = Convert.ToBase64String(buf);
var sha256 = System.Security.Cryptography.SHA256.Create();
sha256.TransformBlock(buf, 0, buf.Length, buf, 0);
buf = Convert.FromBase64String(password);
sha256.TransformFinalBlock(buf, 0, buf.Length);
var pwd = Convert.ToBase64String(sha256.Hash);
m_activeNonces.AddOrUpdate(nonce, key => new Tuple<DateTime, string>(expires, pwd), (key, existingValue) =>
{
// Simulate the original behavior => if the nonce, against all odds, is already used
// we throw an ArgumentException
throw new ArgumentException("An element with the same key already exists in the dictionary.");
});
response.Cookies.Add(new HttpServer.ResponseCookie(NONCE_COOKIE_NAME, nonce, expires));
using(var bw = new BodyWriter(response, request))
{
bw.OutputOK(new {
Status = "OK",
Nonce = nonce,
Salt = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPasswordSalt
});
}
return true;
}
else
{
if (input["password"] != null && !string.IsNullOrWhiteSpace(input["password"].Value))
{
var nonce_el = request.Cookies[NONCE_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(NONCE_COOKIE_NAME)];
var nonce = nonce_el == null || string.IsNullOrWhiteSpace(nonce_el.Value) ? "" : nonce_el.Value;
var urldecoded = nonce == null ? "" : Duplicati.Library.Utility.Uri.UrlDecode(nonce);
if (m_activeNonces.ContainsKey(urldecoded))
nonce = urldecoded;
if (!m_activeNonces.ContainsKey(nonce))
{
response.Status = System.Net.HttpStatusCode.Unauthorized;
response.Reason = "Unauthorized";
response.ContentType = "application/json";
return true;
}
var pwd = m_activeNonces[nonce].Item2;
// Remove the nonce
m_activeNonces.TryRemove(nonce, out _);
if (pwd != input["password"].Value)
{
response.Status = System.Net.HttpStatusCode.Unauthorized;
response.Reason = "Unauthorized";
response.ContentType = "application/json";
return true;
}
var buf = new byte[32];
var expires = DateTime.UtcNow.AddHours(1);
m_prng.GetBytes(buf);
var token = Duplicati.Library.Utility.Utility.Base64UrlEncode(buf);
while (token.Length > 0 && token.EndsWith("=", StringComparison.Ordinal))
token = token.Substring(0, token.Length - 1);
m_activeTokens.AddOrUpdate(token, key => expires, (key, existingValue) =>
{
// Simulate the original behavior => if the token, against all odds, is already used
// we throw an ArgumentException
throw new ArgumentException("An element with the same key already exists in the dictionary.");
});
response.Cookies.Add(new HttpServer.ResponseCookie(AUTH_COOKIE_NAME, token, expires));
using(var bw = new BodyWriter(response, request))
bw.OutputOK();
return true;
}
}
}
var limitedAccess =
request.Uri.AbsolutePath.StartsWith(RESTHandler.API_URI_PATH, StringComparison.OrdinalIgnoreCase)
;
// Override to allow the CAPTCHA call to go through
if (request.Uri.AbsolutePath.StartsWith(CAPTCHA_IMAGE_URI, StringComparison.OrdinalIgnoreCase) && request.Method == "GET")
limitedAccess = false;
if (limitedAccess)
{
if (xsrf_token != null && m_activexsrf.ContainsKey(xsrf_token))
{
var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
m_activexsrf[xsrf_token] = expires;
response.Cookies.Add(new ResponseCookie(XSRF_COOKIE_NAME, xsrf_token, expires));
}
else
{
response.Status = System.Net.HttpStatusCode.BadRequest;
response.Reason = "Missing XSRF Token. Please reload the page";
return true;
}
}
if (string.IsNullOrWhiteSpace(FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPassword))
return false;
foreach(var k in (from n in m_activeTokens where DateTime.UtcNow > n.Value select n.Key))
m_activeTokens.TryRemove(k, out _);
// If we have a valid token, proceed
if (!string.IsNullOrWhiteSpace(auth_token))
{
DateTime expires;
var found = m_activeTokens.TryGetValue(auth_token, out expires);
if (!found)
{
auth_token = Duplicati.Library.Utility.Uri.UrlDecode(auth_token);
found = m_activeTokens.TryGetValue(auth_token, out expires);
}
if (found && DateTime.UtcNow < expires)
{
expires = DateTime.UtcNow.AddHours(1);
m_activeTokens[auth_token] = expires;
response.Cookies.Add(new ResponseCookie(AUTH_COOKIE_NAME, auth_token, expires));
return false;
}
}
if ("/".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase) || "/index.html".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
{
response.Redirect("/login.html");
return true;
}
if (limitedAccess)
{
response.Status = System.Net.HttpStatusCode.Unauthorized;
response.Reason = "Not logged in";
response.AddHeader("Location", "login.html");
return true;
}
return false;
}
}
}
@@ -1,124 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Globalization;
using System.Threading.Tasks;
using Duplicati.Server.Serialization;
namespace Duplicati.Server.WebServer
{
public class BodyWriter : IDisposable, IAsyncDisposable
{
private readonly HttpServer.IHttpResponse m_resp;
private readonly string m_jsonp;
private static readonly object SUCCESS_RESPONSE = new { Status = "OK" };
private readonly System.IO.StreamWriter m_bodyStreamWriter;
public BodyWriter(HttpServer.IHttpResponse resp, HttpServer.IHttpRequest request)
: this(resp, request.QueryString["jsonp"].Value)
{
}
public BodyWriter(HttpServer.IHttpResponse resp, string jsonp)
{
m_bodyStreamWriter = new System.IO.StreamWriter(resp.Body, resp.Encoding);
m_resp = resp;
m_jsonp = jsonp;
if (!m_resp.HeadersSent)
m_resp.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
}
public void SetOK()
{
m_resp.Reason = "OK";
m_resp.Status = System.Net.HttpStatusCode.OK;
}
public void OutputOK(object result = null)
{
SetOK();
WriteJsonObject(result ?? SUCCESS_RESPONSE);
}
public void WriteJsonObject(object o)
{
if (!m_resp.HeadersSent)
m_resp.ContentType = "application/json";
Task.Run(async () => {
if (!string.IsNullOrEmpty(m_jsonp))
{
await m_bodyStreamWriter.WriteAsync(m_jsonp);
await m_bodyStreamWriter.WriteAsync('(');
}
var oldCulture = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
await Serializer.SerializeJsonAsync(m_bodyStreamWriter, o, true);
CultureInfo.CurrentCulture = oldCulture;
if (!string.IsNullOrEmpty(m_jsonp))
{
await m_bodyStreamWriter.WriteAsync(')');
await m_bodyStreamWriter.FlushAsync();
}
}).GetAwaiter().GetResult();
}
public void Dispose()
{
Task.Run(async () => {
await DisposeAsync();
}).GetAwaiter().GetResult();
}
private bool disposed = false;
public async ValueTask DisposeAsync()
{
if(disposed) return;
disposed = true;
if (!m_resp.HeadersSent)
{
await m_bodyStreamWriter.FlushAsync();
m_resp.ContentLength = m_bodyStreamWriter.BaseStream.Length;
m_resp.Send();
}
await m_bodyStreamWriter.DisposeAsync();
}
internal void Flush()
{
Task.Run(async () => {
await m_bodyStreamWriter.FlushAsync();
}).GetAwaiter().GetResult();
}
internal void Write(string v)
{
Task.Run(async () => {
await m_bodyStreamWriter.WriteAsync(v);
}).GetAwaiter().GetResult();
}
}
}
@@ -1,133 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Text;
namespace Duplicati.Server.WebServer
{
/// <summary>
/// Helper class for creating Captcha images
/// </summary>
public static class CaptchaUtil
{
/// <summary>
/// A lookup string with characters to use
/// </summary>
private static readonly string DEFAULT_CHARS = "ACDEFGHJKLMNPQRTUVWXY34679";
/// <summary>
/// A range of possible brush colors
/// </summary>
private static readonly Brush[] BRUSH_COLORS =
typeof(Brushes)
.GetProperties(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public)
.Where(x => x.PropertyType == typeof(Brush))
.Select(x => x.GetValue(null, null) as Brush)
.Where(x => x != null)
.ToArray();
/// <summary>
/// Approximate the size in pixels of text drawn at the given fontsize
/// </summary>
private static int ApproxTextWidth(string text, FontFamily fontfamily, int fontsize)
{
using (var font = new Font(fontfamily, fontsize, GraphicsUnit.Pixel))
using (var graphics = Graphics.FromImage(new Bitmap(1, 1))) {
return (int) graphics.MeasureString(text, font).Width;
}
}
/// <summary>
/// Creates a random answer.
/// </summary>
/// <returns>The random answer.</returns>
/// <param name="allowedchars">The list of allowed chars, supply a character multiple times to change frequency.</param>
/// <param name="minlength">The minimum answer length.</param>
/// <param name="maxlength">The maximum answer length.</param>
public static string CreateRandomAnswer(string allowedchars = null, int minlength = 10, int maxlength = 12)
{
allowedchars = allowedchars ?? DEFAULT_CHARS;
var rnd = new Random();
var len = rnd.Next(Math.Min(minlength, maxlength), Math.Max(minlength, maxlength) + 1);
if (len <= 0)
throw new ArgumentException($"The values ${minlength} and ${maxlength} gave a final length of {len} and it must be greater than 0");
return new string(Enumerable.Range(0, len).Select(x => allowedchars[rnd.Next(0, allowedchars.Length)]).ToArray());
}
/// <summary>
/// Creates a captcha image.
/// </summary>
/// <returns>The captcha image.</returns>
/// <param name="answer">The captcha solution string.</param>
/// <param name="size">The size of the image, omit to get a size based on the string.</param>
/// <param name="fontsize">The size of the font used to create the captcha, in pixels.</param>
public static Bitmap CreateCaptcha(string answer, Size size = default(Size), int fontsize = 40)
{
var fontfamily = FontFamily.GenericSansSerif;
var text_width = ApproxTextWidth(answer, fontfamily, fontsize);
if (size.Width == 0 || size.Height == 0)
size = new Size((int) (text_width * 1.2), (int) (fontsize * 1.2));
var bmp = new Bitmap(size.Width, size.Height);
var rnd = new Random();
var stray_x = fontsize / 2;
var stray_y = size.Height / 4;
var ans_stray_x = fontsize / 3;
var ans_stray_y = size.Height / 6;
using (var graphics = Graphics.FromImage(bmp))
using (var font1 = new Font(fontfamily, fontsize, GraphicsUnit.Pixel))
using (var font2 = new Font(fontfamily, fontsize, GraphicsUnit.Pixel))
using (var font3 = new HatchBrush(HatchStyle.Shingle, Color.GhostWhite, Color.DarkBlue))
{
graphics.Clear(Color.White);
graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
// Apply a some background string to make it hard to do OCR
foreach (var color in new[] { Color.Yellow, Color.LightGreen, Color.GreenYellow })
using (var brush = new SolidBrush(color))
graphics.DrawString(CreateRandomAnswer(minlength: answer.Length, maxlength: answer.Length), font2, brush, rnd.Next(-stray_x, stray_x), rnd.Next(-stray_y, stray_y));
var spacing = (size.Width / fontsize) + rnd.Next(0, stray_x);
// Create a vertical background lines
for (var i = rnd.Next(0, stray_x); i < size.Width; i += spacing)
using (var pen = new Pen(BRUSH_COLORS[rnd.Next(0, BRUSH_COLORS.Length)]))
graphics.DrawLine(pen, i + rnd.Next(-stray_x, stray_x), rnd.Next(0, stray_y), i + rnd.Next(-stray_x, stray_x), size.Height - rnd.Next(0, stray_y));
spacing = (size.Height / fontsize) + rnd.Next(0, stray_y);
// Create a horizontal background lines
for (var i = rnd.Next(0, stray_y); i < size.Height; i += spacing)
using (var pen = new Pen(BRUSH_COLORS[rnd.Next(0, BRUSH_COLORS.Length)]))
graphics.DrawLine(pen, rnd.Next(0, stray_x), i + rnd.Next(-stray_y, stray_y), size.Width - rnd.Next(0, stray_x), i + rnd.Next(-stray_y, stray_y));
// Draw the actual answer
graphics.DrawString(answer, font1, font3, ((size.Width - text_width) / 2) + rnd.Next(-ans_stray_x, ans_stray_x), ((size.Height - fontsize) / 2) + rnd.Next(-ans_stray_y, ans_stray_y));
return bmp;
}
}
}
}
@@ -1,82 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Linq;
using HttpServer;
using HttpServer.HttpModules;
using HttpServer.Exceptions;
using Duplicati.Library.Common.IO;
namespace Duplicati.Server.WebServer
{
internal class IndexHtmlHandler : HttpModule
{
private readonly string m_webroot;
private static readonly string[] ForbiddenChars = new string[] {"\\", "..", ":"}.Union(from n in System.IO.Path.GetInvalidPathChars() select n.ToString()).Distinct().ToArray();
private static readonly string DirSep = Util.DirectorySeparatorString;
public IndexHtmlHandler(string webroot) { m_webroot = webroot; }
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
{
var path = this.GetPath(request.Uri);
var html = System.IO.Path.Combine(path, "index.html");
var htm = System.IO.Path.Combine(path, "index.htm");
if (System.IO.Directory.Exists(path) && (System.IO.File.Exists(html) || System.IO.File.Exists(htm)))
{
if (!request.Uri.AbsolutePath.EndsWith("/", StringComparison.Ordinal))
{
response.Redirect(request.Uri.AbsolutePath + "/");
return true;
}
response.Status = System.Net.HttpStatusCode.OK;
response.Reason = "OK";
response.ContentType = "text/html; charset=utf-8";
response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
using (var fs = System.IO.File.OpenRead(System.IO.File.Exists(html) ? html : htm))
{
response.ContentLength = fs.Length;
response.Body = fs;
response.Send();
}
return true;
}
return false;
}
private string GetPath(Uri uri)
{
if (ForbiddenChars.Any(x => uri.AbsolutePath.Contains(x)))
throw new BadRequestException("Illegal path");
var uripath = Uri.UnescapeDataString(uri.AbsolutePath);
while(uripath.Length > 0 && (uripath.StartsWith("/", StringComparison.Ordinal) || uripath.StartsWith(DirSep, StringComparison.Ordinal)))
uripath = uripath.Substring(1);
return System.IO.Path.Combine(m_webroot, uripath.Replace('/', System.IO.Path.DirectorySeparatorChar));
}
}
}
@@ -1,263 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using HttpServer.HttpModules;
using Duplicati.Server.WebServer.RESTMethods;
using Duplicati.Library.RestAPI;
namespace Duplicati.Server.WebServer
{
public class RESTHandler : HttpModule
{
public const string API_URI_PATH = "/api/v1";
public static readonly int API_URI_SEGMENTS = API_URI_PATH.Split(new char[] {'/'}).Length;
private static readonly Dictionary<string, IRESTMethod> _modules = new Dictionary<string, IRESTMethod>(StringComparer.OrdinalIgnoreCase);
public static IDictionary<string, IRESTMethod> Modules { get { return _modules; } }
/// <summary>
/// Loads all REST modules in the Duplicati.Server.WebServer.RESTMethods namespace
/// </summary>
static RESTHandler()
{
var lst =
from n in typeof(IRESTMethod).Assembly.GetTypes()
where
n.Namespace == typeof(IRESTMethod).Namespace
&&
typeof(IRESTMethod).IsAssignableFrom(n)
&&
!n.IsAbstract
&&
!n.IsInterface
select n;
foreach(var t in lst)
{
var m = (IRESTMethod)Activator.CreateInstance(t);
_modules.Add(t.Name.ToLowerInvariant(), m);
}
}
public static void HandleControlCGI(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, Type module)
{
var method = request.Method;
if (!string.IsNullOrWhiteSpace(request.Headers["X-HTTP-Method-Override"]))
method = request.Headers["X-HTTP-Method-Override"];
DoProcess(request, response, session, method, module.Name.ToLowerInvariant(), (String.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? request.Form : request.QueryString)["id"].Value);
}
private static readonly ConcurrentDictionary<string, System.Globalization.CultureInfo> _cultureCache = new ConcurrentDictionary<string, System.Globalization.CultureInfo>(StringComparer.OrdinalIgnoreCase);
private static System.Globalization.CultureInfo ParseRequestCulture(RequestInfo info)
{
// Inject the override
return ParseRequestCulture(string.Format("{0},{1}", info.Request.Headers["X-UI-Language"], info.Request.Headers["Accept-Language"]));
}
public static System.Globalization.CultureInfo ParseDefaultRequestCulture(RequestInfo info)
{
if (info == null)
return null;
return ParseRequestCulture(info.Request.Headers["Accept-Language"]);
}
private static System.Globalization.CultureInfo ParseRequestCulture(string acceptheader)
{
acceptheader = acceptheader ?? string.Empty;
// Lock-free read
System.Globalization.CultureInfo ci;
if (_cultureCache.TryGetValue(acceptheader, out ci))
return ci;
// Lock-free assignment, we might compute the value twice
return _cultureCache[acceptheader] =
// Parse headers like "Accept-Language: da, en-gb;q=0.8, en;q=0.7"
acceptheader
.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
.Select(x =>
{
var opts = x.Split(new[] { ";" }, StringSplitOptions.RemoveEmptyEntries);
var lang = opts.FirstOrDefault();
var weight =
opts.Where(y => y.StartsWith("q=", StringComparison.OrdinalIgnoreCase))
.Select(y =>
{
float f;
float.TryParse(y.Substring(2), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out f);
return f;
}).FirstOrDefault();
// Set the default weight=1
if (weight <= 0.001 && weight >= 0)
weight = 1;
return new KeyValuePair<string, float>(lang, weight);
})
// Handle priority
.OrderByDescending(x => x.Value)
.Select(x => x.Key)
.Distinct()
// Filter invalid/unsupported items
.Where(x => !string.IsNullOrWhiteSpace(x) && Library.Localization.LocalizationService.ParseCulture(x) != null)
.Select(x => Library.Localization.LocalizationService.ParseCulture(x))
// And get the first that works
.FirstOrDefault();
}
public static void DoProcess(RequestInfo info, string method, string module, string key)
{
var ci = ParseRequestCulture(info);
using (Library.Localization.LocalizationService.TemporaryContext(ci))
{
try
{
if (ci != null)
info.Response.AddHeader("Content-Language", ci.Name);
IRESTMethod mod;
_modules.TryGetValue(module, out mod);
if (mod == null)
{
info.Response.Status = System.Net.HttpStatusCode.NotFound;
info.Response.Reason = "No such module";
}
else if (method == HttpServer.Method.Get && mod is IRESTMethodGET get)
{
if (info.Request.Form != HttpServer.HttpForm.EmptyForm)
{
if (info.Request.QueryString == HttpServer.HttpInput.Empty)
{
var r = info.Request.GetType().GetField("_queryString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
r.SetValue(info.Request, new HttpServer.HttpInput("formdata"));
}
foreach (HttpServer.HttpInputItem v in info.Request.Form)
if (!info.Request.QueryString.Contains(v.Name))
info.Request.QueryString.Add(v.Name, v.Value);
}
get.GET(key, info);
}
else if (method == HttpServer.Method.Put && mod is IRESTMethodPUT put)
put.PUT(key, info);
else if (method == HttpServer.Method.Post && mod is IRESTMethodPOST post)
{
if (info.Request.Form == HttpServer.HttpForm.EmptyForm || info.Request.Form == HttpServer.HttpInput.Empty)
{
var r = info.Request.GetType().GetMethod("AssignForm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new Type[] {typeof(HttpServer.HttpForm)}, null);
r.Invoke(info.Request, new object[] {new HttpServer.HttpForm(info.Request.QueryString)});
}
else
{
foreach (HttpServer.HttpInputItem v in info.Request.QueryString)
if (!info.Request.Form.Contains(v.Name))
info.Request.Form.Add(v.Name, v.Value);
}
post.POST(key, info);
}
else if (method == HttpServer.Method.Delete && mod is IRESTMethodDELETE delete)
delete.DELETE(key, info);
else if (method == "PATCH" && mod is IRESTMethodPATCH patch)
patch.PATCH(key, info);
else
{
info.Response.Status = System.Net.HttpStatusCode.MethodNotAllowed;
info.Response.Reason = "Method is not allowed";
}
}
catch (Exception ex)
{
FIXMEGlobal.DataConnection.LogError("", string.Format("Request for {0} gave error", info.Request.Uri), ex);
Console.WriteLine(ex);
try
{
if (!info.Response.HeadersSent)
{
info.Response.Status = System.Net.HttpStatusCode.InternalServerError;
info.Response.Reason = "Error";
info.Response.ContentType = "text/plain";
var wex = ex;
while (wex is System.Reflection.TargetInvocationException && wex.InnerException != wex)
wex = wex.InnerException;
info.BodyWriter.WriteJsonObject(new
{
Message = wex.Message,
Type = wex.GetType().Name,
#if DEBUG
Stacktrace = wex.ToString()
#endif
});
info.BodyWriter.Flush();
}
}
catch (Exception flex)
{
FIXMEGlobal.DataConnection.LogError("", "Reporting error gave error", flex);
}
}
}
}
public static void DoProcess(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, string method, string module, string key)
{
using(var reqinfo = new RequestInfo(request, response, session))
DoProcess(reqinfo, method, module, key);
}
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
{
if (!request.Uri.AbsolutePath.StartsWith(API_URI_PATH, StringComparison.OrdinalIgnoreCase))
return false;
var module = request.Uri.Segments.Skip(API_URI_SEGMENTS).FirstOrDefault();
if (string.IsNullOrWhiteSpace(module))
module = "help";
module = module.Trim('/');
var key = string.Join("", request.Uri.Segments.Skip(API_URI_SEGMENTS + 1)).Trim('/');
var method = request.Method;
if (!string.IsNullOrWhiteSpace(request.Headers["X-HTTP-Method-Override"]))
method = request.Headers["X-HTTP-Method-Override"];
DoProcess(request, response, session, method, module, key);
return true;
}
}
}
@@ -1,449 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using HttpServer.HttpModules;
using System.Security.Cryptography.X509Certificates;
using Duplicati.Library.Common.IO;
using Duplicati.Library.RestAPI;
namespace Duplicati.Server.WebServer
{
public class Server
{
/// <summary>
/// The tag used for logging
/// </summary>
private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType<Server>();
/// <summary>
/// Option for changing the webroot folder
/// </summary>
public const string OPTION_WEBROOT = "webservice-webroot";
/// <summary>
/// Option for changing the webservice listen port
/// </summary>
public const string OPTION_PORT = "webservice-port";
/// <summary>
/// Option for changing the webservice listen interface
/// </summary>
public const string OPTION_INTERFACE = "webservice-interface";
/// <summary>
/// The default path to the web root
/// </summary>
public const string DEFAULT_OPTION_WEBROOT = "webroot";
/// <summary>
/// The default listening port
/// </summary>
public const int DEFAULT_OPTION_PORT = 8200;
/// <summary>
/// Option for setting the webservice SSL certificate
/// </summary>
public const string OPTION_SSLCERTIFICATEFILE = "webservice-sslcertificatefile";
/// <summary>
/// Option for setting the webservice SSL certificate key
/// </summary>
public const string OPTION_SSLCERTIFICATEFILEPASSWORD = "webservice-sslcertificatepassword";
/// <summary>
/// The default listening interface
/// </summary>
public const string DEFAULT_OPTION_INTERFACE = "loopback";
/// <summary>
/// The single webserver instance
/// </summary>
private readonly HttpServer.HttpServer m_server;
/// <summary>
/// The webserver listening port
/// </summary>
public readonly int Port;
/// <summary>
/// A string that is sent out instead of password values
/// </summary>
public const string PASSWORD_PLACEHOLDER = "**********";
/// <summary>
/// Sets up the webserver and starts it
/// </summary>
/// <param name="options">A set of options</param>
public Server(IDictionary<string, string> options)
{
string portstring;
IEnumerable<int> ports = null;
options.TryGetValue(OPTION_PORT, out portstring);
if (!string.IsNullOrEmpty(portstring))
ports =
from n in portstring.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
where int.TryParse(n, out _)
select int.Parse(n);
if (ports == null || !ports.Any())
ports = new int[] { DEFAULT_OPTION_PORT };
string interfacestring;
System.Net.IPAddress listenInterface;
options.TryGetValue(OPTION_INTERFACE, out interfacestring);
if (string.IsNullOrWhiteSpace(interfacestring))
interfacestring = FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface;
if (string.IsNullOrWhiteSpace(interfacestring))
interfacestring = DEFAULT_OPTION_INTERFACE;
if (interfacestring.Trim() == "*" || interfacestring.Trim().Equals("any", StringComparison.OrdinalIgnoreCase) || interfacestring.Trim().Equals("all", StringComparison.OrdinalIgnoreCase))
listenInterface = System.Net.IPAddress.Any;
else if (interfacestring.Trim() == "loopback")
listenInterface = System.Net.IPAddress.Loopback;
else
listenInterface = System.Net.IPAddress.Parse(interfacestring);
string certificateFile;
options.TryGetValue(OPTION_SSLCERTIFICATEFILE, out certificateFile);
string certificateFilePassword;
options.TryGetValue(OPTION_SSLCERTIFICATEFILEPASSWORD, out certificateFilePassword);
X509Certificate2 cert = null;
bool certValid = false;
if (certificateFile == null)
{
try
{
cert = FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate;
if (cert != null)
certValid = cert.HasPrivateKey;
}
catch (Exception ex)
{
Duplicati.Library.Logging.Log.WriteWarningMessage(LOGTAG, "DefectStoredSSLCert", ex, Strings.Server.DefectSSLCertInDatabase);
}
}
else if (certificateFile.Length == 0)
{
FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate = null;
}
else
{
try
{
if (string.IsNullOrWhiteSpace(certificateFilePassword))
cert = new X509Certificate2(certificateFile, "", X509KeyStorageFlags.Exportable);
else
cert = new X509Certificate2(certificateFile, certificateFilePassword, X509KeyStorageFlags.Exportable);
certValid = cert.HasPrivateKey;
}
catch (Exception ex)
{
throw new Exception(Strings.Server.SSLCertificateFailure(ex.Message), ex);
}
}
// If we are in hosted mode with no specified port,
// then try different ports
foreach (var p in ports)
try
{
// Due to the way the server is initialized,
// we cannot try to start it again on another port,
// so we create a new server for each attempt
var server = CreateServer(options);
if (!certValid)
server.Start(listenInterface, p);
else
{
var secProtocols = System.Security.Authentication.SslProtocols.Tls12;
try
{
//try TLS 1.3 (type not available on .NET < 4.8)
secProtocols = System.Security.Authentication.SslProtocols.Tls12 | (System.Security.Authentication.SslProtocols)12288;
}
catch (NotSupportedException)
{
}
server.Start(listenInterface, p, cert, secProtocols, null, false);
}
m_server = server;
m_server.ServerName = string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
this.Port = p;
if (interfacestring != FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface)
FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface = interfacestring;
if (certValid && !cert.Equals(FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate))
FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate = cert;
Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "ServerListening", Strings.Server.StartedServer(listenInterface.ToString(), p));
return;
}
catch (System.Net.Sockets.SocketException)
{
}
throw new Exception(Strings.Server.ServerStartFailure(ports));
}
private static void AddMimeTypes(FileModule fm)
{
fm.AddDefaultMimeTypes();
fm.MimeTypes["htc"] = "text/x-component";
fm.MimeTypes["json"] = "application/json";
fm.MimeTypes["map"] = "application/json";
fm.MimeTypes["htm"] = "text/html; charset=utf-8";
fm.MimeTypes["html"] = "text/html; charset=utf-8";
fm.MimeTypes["hbs"] = "application/x-handlebars-template";
fm.MimeTypes["woff"] = "application/font-woff";
fm.MimeTypes["woff2"] = "application/font-woff";
}
private static HttpServer.HttpServer CreateServer(IDictionary<string, string> options)
{
HttpServer.HttpServer server = new HttpServer.HttpServer();
server.Add(new HostHeaderChecker());
if (string.Equals(Environment.GetEnvironmentVariable("SYNO_DSM_AUTH") ?? string.Empty, "1"))
server.Add(new SynologyAuthenticationHandler());
server.Add(new AuthenticationHandler());
server.Add(new RESTHandler());
string webroot = Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR;
string install_webroot = System.IO.Path.Combine(webroot, "webroot");
#if DEBUG
// Easy test for extensions while debugging
install_webroot = Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR;
if (!System.IO.Directory.Exists(System.IO.Path.Combine(webroot, "webroot")))
{
//For debug we go "../../../.." to get out of "GUI/Duplicati.GUI.TrayIcon/bin/debug"
string tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..", "..", ".."));
tmpwebroot = System.IO.Path.Combine(tmpwebroot, "Server");
if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot")))
webroot = tmpwebroot;
else
{
//If we are running the server standalone, we only need to exit "bin/Debug"
tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", ".."));
if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot")))
webroot = tmpwebroot;
}
}
#endif
webroot = System.IO.Path.Combine(webroot, "webroot");
if (options.ContainsKey(OPTION_WEBROOT))
{
string userroot = options[OPTION_WEBROOT];
#if DEBUG
//In debug mode we do not care where the path points
#else
//In release mode we check that the user supplied path is located
// in the same folders as the running application, to avoid users
// that inadvertently expose top level folders
if (!string.IsNullOrWhiteSpace(userroot)
&& userroot.StartsWith(Util.AppendDirSeparator(Duplicati.Library.Utility.Utility.getEntryAssembly().Location), Library.Utility.Utility.ClientFilenameStringComparison))
#endif
{
webroot = userroot;
install_webroot = webroot;
}
}
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "customized")))
{
var customized_files = new CacheControlFileHandler("/customized/", System.IO.Path.Combine(install_webroot, "customized"));
AddMimeTypes(customized_files);
server.Add(customized_files);
}
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "oem")))
{
var oem_files = new CacheControlFileHandler("/oem/", System.IO.Path.Combine(install_webroot, "oem"));
AddMimeTypes(oem_files);
server.Add(oem_files);
}
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "package")))
{
var proxy_files = new CacheControlFileHandler("/proxy/", System.IO.Path.Combine(install_webroot, "package"));
AddMimeTypes(proxy_files);
server.Add(proxy_files);
}
var fh = new CacheControlFileHandler("/", webroot, true);
AddMimeTypes(fh);
server.Add(fh);
server.Add(new IndexHtmlHandler(webroot));
#if DEBUG
//For debugging, it is nice to know when we get a 404
server.Add(new DebugReportHandler());
#endif
return server;
}
private class DebugReportHandler : HttpModule
{
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
{
System.Diagnostics.Trace.WriteLine(string.Format("Rejecting request for {0}", request.Uri));
return false;
}
}
private class CacheControlFileHandler : FileModule
{
public CacheControlFileHandler(string baseUri, string basePath, bool useLastModifiedHeader = false)
: base(baseUri, basePath, useLastModifiedHeader)
{
}
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
{
if (!this.CanHandle(request.Uri))
return false;
if (request.Uri.AbsolutePath.EndsWith("index.html", StringComparison.Ordinal) || request.Uri.AbsolutePath.EndsWith("index.htm", StringComparison.Ordinal))
response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
else
response.AddHeader("Cache-Control", "max-age=" + (60 * 60 * 24));
return base.Process(request, response, session);
}
}
/// <summary>
/// Module for injecting host header verification
/// </summary>
private class HostHeaderChecker : HttpModule
{
/// <summary>
/// The hostnames that we allow
/// </summary>
private string[] m_lastSplitNames;
/// <summary>
/// The string used to generate m_lastSplitNames;
/// </summary>
private string m_lastAllowed;
/// <summary>
/// A regex to detect potential IPv4 addresses.
/// Note that this also detects things that are not valid IPv4.
/// </summary>
private static readonly System.Text.RegularExpressions.Regex IPV4 = new System.Text.RegularExpressions.Regex(@"((\d){1,3}\.){3}(\d){1,3}");
/// <summary>
/// A regex to detect potential IPv6 addresses.
/// Note that this also detects things that are not valid IPv6.
/// </summary>
private static readonly System.Text.RegularExpressions.Regex IPV6 = new System.Text.RegularExpressions.Regex(@"(\:)?(\:?[A-Fa-f0-9]{1,4}\:?){1,8}(\:)?");
/// <summary>
/// The hostnames that are always allowed
/// </summary>
private static readonly string[] DEFAULT_ALLOWED = new string[] { "localhost", "127.0.0.1", "::1", "localhost.localdomain" };
/// <summary>
/// Process the received request
/// </summary>
/// <returns>A flag indicating if the request is handled.</returns>
/// <param name="request">The received request.</param>
/// <param name="response">The response object.</param>
/// <param name="session">The session state.</param>
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
{
string[] h = null;
var hstring = FIXMEGlobal.DataConnection.ApplicationSettings.AllowedHostnames;
if (!string.IsNullOrWhiteSpace(hstring))
{
h = m_lastSplitNames;
if (hstring != m_lastAllowed)
{
m_lastAllowed = hstring;
h = m_lastSplitNames = (hstring ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
}
if (h == null || h.Length == 0)
h = null;
}
// For some reason, the web server strips out the host header
var host = request.Headers["Host"];
if (string.IsNullOrWhiteSpace(host))
host = request.Uri.Host;
// This should not happen
if (string.IsNullOrWhiteSpace(host))
{
response.Reason = "Invalid request, missing host header";
response.Status = System.Net.HttpStatusCode.Forbidden;
var msg = System.Text.Encoding.ASCII.GetBytes(response.Reason);
response.ContentType = "text/plain";
response.ContentLength = msg.Length;
response.Body.Write(msg, 0, msg.Length);
response.Send();
return true;
}
// Check the hostnames we always allow
if (DEFAULT_ALLOWED.Contains(host, StringComparer.OrdinalIgnoreCase))
return false;
// Then the user specified ones
if (h != null && h.Contains(host, StringComparer.OrdinalIgnoreCase))
return false;
// Disable checks if we have an asterisk
if (h != null && Array.IndexOf(h, "*") >= 0)
return false;
// Finally, check if we have a potential IP address
var v4 = IPV4.Match(host);
var v6 = IPV6.Match(host);
if ((v4.Success && v4.Length == host.Length) || (v6.Success && v6.Length == host.Length))
{
try
{
// Verify that the hostname is indeed a valid IP address
System.Net.IPAddress.Parse(host);
return false;
}
catch
{ }
}
// Failed to find a valid header
response.Reason = $"The host header sent by the client is not allowed";
response.Status = System.Net.HttpStatusCode.Forbidden;
var txt = System.Text.Encoding.ASCII.GetBytes(response.Reason);
response.ContentType = "text/plain";
response.ContentLength = txt.Length;
response.Body.Write(txt, 0, txt.Length);
response.Send();
return true;
}
}
}
}
@@ -1,293 +0,0 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// 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
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using HttpServer.HttpModules;
namespace Duplicati.Server.WebServer
{
/// <summary>
/// Helper class for enforcing the built-in authentication on Synology DSM
/// </summary>
public class SynologyAuthenticationHandler : HttpModule
{
/// <summary>
/// The path to the login.cgi script
/// </summary>
private readonly string LOGIN_CGI = GetEnvArg("SYNO_LOGIN_CGI", "/usr/syno/synoman/webman/login.cgi");
/// <summary>
/// The path to the authenticate.cgi script
/// </summary>
private readonly string AUTH_CGI = GetEnvArg("SYNO_AUTHENTICATE_CGI", "/usr/syno/synoman/webman/modules/authenticate.cgi");
/// <summary>
/// A flag indicating if only admins are allowed
/// </summary>
private readonly bool ADMIN_ONLY = !(GetEnvArg("SYNO_ALL_USERS", "0") == "1");
/// <summary>
/// A flag indicating if the XSRF token should be fetched automatically
/// </summary>
private readonly bool AUTO_XSRF = GetEnvArg("SYNO_AUTO_XSRF", "1") == "1";
/// <summary>
/// A flag indicating that the auth-module is fully disabled
/// </summary>
private readonly bool FULLY_DISABLED;
/// <summary>
/// Re-evaluate the logins periodically to ensure it is still valid
/// </summary>
private readonly TimeSpan CACHE_TIMEOUT = TimeSpan.FromMinutes(3);
/// <summary>
/// A cache of previously authenticated logins
/// </summary>
private readonly ConcurrentDictionary<string, DateTime> m_logincache = new ConcurrentDictionary<string, DateTime>();
/// <summary>
/// Initializes a new instance of the <see cref="T:Duplicati.Server.WebServer.SynologyAuthenticationHandler"/> class.
/// </summary>
public SynologyAuthenticationHandler()
{
Console.WriteLine("Enabling Synology integrated authentication handler");
var disable = false;
if (!File.Exists(LOGIN_CGI))
{
Console.WriteLine("Disabling webserver as the login script is not found: {0}", LOGIN_CGI);
disable = true;
}
if (!File.Exists(AUTH_CGI))
{
Console.WriteLine("Disabling webserver as the auth script is not found: {0}", AUTH_CGI);
disable = true;
}
FULLY_DISABLED = disable;
}
/// <summary>
/// Processes the request
/// </summary>
/// <returns><c>true</c> if the request is handled <c>false</c> otherwise.</returns>
/// <param name="request">The request.</param>
/// <param name="response">The response.</param>
/// <param name="session">The session.</param>
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
{
if (FULLY_DISABLED)
{
response.Status = System.Net.HttpStatusCode.ServiceUnavailable;
response.Reason = "The system is incorrectly configured";
return true;
}
var limitedAccess =
request.Uri.AbsolutePath.StartsWith(RESTHandler.API_URI_PATH, StringComparison.OrdinalIgnoreCase)
||
request.Uri.AbsolutePath.StartsWith(AuthenticationHandler.LOGIN_SCRIPT_URI, StringComparison.OrdinalIgnoreCase)
||
request.Uri.AbsolutePath.StartsWith(AuthenticationHandler.LOGOUT_SCRIPT_URI, StringComparison.OrdinalIgnoreCase);
if (!limitedAccess)
return false;
var tmpenv = new Dictionary<string, string>();
tmpenv["REMOTE_ADDR"] = request.RemoteEndPoint.Address.ToString();
tmpenv["REMOTE_PORT"] = request.RemoteEndPoint.Port.ToString();
if (!string.IsNullOrWhiteSpace(request.Headers["X-Real-IP"]))
tmpenv["REMOTE_ADDR"] = request.Headers["X-Real-IP"];
if (!string.IsNullOrWhiteSpace(request.Headers["X-Real-IP"]))
tmpenv["REMOTE_PORT"] = request.Headers["X-Real-Port"];
var loginid = request.Cookies["id"]?.Value;
if (!string.IsNullOrWhiteSpace(loginid))
tmpenv["HTTP_COOKIE"] = "id=" + loginid;
var xsrftoken = request.Headers["X-Syno-Token"];
if (string.IsNullOrWhiteSpace(xsrftoken))
xsrftoken = request.QueryString["SynoToken"]?.Value;
var cachestring = BuildCacheKey(tmpenv, xsrftoken);
DateTime cacheExpires;
if (m_logincache.TryGetValue(cachestring, out cacheExpires) && cacheExpires > DateTime.Now)
{
// We do not refresh the cache, as we need to ask the synology auth system periodically
return false;
}
if (string.IsNullOrWhiteSpace(xsrftoken) && AUTO_XSRF)
{
var authre = new Regex(@"""SynoToken""\s?\:\s?""(?<token>[^""]+)""");
try
{
var resp = ShellExec(LOGIN_CGI, env: tmpenv).Result;
var m = authre.Match(resp);
if (m.Success)
xsrftoken = m.Groups["token"].Value;
else
throw new Exception("Unable to get XSRF token");
}
catch (Exception)
{
response.Status = System.Net.HttpStatusCode.InternalServerError;
response.Reason = "The system is incorrectly configured";
return true;
}
}
if (!string.IsNullOrWhiteSpace(xsrftoken))
tmpenv["HTTP_X_SYNO_TOKEN"] = xsrftoken;
cachestring = BuildCacheKey(tmpenv, xsrftoken);
var username = GetEnvArg("SYNO_USERNAME");
if (string.IsNullOrWhiteSpace(username))
{
try
{
username = ShellExec(AUTH_CGI, shell: false, exitcode: 0, env: tmpenv).Result;
}
catch (Exception)
{
response.Status = System.Net.HttpStatusCode.InternalServerError;
response.Reason = "The system is incorrectly configured";
return true;
}
}
if (string.IsNullOrWhiteSpace(username))
{
response.Status = System.Net.HttpStatusCode.Forbidden;
response.Reason = "Permission denied, not logged in";
return true;
}
username = username.Trim();
if (ADMIN_ONLY)
{
var groups = GetEnvArg("SYNO_GROUP_IDS");
if (string.IsNullOrWhiteSpace(groups))
{
groups = ShellExec("id", "-G '" + username.Trim().Replace("'", "\\'") + "'", exitcode: 0).Result ?? string.Empty;
groups = groups.Replace(Environment.NewLine, String.Empty);
}
if (!groups.Split(new char[] { ' ' }).Contains("101"))
{
response.Status = System.Net.HttpStatusCode.Forbidden;
response.Reason = "Administrator login required";
return true;
}
}
// We are now authenticated, add to cache
m_logincache[cachestring] = DateTime.Now + CACHE_TIMEOUT;
return false;
}
/// <summary>
/// Builds a cache key from the environment data
/// </summary>
/// <returns>The cache key.</returns>
/// <param name="values">The environment.</param>
/// <param name="xsrftoken">The XSRF token.</param>
private static string BuildCacheKey(Dictionary<string, string> values, string xsrftoken)
{
if (!values.ContainsKey("REMOTE_ADDR") || !values.ContainsKey("REMOTE_PORT") || !values.ContainsKey("HTTP_COOKIE"))
return null;
return string.Format("{0}:{1}/{2}?{3}", values["REMOTE_ADDR"], values["REMOTE_PORT"], values["HTTP_COOKIE"], xsrftoken);
}
/// <summary>
/// Runs an external command
/// </summary>
/// <returns>The stdout data.</returns>
/// <param name="command">The executable</param>
/// <param name="args">The executable and the arguments.</param>
/// <param name="shell">If set to <c>true</c> use the shell context for execution.</param>
/// <param name="exitcode">Set the value to check for a particular exitcode.</param>
private static async Task<string> ShellExec(string command, string args = null, bool shell = false, int exitcode = -1, Dictionary<string, string> env = null)
{
var psi = new ProcessStartInfo()
{
FileName = command,
Arguments = shell ? null : args,
UseShellExecute = false,
RedirectStandardInput = shell,
RedirectStandardOutput = true,
RedirectStandardError = false
};
if (env != null)
foreach (var pk in env)
psi.EnvironmentVariables[pk.Key] = pk.Value;
using (var p = System.Diagnostics.Process.Start(psi))
{
if (shell && args != null)
await p.StandardInput.WriteLineAsync(args);
var res = p.StandardOutput.ReadToEndAsync();
var tries = 10;
var ms = (int)TimeSpan.FromSeconds(0.5).TotalMilliseconds;
while (tries > 0 && !p.HasExited)
{
tries--;
p.WaitForExit(ms);
}
if (!p.HasExited)
try { p.Kill(); }
catch { }
if (!p.HasExited || (p.ExitCode != exitcode && exitcode != -1))
throw new Exception(string.Format("Exit code was: {0}, stdout: {1}", p.ExitCode, res));
return await res;
}
}
/// <summary>
/// Gets the environment variable argument.
/// </summary>
/// <returns>The environment variable.</returns>
/// <param name="key">The name of the environment variable.</param>
/// <param name="default">The default value.</param>
private static string GetEnvArg(string key, string @default = null)
{
var res = Environment.GetEnvironmentVariable(key);
return string.IsNullOrWhiteSpace(res) ? @default : res.Trim();
}
}
}
+14 -14
View File
@@ -1,15 +1,15 @@
{
Backup: {
Settings: [
{Name: "encryption-module", Value: "aes"},
{Name: "compression-module", Value: "zip"},
{Name: "dblock-size", Value: "50mb"},
{Name: "keep-time", Value: ""}
]
},
Schedule: {
Repeat: "1D",
Time: "13:00:00",
AllowedDays: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
}
}
"Backup": {
"Settings": [
{ "Name": "encryption-module", "Value": "aes" },
{ "Name": "compression-module", "Value": "zip" },
{ "Name": "dblock-size", "Value": "50mb" },
{ "Name": "keep-time", "Value": "" }
]
},
"Schedule": {
"Repeat": "1D",
"Time": "13:00:00",
"AllowedDays": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
}
}
+15 -1
View File
@@ -109,7 +109,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "net8", "net8", "{6B46F6B1-1
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.AutoUpdater", "Executables\net8\Duplicati.CommandLine.AutoUpdater\Duplicati.CommandLine.AutoUpdater.csproj", "{95B7DD83-2C5A-4F1E-8EA7-39654B2B236A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Service", "Executables\net8\Duplicati_Service\Duplicati.Service.csproj", "{34149709-F3ED-4FB5-A087-43EB195C948B}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Service", "Executables\net8\Duplicati.Service\Duplicati.Service.csproj", "{34149709-F3ED-4FB5-A087-43EB195C948B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.BackendTester", "Executables\net8\Duplicati.CommandLine.BackendTester\Duplicati.CommandLine.BackendTester.csproj", "{2F1C0C8D-5C15-4BC0-811F-87F2C98D9790}"
EndProject
@@ -148,6 +148,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.RestAPI",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.AliyunOSS", "Duplicati\Library\Backend\AliyunOSS\Duplicati.Library.Backend.AliyunOSS.csproj", "{4EB3DABC-D412-4C12-8876-41A1427A389E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.SharpAESCrypt", "Executables\net8\Duplicati.CommandLine.SharpAESCrypt\Duplicati.CommandLine.SharpAESCrypt.csproj", "{FE6FD36C-E171-4599-8D55-62DA579C0864}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.Snapshots", "Executables\net8\Duplicati.CommandLine.Snapshots\Duplicati.CommandLine.Snapshots.csproj", "{0364E724-1929-445E-9145-90A70B01DDC0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -414,6 +418,14 @@ Global
{C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}.Release|Any CPU.Build.0 = Release|Any CPU
{FE6FD36C-E171-4599-8D55-62DA579C0864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FE6FD36C-E171-4599-8D55-62DA579C0864}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FE6FD36C-E171-4599-8D55-62DA579C0864}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FE6FD36C-E171-4599-8D55-62DA579C0864}.Release|Any CPU.Build.0 = Release|Any CPU
{0364E724-1929-445E-9145-90A70B01DDC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0364E724-1929-445E-9145-90A70B01DDC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0364E724-1929-445E-9145-90A70B01DDC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0364E724-1929-445E-9145-90A70B01DDC0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -467,6 +479,8 @@ Global
{0F5A1F4E-25FA-4D02-920D-CA2138498081} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{6B594D23-B629-465C-B799-70EE9E56C218} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
{D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B} = {FA88A246-EF8E-46E3-90AF-539B8C0A6ADE}
{FE6FD36C-E171-4599-8D55-62DA579C0864} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{0364E724-1929-445E-9145-90A70B01DDC0} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8B40BAFE-D862-4397-9495-8F5EAF5CE80C}
@@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>Duplicati.CommandLine.AutoUpdater.Implementation</AssemblyName>
<RootNamespace>Duplicati.CommandLine.AutoUpdater</RootNamespace>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
</PropertyGroup>
<ItemGroup>
@@ -20,3 +21,4 @@
</ItemGroup>
</Project>
@@ -1,37 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Description>A backend debugging tool for Duplicati</Description>
<AssemblyName>Duplicati.CommandLine.BackendTester.Implementation</AssemblyName>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Library\Backends\Duplicati.Library.Backends.csproj" />
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Interface\Duplicati.Library.Interface.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj">
</ProjectReference>
<ProjectReference Include="..\..\License\Duplicati.License.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Modules\Builtin\Duplicati.Library.Modules.Builtin.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Backend\OAuthHelper\Duplicati.Library.OAuthHelper.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Localization\Duplicati.Library.Localization.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Logging\Duplicati.Library.Logging.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Common\Duplicati.Library.Common.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Compression\Duplicati.Library.Compression.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\SQLiteHelper\Duplicati.Library.SQLiteHelper.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj" />
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj" />
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj" />
<ProjectReference Include="..\..\License\Duplicati.License.csproj" />
<ProjectReference Include="..\..\Library\Modules\Builtin\Duplicati.Library.Modules.Builtin.csproj" />
<ProjectReference Include="..\..\Library\Backend\OAuthHelper\Duplicati.Library.OAuthHelper.csproj" />
<ProjectReference Include="..\..\Library\Localization\Duplicati.Library.Localization.csproj" />
<ProjectReference Include="..\..\Library\Logging\Duplicati.Library.Logging.csproj" />
<ProjectReference Include="..\..\Library\Common\Duplicati.Library.Common.csproj" />
<ProjectReference Include="..\..\Library\SQLiteHelper\Duplicati.Library.SQLiteHelper.csproj" />
</ItemGroup>
<ItemGroup>
@@ -42,3 +28,4 @@
</ItemGroup>
</Project>
@@ -1,22 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>Duplicati.CommandLine.BackendTool.Implementation</AssemblyName>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Library\Backends\Duplicati.Library.Backends.csproj" />
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Interface\Duplicati.Library.Interface.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj">
</ProjectReference>
<ProjectReference Include="..\..\License\Duplicati.License.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj" />
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj" />
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj" />
<ProjectReference Include="..\..\License\Duplicati.License.csproj" />
</ItemGroup>
<ItemGroup>
@@ -27,3 +21,4 @@
</ItemGroup>
</Project>
+18 -16
View File
@@ -134,13 +134,15 @@ namespace Duplicati.CommandLine
public static int Examples(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
{
Duplicati.CommandLine.Help.PrintUsage(outwriter, "example", options);
using (new Duplicati.Library.Main.LocaleChange(options))
Duplicati.CommandLine.Help.PrintUsage(outwriter, "example", options);
return 0;
}
public static int Help(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
{
Duplicati.CommandLine.Help.PrintUsage(outwriter, args.Count > 1? args[1] : "help", options);
using (new Duplicati.Library.Main.LocaleChange(options))
Duplicati.CommandLine.Help.PrintUsage(outwriter, args.Count > 1 ? args[1] : "help", options);
return 0;
}
@@ -175,8 +177,8 @@ namespace Duplicati.CommandLine
options["no-encryption"] = "true";
}
using(var console = new ConsoleOutput(outwriter, options))
using(var i = new Library.Main.Controller(backend, options, console))
using (var console = new ConsoleOutput(outwriter, options))
using (var i = new Library.Main.Controller(backend, options, console))
{
setup(i);
i.ListAffected(args, res =>
@@ -259,7 +261,7 @@ namespace Duplicati.CommandLine
if (!containsSeparators && arg.StartsWith("@", StringComparison.Ordinal))
{
// Convert to Regexp filter and prefix with ".*/"
return $"[.*{Utility.ConvertLiteralToRegExp(Util.DirectorySeparatorString + arg.Substring(1))}]";
return $"[.*{Library.Utility.Utility.ConvertLiteralToRegExp(Util.DirectorySeparatorString + arg.Substring(1))}]";
}
else if (!containsSeparators && !containsWildcards && !arg.StartsWith("[", StringComparison.Ordinal))
{
@@ -290,7 +292,7 @@ namespace Duplicati.CommandLine
if (endsWithSeparator && arg.StartsWith("@", StringComparison.Ordinal))
{
// Convert to Regexp filter and suffix with ".*"
return $"[{Utility.ConvertLiteralToRegExp(arg.Substring(1))}.*]";
return $"[{Library.Utility.Utility.ConvertLiteralToRegExp(arg.Substring(1))}.*]";
}
else if (endsWithSeparator && !containsWildcards && !arg.StartsWith("[", StringComparison.Ordinal))
{
@@ -324,7 +326,7 @@ namespace Duplicati.CommandLine
filter = new Duplicati.Library.Utility.FilterExpression();
using (var console = new ConsoleOutput(outwriter, options))
using(var i = new Library.Main.Controller(args[0], options, console))
using (var i = new Library.Main.Controller(args[0], options, console))
{
setup(i);
var backend = args[0];
@@ -412,7 +414,7 @@ namespace Duplicati.CommandLine
{
outwriter.WriteLine("Listing filesets:");
foreach(var e in res.Filesets)
foreach (var e in res.Filesets)
{
if (e.FileCount >= 0)
outwriter.WriteLine("{0}\t: {1} ({2} files, {3})", e.Version, e.Time, e.FileCount, Library.Utility.Utility.FormatSizeString(e.FileSizes));
@@ -444,10 +446,10 @@ namespace Duplicati.CommandLine
else
{
outwriter.WriteLine("Listing files and versions:");
foreach(var e in res.Files)
foreach (var e in res.Files)
{
outwriter.WriteLine(e.Path);
foreach(var nx in res.Filesets.Zip(e.Sizes, (a, b) => new { Index = a.Version, Time = a.Time, Size = b } ))
foreach (var nx in res.Filesets.Zip(e.Sizes, (a, b) => new { Index = a.Version, Time = a.Time, Size = b }))
outwriter.WriteLine("{0}\t: {1} {2}", nx.Index, nx.Time, nx.Size < 0 ? " - " : Library.Utility.Utility.FormatSizeString(nx.Size));
outwriter.WriteLine();
@@ -471,7 +473,7 @@ namespace Duplicati.CommandLine
}
using (var console = new ConsoleOutput(outwriter, options))
using(var i = new Library.Main.Controller(args[0], options, console))
using (var i = new Library.Main.Controller(args[0], options, console))
{
setup(i);
args.RemoveAt(0);
@@ -488,7 +490,7 @@ namespace Duplicati.CommandLine
else
outwriter.WriteLine(Strings.Program.DeletedBackups);
foreach(var f in res.DeletedSets)
foreach (var f in res.DeletedSets)
outwriter.WriteLine("{0}: {1}", f.Item1, f.Item2);
}
}
@@ -525,7 +527,7 @@ namespace Duplicati.CommandLine
args = SuffixArgsWithAsterisk(PrefixArgsWithAsterisk(args)).ToList();
using(var output = new ConsoleOutput(outwriter, options))
using (var output = new ConsoleOutput(outwriter, options))
using (var i = new Library.Main.Controller(backend, options, output))
{
output.MessageEvent(string.Format("Restore started at {0}", DateTime.Now));
@@ -717,7 +719,7 @@ namespace Duplicati.CommandLine
if (args.Count != 1)
return PrintWrongNumberOfArguments(outwriter, args, 1);
using(var console = new ConsoleOutput(outwriter, options))
using (var console = new ConsoleOutput(outwriter, options))
using (var i = new Library.Main.Controller(args[0], options, console))
{
setup(i);
@@ -766,7 +768,7 @@ namespace Duplicati.CommandLine
}
else
{
foreach(var n in result.Verifications)
foreach (var n in result.Verifications)
{
var changecount = n.Value.Count();
if (changecount == 0)
@@ -1001,7 +1003,7 @@ namespace Duplicati.CommandLine
}
using (var console = new ConsoleOutput(outwriter, options))
using(var i = new Library.Main.Controller("dummy://", options, console))
using (var i = new Library.Main.Controller("dummy://", options, console))
{
setup(i);
var result = i.TestFilter(args.ToArray(), filter);
@@ -1,48 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Description>A commandline version of Duplicati</Description>
<AssemblyName>Duplicati.CommandLine.Implementation</AssemblyName>
<ApplicationIcon>TrayWarning.ico</ApplicationIcon>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Library\Backends\Duplicati.Library.Backends.csproj" />
<ProjectReference Include="..\..\Library\Compression\Duplicati.Library.Compression.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Common\Duplicati.Library.Common.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Encryption\Duplicati.Library.Encryption.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Interface\Duplicati.Library.Interface.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Logging\Duplicati.Library.Logging.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Main\Duplicati.Library.Main.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Modules\Builtin\Duplicati.Library.Modules.Builtin.csproj">
</ProjectReference>
<ProjectReference Include="..\..\License\Duplicati.License.csproj">
</ProjectReference>
<ProjectReference Include="..\BackendTool\Duplicati.CommandLine.BackendTool.csproj">
</ProjectReference>
<ProjectReference Include="..\BackendTester\Duplicati.CommandLine.BackendTester.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Localization\Duplicati.Library.Localization.csproj">
</ProjectReference>
<ProjectReference Include="..\RecoveryTool\Duplicati.CommandLine.RecoveryTool.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\UsageReporter\Duplicati.Library.UsageReporter.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Tools\Duplicati.Tools.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj" />
<ProjectReference Include="..\..\Library\Common\Duplicati.Library.Common.csproj" />
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj" />
<ProjectReference Include="..\..\Library\Logging\Duplicati.Library.Logging.csproj" />
<ProjectReference Include="..\..\Library\Main\Duplicati.Library.Main.csproj" />
<ProjectReference Include="..\..\License\Duplicati.License.csproj" />
<ProjectReference Include="..\BackendTool\Duplicati.CommandLine.BackendTool.csproj" />
<ProjectReference Include="..\BackendTester\Duplicati.CommandLine.BackendTester.csproj" />
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj" />
<ProjectReference Include="..\..\Library\Localization\Duplicati.Library.Localization.csproj" />
<ProjectReference Include="..\RecoveryTool\Duplicati.CommandLine.RecoveryTool.csproj" />
<ProjectReference Include="..\..\Library\UsageReporter\Duplicati.Library.UsageReporter.csproj" />
<ProjectReference Include="..\..\Tools\Duplicati.Tools.csproj" />
</ItemGroup>
<ItemGroup>
@@ -56,4 +35,4 @@
</PackageReference>
</ItemGroup>
</Project>
</Project>
+20 -20
View File
@@ -33,18 +33,17 @@ namespace Duplicati.CommandLine
public static class Help
{
private static readonly Dictionary<string, string> _document;
private const string RESOURCE_NAME = "Duplicati.CommandLine.help.txt";
private const string RESOURCE_NAME = "help.txt";
private static readonly System.Text.RegularExpressions.Regex NAMEDOPTION_REGEX = new System.Text.RegularExpressions.Regex("\\%OPTION\\:(?<name>[^\\%]+)\\%");
static Help()
{
_document = new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
using (System.IO.StreamReader sr = new System.IO.StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(RESOURCE_NAME)))
_document = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
using (var sr = new StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(Help), RESOURCE_NAME)))
{
List<string> keywords = new List<string>();
StringBuilder sb = new StringBuilder();
foreach(var line in sr.ReadToEnd().Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.None))
foreach (var line in sr.ReadToEnd().Split(new string[] { "\r\n", "\n", "\r" }, StringSplitOptions.None))
{
if (line.Trim().StartsWith("#", StringComparison.Ordinal))
continue;
@@ -54,14 +53,14 @@ namespace Duplicati.CommandLine
if (sb.Length > 0)
{
string s = sb.ToString();
foreach(var k in keywords)
foreach (var k in keywords)
_document[k] = s;
keywords.Clear();
sb.Clear();
}
string[] elems = line.Split(new string[] {" ", "\t"}, StringSplitOptions.RemoveEmptyEntries);
string[] elems = line.Split(new string[] { " ", "\t" }, StringSplitOptions.RemoveEmptyEntries);
if (elems.Length >= 2 && string.Equals(elems[elems.Length - 2], "help", StringComparison.OrdinalIgnoreCase))
keywords.Add(elems[elems.Length - 1]);
else if (elems.Length == 3 && string.Equals(elems[elems.Length - 1], "help", StringComparison.OrdinalIgnoreCase))
@@ -76,7 +75,7 @@ namespace Duplicati.CommandLine
if (sb.Length > 0)
{
string s = sb.ToString();
foreach(var k in keywords)
foreach (var k in keywords)
_document[k] = s;
}
}
@@ -116,13 +115,14 @@ namespace Duplicati.CommandLine
string tp = _document[topic];
Library.Main.Options opts = new Library.Main.Options(new Dictionary<string, string>());
tp = tp.Replace("%CLI_EXE%", PackageHelper.GetExecutableName(PackageHelper.NamedExecutable.CommandLine));
tp = tp.Replace("%VERSION%", License.VersionNumbers.Version);
tp = tp.Replace("%BACKENDS%", string.Join(", ", Library.DynamicLoader.BackendLoader.Keys));
tp = tp.Replace("%APP_PATH%", Path.Combine(UpdaterManager.INSTALLATIONDIR, PackageHelper.GetExecutableName(PackageHelper.NamedExecutable.CommandLine)));
tp = tp.Replace("%PATH_SEPARATOR%", System.IO.Path.PathSeparator.ToString());
tp = tp.Replace("%EXAMPLE_SOURCE_PATH%", Platform.IsClientPosix ? "/source" : @"D:\source");
tp = tp.Replace("%EXAMPLE_SOURCE_FILE%", Platform.IsClientPosix ? "/source/myfile.txt" : @"D:\source\file.txt");
tp = tp.Replace("%EXAMPLE_RESTORE_PATH%", Platform.IsClientPosix ? "/restore" : @"D:\restore");
tp = tp.Replace("%EXAMPLE_SOURCE_PATH%", !OperatingSystem.IsWindows() ? "/source" : @"D:\source");
tp = tp.Replace("%EXAMPLE_SOURCE_FILE%", !OperatingSystem.IsWindows() ? "/source/myfile.txt" : @"D:\source\file.txt");
tp = tp.Replace("%EXAMPLE_RESTORE_PATH%", !OperatingSystem.IsWindows() ? "/restore" : @"D:\restore");
tp = tp.Replace("%ENCRYPTIONMODULES%", string.Join(", ", Library.DynamicLoader.EncryptionLoader.Keys));
tp = tp.Replace("%COMPRESSIONMODULES%", string.Join(", ", Library.DynamicLoader.CompressionLoader.Keys));
tp = tp.Replace("%DEFAULTENCRYPTIONMODULE%", opts.EncryptionModule);
@@ -132,7 +132,7 @@ namespace Duplicati.CommandLine
tp = tp.Replace("%FILTER_GROUPS_SHORT%", string.Join(Environment.NewLine + " ", metaGroupNames.Concat(Enum.GetNames(typeof(FilterGroup)).Except(metaGroupNames, StringComparer.OrdinalIgnoreCase).OrderBy(x => x, StringComparer.OrdinalIgnoreCase)).Select(group => "{" + group + "}")));
tp = tp.Replace("%FILTER_GROUPS_LONG%", Library.Utility.FilterGroups.GetOptionDescriptions(4, true));
if (Platform.IsClientWindows)
if (OperatingSystem.IsWindows())
{
// These properties are only valid for Windows
tp = tp.Replace("%EXAMPLE_WILDCARD_DRIVE_SOURCE_PATH%", @"*:\source");
@@ -205,7 +205,7 @@ namespace Duplicati.CommandLine
tp = tp.Replace("%ALLOPTIONS%", string.Join(Environment.NewLine, lines.ToArray()));
}
if (tp.Contains("%MODULEOPTIONS%"))
{
//Figure out which module we are in
@@ -261,7 +261,7 @@ namespace Duplicati.CommandLine
}
if (NAMEDOPTION_REGEX.IsMatch(tp))
tp = NAMEDOPTION_REGEX.Replace(tp, new Matcher().MathEvaluator);
tp = NAMEDOPTION_REGEX.Replace(tp, new Matcher().MathEvaluator);
PrintFormatted(outwriter, tp.Split(new string[] { Environment.NewLine }, StringSplitOptions.None));
}
@@ -398,15 +398,15 @@ namespace Duplicati.CommandLine
private static void PrintFormatted(TextWriter outwriter, IEnumerable<string> lines)
{
int windowWidth = 80;
try
try
{
// This can go wrong if we have no attached console
if (outwriter == Console.Out)
windowWidth = Math.Max(12, Console.WindowWidth == 0 ? 80 : Console.WindowWidth);
windowWidth = Math.Max(12, Console.WindowWidth == 0 ? 80 : Console.WindowWidth);
}
catch { }
foreach (string s in lines)
{
if (string.IsNullOrEmpty(s) || s.Trim().Length == 0)
@@ -472,7 +472,7 @@ namespace Duplicati.CommandLine
foreach (Duplicati.Library.Interface.IGenericModule mod in Library.DynamicLoader.GenericLoader.Modules)
if (mod.SupportedCommands != null)
foundArgs.Add(mod.SupportedCommands);
foreach (IEnumerable<Library.Interface.ICommandLineArgument> arglst in foundArgs)
if (arglst != null)
{
@@ -510,7 +510,7 @@ namespace Duplicati.CommandLine
return "";
List<string> lines = new List<string>();
foreach(Duplicati.Library.Interface.ICommandLineArgument arg in args)
foreach (Duplicati.Library.Interface.ICommandLineArgument arg in args)
if (!arg.Deprecated)
lines.Add(PrintArgSimple(arg, arg.Name));
+7 -6
View File
@@ -20,8 +20,10 @@
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.Localization.Short;
namespace Duplicati.CommandLine.Strings {
internal static class Program {
namespace Duplicati.CommandLine.Strings
{
internal static class Program
{
public static string DeleteCommandNeedsOptions(string commandname, string[] options) { return LC.L(@"The command {0} needs at least one of the following options set: {1}", commandname, string.Join(", ", options)); }
public static string WrongNumberOfCommandsError_v2(int actualcommands, int expectedcommands, string[] commands) { return LC.L(@"Found {0} commands but expected {1}, commands:
{2}", actualcommands, expectedcommands, string.Join(System.Environment.NewLine, commands ?? new string[0])); }
@@ -47,13 +49,12 @@ namespace Duplicati.CommandLine.Strings {
public static string IncludeShort { get { return LC.L(@"Include files"); } }
public static string ExcludeLong { get { return LC.L(@"Exclude files that match this filter. The special character * means any number of character, and the special character ? means any single character, use *.txt to exclude all files with a txt extension. Regular expressions are also supported and can be supplied by using hard braces, i.e. [.*\.txt]. Filter groups (which encapsulate a built-in set of well-known files and folders) can be specified by using curly braces, i.e. {{TemporaryFiles}}."); } }
public static string ExcludeShort { get { return LC.L(@"Exclude files"); } }
public static string ControlFilesOptionShort { get { return LC.L(@"If this option is used with a backup operation, it is interpreted as a list of files to add to the filesets. When used with list or restore, it will list or restore the control files instead of the normal files."); } }
public static string ControlFilesOptionLong { get { return LC.L(@"Use control files"); } }
public static string ControlFilesOptionShort { get { return LC.L(@"Use control files"); } }
public static string ControlFilesOptionLong { get { return LC.L(@"If this option is used with a backup operation, it is interpreted as a list of files to add to the filesets. When used with list or restore, it will list or restore the control files instead of the normal files."); } }
public static string QuietConsoleOptionLong { get { return LC.L(@"If this option is set, progress reports and other messages that would normally go to the console will be redirected to the log."); } }
public static string QuietConsoleOptionShort { get { return LC.L(@"Disable console output"); } }
public static string SkippingSourceArgumentsOnNonBackupOperation { get { return @"The --source argument was specified in the parameter file, but the current operation is not a backup operation, so the argument is ignored"; } }
// ReSharper disable once UnusedMember.Global
// This is a placeholder message that is intended to be used with the code
// for each error and log message. The idea is that the commandline will
+26 -17
View File
@@ -9,14 +9,14 @@
> duplicati.commandline.exe
> duplicati.commandline.exe help
See duplicati.commandline.exe help <topic> for more information.
See %CLI_EXE% help <topic> for more information.
General: example, changelog
Commands: backup, find, restore, delete, compact, test, compare, purge, vacuum
Repair: repair, affected, list-broken-files, purge-broken-files
Debug: debug, logging, create-report, test-filters, system-info, send-mail
Targets: %BACKENDS%
Modules: %ENCRYPTIONMODULES%, %COMPRESSIONMODULES%, %GENERICMODULES%
Formats: date, time, size, encryption, compression
Formats: date, time, size, decimal, encryption, compression
Advanced: mail, advanced, returncodes, filter, filter-groups, <option>
https://duplicati.com/ Version: %VERSION%
@@ -68,7 +68,7 @@ Restore a specific file
# =============================================================================
> duplicati.commandline.exe help backup
Usage: backup <storage-URL> "<source-path>" [<options>]
Usage: %CLI_EXE% backup <storage-URL> "<source-path>" [<options>]
Makes a backup of <source-path> and stores it in <storage-URL>. The format for <storage-URL> is
protocol://username:password@hostname:port/path?backend_option1=value1&backend_option2=value2.
@@ -97,7 +97,7 @@ Usage: backup <storage-URL> "<source-path>" [<options>]
> duplicati.commandline.exe help find
> duplicati.commandline.exe help list
Usage: find <storage-URL> ["<filename>"] [<options>]
Usage: %CLI_EXE% find <storage-URL> ["<filename>"] [<options>]
Finds specific files in specific backups. If <filename> is specified, all occurrences of <filename> in the backup are listed. <filename> can contain * and ? as wildcards. File names in [brackets] are interpreted as regular expression. Latest backup is searched by default. If entire path is specified, all available versions of the file are listed. If no <filename> is specified, a list of all available backups is shown.
@@ -116,7 +116,7 @@ Usage: find <storage-URL> ["<filename>"] [<options>]
# =============================================================================
> duplicati.commandline.exe help restore
Usage: restore <storage-URL> ["<filename>"] [<options>]
Usage: %CLI_EXE% restore <storage-URL> ["<filename>"] [<options>]
Restores <filename> to its original destination. If <filename> exists already, <filename> is changed to <filename-timestamp.extension>. To restore all files use "*" or leave empty.
@@ -134,7 +134,7 @@ Usage: restore <storage-URL> ["<filename>"] [<options>]
# =============================================================================
> duplicati.commandline.exe help delete
Usage: delete <storage-URL> [<options>]
Usage: %CLI_EXE% delete <storage-URL> [<options>]
Marks old data deleted and removes outdated dlist files. A backup is deleted when it is older than <keep-time> or when there are more newer versions than <keep-versions>. Data is considered old, when it is not required from any existing backup anymore.
@@ -154,7 +154,7 @@ Usage: delete <storage-URL> [<options>]
# =============================================================================
> duplicati.commandline.exe help compact
Usage: compact <storage-URL> [<options>]
Usage: %CLI_EXE% compact <storage-URL> [<options>]
Old data is not deleted immediately as in most cases only small parts of a dblock file are old data. When the amount of old data in a dblock file grows it might be worth to replace it. This is especially the case when the number of dblock files and thus the required storage space can be reduced. When backups are frequently made and only few files have changed, the uploaded dblock files are small. At some point it might make sense to replace a large number of small files with one large file. This is what compacting does.
@@ -171,7 +171,7 @@ Usage: compact <storage-URL> [<options>]
# =============================================================================
> duplicati.commandline.exe help compare
Usage: compare <storage-URL> [<base-version>] [<compare-to>] [<options>]
Usage: %CLI_EXE% compare <storage-URL> [<base-version>] [<compare-to>] [<options>]
Compares two backups and shows differences. If no versions are given, changes are shown between the two latest backups. The versions can either be timestamps or backup version numbers. If only one version is given, the most recent backup is compared to that version.
@@ -187,7 +187,7 @@ Usage: compare <storage-URL> [<base-version>] [<compare-to>] [<options>]
# =============================================================================
> duplicati.commandline.exe help create-report
Usage: create-report <storage-URL> <output-file> [<options>]
Usage: %CLI_EXE% create-report <storage-URL> <output-file> [<options>]
Analyses the backup and prepares a report with anonymous information. This report can be sent to the developers for a better analysis in case something went wrong.
@@ -197,7 +197,7 @@ Usage: create-report <storage-URL> <output-file> [<options>]
# =============================================================================
> duplicati.commandline.exe help repair
Usage: repair <storage-URL> [<options>]
Usage: %CLI_EXE% repair <storage-URL> [<options>]
Tries to repair the backup. If no local db is found or the db is empty, the db is re-created with data from the storage. If the db is in place but the remote storage is corrupt, the remote storage gets repaired with local data (if available).
@@ -206,7 +206,7 @@ Usage: repair <storage-URL> [<options>]
# =============================================================================
> duplicati.commandline.exe help purge
Usage: purge <storage-URL> <filenames> [<options>]
Usage: %CLI_EXE% purge <storage-URL> <filenames> [<options>]
Purges (removes) files from remote backup data. This command can either take a list of filenames or use the filters to choose which files to purge. The purge process creates new filesets on the remote destination with the purged files removed, and will start the compacting process after a purge. By default, the matching files are purged in all versions, but this can be limited by choosing one or more versions. To test what will happen, use the --dry-run flag.
@@ -228,11 +228,11 @@ Usage: purge <storage-URL> <filenames> [<options>]
> duplicati.commandline.exe help list-broken-files
> duplicati.commandline.exe help purge-broken-files
Usage: list-broken-files <storage-URL> [<options>]
Usage: %CLI_EXE% list-broken-files <storage-URL> [<options>]
Checks the database for missing data that cause files not not be restoreable. Files can become unrestoreable if remote data files are defect or missing. Use the list-broken-files command to see what the purge-broken-files command will remove.
Usage: purge-broken-files <storage-URL> [<options>]
Usage: %CLI_EXE% purge-broken-files <storage-URL> [<options>]
Removes all files from the database and remote storage that are no longer restoreable. Use this operation with caution, and only if you cannot recover the missing remote files, but want to continue a backup. Even with missing remote files, it may be possible to restore parts of the files that will be removed with this command.
@@ -244,7 +244,7 @@ Usage: purge-broken-files <storage-URL> [<options>]
> duplicati.commandline.exe help test
> duplicati.commandline.exe help verify
Usage: test <storage-URL> <samples> [<options>]
Usage: %CLI_EXE% test <storage-URL> <samples> [<options>]
Verifies integrity of a backup. A random sample of dlist, dindex, dblock files is downloaded, decrypted and the content is checked against recorded size values and data hashes. <samples> specifies the number of samples to be tested. If "all” is specified, all files in the backup will be tested. This is a rolling check, i.e. when executed another time different samples are verified than in the first run. A sample consists of 1 dlist, 1 dindex, 1 dblock.
@@ -262,21 +262,21 @@ Usage: test <storage-URL> <samples> [<options>]
> duplicati.commandline.exe help test-filters
> duplicati.commandline.exe help test-filter
Usage: test-filters <source-path> [<options>]
Usage: %CLI_EXE% test-filters <source-path> [<options>]
Scans the source files and tests against the filters specified, the console output shows which files and folders are examined and the result.
# =============================================================================
> duplicati.commandline.exe help affected
Usage: affected <storage-URL> <remote-filename> [<remote-filenames>] [<options>]
Usage: %CLI_EXE% affected <storage-URL> <remote-filename> [<remote-filenames>] [<options>]
Returns a report explaining what backup sets and files are affected by a remote file. You can use this option to see what source files are affected if one or more remote files are damaged or deleted. Notes that this command requires a local database to be present, if the database is not found automatically, you can set --dbpath to point to the database to use.
# =============================================================================
> duplicati.commandline.exe help vacuum
Usage: vacuum <storage-URL> [<options>]
Usage: %CLI_EXE% vacuum <storage-URL> [<options>]
Rebuilds the local database, repacking it into a minimal amount of disk space.
@@ -434,6 +434,15 @@ Whenever a size is required, you can use any of the following suffixes:
For speed limits, use the same format. If you enter 1MB this will be 1MB/sec.
# =============================================================================
> duplicati.commandline.exe help decimal
For entering decimal values you must use the English notation, with a period as decimal separator:
0.1
12.34
# =============================================================================
> duplicati.commandline.exe help filter
> duplicati.commandline.exe help filters
@@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<AssemblyName>Duplicati.CommandLine.ConfigurationImporter.Implementation</AssemblyName>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
</PropertyGroup>
<ItemGroup>
@@ -18,4 +19,4 @@
</PackageReference>
</ItemGroup>
</Project>
</Project>
@@ -19,8 +19,9 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.AutoUpdater;
using Duplicati.Library.RestAPI;
using Duplicati.Server.Serializable;
using Duplicati.Server.WebServer.RESTMethods;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -29,26 +30,32 @@ namespace Duplicati.CommandLine.ConfigurationImporter
{
public static class ConfigurationImporter
{
private static readonly string usageString = $"Usage: {nameof(ConfigurationImporter)}.exe <configuration-file> --import-metadata=(true | false) --server-datafolder=<folder containing Duplicati-server.sqlite>";
private static readonly string UsageString = $"Usage: {PackageHelper.GetExecutableName(PackageHelper.NamedExecutable.ConfigurationImporter)} <configuration-file> --import-metadata=(true | false) --server-datafolder=<folder containing Duplicati-server.sqlite>";
public static int Main(string[] args)
{
if (args.Length != 3)
{
throw new ArgumentException($"Incorrect number of input arguments. {ConfigurationImporter.usageString}");
Console.WriteLine($"Incorrect number of input arguments.");
Console.WriteLine(UsageString);
return 1;
}
string configurationFile = args[0];
Dictionary<string, string> importOptions = Duplicati.Library.Utility.CommandLineParser.ExtractOptions(args.Skip(1).ToList());
if (!importOptions.TryGetValue("import-metadata", out string importMetadataString))
{
throw new ArgumentException($"Invalid import-metadata argument. {ConfigurationImporter.usageString}");
Console.WriteLine($"Missing import-metadata argument.");
Console.WriteLine(UsageString);
return 1;
}
bool importMetadata = Duplicati.Library.Utility.Utility.ParseBool(importMetadataString, false);
if (!importOptions.TryGetValue("server-datafolder", out string serverDatafolder))
{
throw new ArgumentException($"Invalid server-datafolder argument. {ConfigurationImporter.usageString}");
Console.WriteLine($"Missing server-datafolder argument.");
Console.WriteLine(UsageString);
return 1;
}
Dictionary<string, string> advancedOptions = new Dictionary<string, string>
@@ -56,7 +63,7 @@ namespace Duplicati.CommandLine.ConfigurationImporter
{ "server-datafolder", serverDatafolder }
};
ImportExportStructure importedStructure = Backups.ImportBackup(configurationFile, importMetadata, () => ConfigurationImporter.ReadPassword($"Password for {configurationFile}: "), advancedOptions);
ImportExportStructure importedStructure = BackupImportExportHandler.ImportBackup(configurationFile, importMetadata, () => ConfigurationImporter.ReadPassword($"Password for {configurationFile}: "), advancedOptions);
Console.WriteLine($"Imported \"{importedStructure.Backup.Name}\" with ID {importedStructure.Backup.ID} and local database at {importedStructure.Backup.DBPath}.");
return 0;
@@ -1,27 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Description>RecoveryTool for Duplicati</Description>
<AssemblyName>Duplicati.CommandLine.RecoveryTool.Implementation</AssemblyName>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Backends\Duplicati.Library.Backends.csproj" />
<ProjectReference Include="..\..\Library\Compression\Duplicati.Library.Compression.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Encryption\Duplicati.Library.Encryption.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Interface\Duplicati.Library.Interface.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\Main\Duplicati.Library.Main.csproj">
</ProjectReference>
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj" />
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj" />
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj" />
<ProjectReference Include="..\..\Library\Main\Duplicati.Library.Main.csproj" />
</ItemGroup>
<ItemGroup>
@@ -36,3 +26,4 @@
</ItemGroup>
</Project>
+7 -2
View File
@@ -21,6 +21,7 @@
using System;
using System.Collections.Generic;
using Duplicati.Library.AutoUpdater;
namespace Duplicati.CommandLine.RecoveryTool
{
@@ -35,9 +36,13 @@ namespace Duplicati.CommandLine.RecoveryTool
Console.WriteLine("Unsupported command: {0}", args[0]);
Console.WriteLine();
return 100;
}
}
Console.Write(new System.IO.StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(System.Reflection.Assembly.GetExecutingAssembly().GetName().Name + "." + RESOURCE_NAME)).ReadToEnd());
using var s = new System.IO.StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(Help), RESOURCE_NAME));
Console.Write(s.ReadToEnd()
.Replace("%CLI_EXE%", PackageHelper.GetExecutableName(PackageHelper.NamedExecutable.CommandLine))
.Replace("%RECOVERY_EXE%", PackageHelper.GetExecutableName(PackageHelper.NamedExecutable.RecoveryTool))
);
return 0;
}
+266 -143
View File
@@ -24,10 +24,9 @@ using System.IO;
using System.Linq;
using System.Collections.Generic;
using Duplicati.Library.Common.IO;
using Duplicati.Library.Common;
using System.Security.Cryptography;
using Duplicati.Library.Utility;
using System.IO.Compression;
using Duplicati.Library.Interface;
namespace Duplicati.CommandLine.RecoveryTool
{
@@ -53,11 +52,8 @@ namespace Duplicati.CommandLine.RecoveryTool
Directory.SetCurrentDirectory(folder);
string targetpath;
options.TryGetValue("targetpath", out targetpath);
string ixfile;
options.TryGetValue("indexfile", out ixfile);
options.TryGetValue("targetpath", out var targetpath);
options.TryGetValue("indexfile", out var ixfile);
if (string.IsNullOrWhiteSpace(ixfile))
ixfile = "index.txt";
@@ -84,12 +80,13 @@ namespace Duplicati.CommandLine.RecoveryTool
Library.Main.Volumes.VolumeReaderBase.UpdateOptionsFromManifest(Path.GetExtension(filelist).Trim('.'), filelist, new Duplicati.Library.Main.Options(options));
string blocksize_str;
options.TryGetValue("blocksize", out blocksize_str);
string blockhash_str;
options.TryGetValue("block-hash-algorithm", out blockhash_str);
string filehash_str;
options.TryGetValue("block-hash-algorithm", out filehash_str);
options.TryGetValue("blocksize", out var blocksize_str);
options.TryGetValue("block-hash-algorithm", out var blockhash_str);
options.TryGetValue("block-hash-algorithm", out var filehash_str);
var offset = 0L;
if (options.TryGetValue("offset", out var offset_str))
offset = long.Parse(offset_str);
long blocksize = string.IsNullOrWhiteSpace(blocksize_str) ? 0 : Library.Utility.Sizeparser.ParseSize(blocksize_str);
@@ -99,12 +96,19 @@ namespace Duplicati.CommandLine.RecoveryTool
return 100;
}
if (string.IsNullOrWhiteSpace(blockhash_str))
throw new Duplicati.Library.Interface.UserInformationException(string.Format("Block hash algorithm not valid: {0}", blockhash_str), "BlockHashAlgorithmNotSupported");
if (string.IsNullOrWhiteSpace(filehash_str))
throw new Duplicati.Library.Interface.UserInformationException(string.Format("File hash algorithm not valid: {0}", filehash_str), "FileHashAlgorithmNotSupported");
var useIndexMap = !Library.Utility.Utility.ParseBoolOption(options, "reduce-memory-use");
var disableFileVerify = Library.Utility.Utility.ParseBoolOption(options, "disable-file-verify");
var startedCount = 0L;
var restoredCount = 0L;
var errorCount = 0L;
var fileErrors = 0L;
var totalSize = 0L;
using (var blockhasher = HashFactory.CreateHasher(blockhash_str))
using (var filehasher = HashFactory.CreateHasher(filehash_str))
@@ -113,66 +117,45 @@ namespace Duplicati.CommandLine.RecoveryTool
var hashesprblock = blocksize / (blockhasher.HashSize / 8);
Console.WriteLine("Building lookup table for file hashes");
using (HashLookupHelper lookup = new HashLookupHelper(ixfile, mru, (int) blocksize, blockhasher.HashSize / 8))
// Source OS can have different directory separator
string sourceDirsep = null;
using (HashLookupHelper lookup = new HashLookupHelper(ixfile, mru, (int)blocksize, blockhasher.HashSize / 8, useIndexMap))
{
var filecount = 0L;
string largestprefix = null;
string[] largestprefixparts = null;
if (!string.IsNullOrWhiteSpace(targetpath))
Console.WriteLine("Computing restore path");
foreach (var f in List.EnumerateFilesInDList(filelist, filter, options))
{
if (largestprefix == null)
{
largestprefix = f.Path;
largestprefixparts = largestprefix.Split(new char[] { Path.DirectorySeparatorChar });
}
else if (largestprefix.Length > 1)
{
var parts = f.Path.Split(new char[] { Path.DirectorySeparatorChar });
var ni = 0;
for (; ni < Math.Min(parts.Length, largestprefixparts.Length); ni++)
if (!Library.Utility.Utility.ClientFilenameStringComparer.Equals(parts[ni], largestprefixparts[ni]))
break;
if (ni != largestprefixparts.Length)
{
if (ni == 0)
{
largestprefixparts = new string[0];
largestprefix = string.Empty;
}
else
{
Array.Resize(ref largestprefixparts, ni - 1);
largestprefix = string.Join(Util.DirectorySeparatorString, largestprefixparts);
}
}
}
filecount++;
}
string largestprefix = GetLargestPrefix(from f in List.EnumerateFilesInDList(filelist, filter, options) select f.Path, out sourceDirsep, out filecount);
Console.WriteLine("Restoring {0} files to {1}", filecount, string.IsNullOrWhiteSpace(targetpath) ? "original position" : targetpath);
if (Platform.IsClientPosix || largestprefix.Length > 0)
largestprefix = Util.AppendDirSeparator(largestprefix);
if (!string.IsNullOrEmpty(largestprefix))
Console.WriteLine("Removing common prefix {0} from files", largestprefix);
var i = 0L;
// Allocate buffers for block and blocklist hashes
var buffer = new byte[blocksize];
var blocklistbuffer = new byte[blocksize];
foreach (var f in List.EnumerateFilesInDList(filelist, filter, options))
{
if (i < offset)
{
Console.WriteLine($"Skipping {i}: {f.Path} ({Utility.FormatSizeString(f.Size)})");
i++;
continue;
}
startedCount++;
try
{
var targetfile = MapToRestorePath(f.Path, largestprefix, targetpath);
var targetfile = MapToRestorePath(f.Path, largestprefix, targetpath, sourceDirsep);
if (!systemIO.DirectoryExists(systemIO.PathGetDirectoryName(targetfile)))
systemIO.DirectoryCreate(systemIO.PathGetDirectoryName(targetfile));
Console.Write("{0}: {1} ({2})", i, targetfile, Library.Utility.Utility.FormatSizeString(f.Size));
Console.Write($"{i} ({Utility.FormatSizeString(totalSize)}): {targetfile} ({Utility.FormatSizeString(f.Size)})");
totalSize += f.Size;
using (var tf = new Library.Utility.TempFile())
{
@@ -180,7 +163,8 @@ namespace Duplicati.CommandLine.RecoveryTool
{
if (f.BlocklistHashes == null)
{
lookup.WriteHash(sw, f.Hash);
if (f.Size > 0)
lookup.WriteHash(sw, f.Hash, buffer);
}
else
{
@@ -193,15 +177,16 @@ namespace Duplicati.CommandLine.RecoveryTool
try
{
var bi = 0;
foreach (var h in lookup.ReadBlocklistHashes(blh))
foreach (var h in lookup.ReadBlocklistHashes(blh, blocklistbuffer))
{
try
{
sw.Position = blockhashoffset + (bi * blocksize);
lookup.WriteHash(sw, h);
lookup.WriteHash(sw, h, buffer);
}
catch (Exception ex)
{
errorCount++;
Console.WriteLine("Failed to read hash: {0}{1}{2}", h, Environment.NewLine, ex);
}
@@ -210,6 +195,7 @@ namespace Duplicati.CommandLine.RecoveryTool
}
catch (Exception ex)
{
errorCount++;
Console.WriteLine("Failed to read Blocklist hash: {0}{1}{2}", blh, Environment.NewLine, ex);
}
@@ -218,30 +204,45 @@ namespace Duplicati.CommandLine.RecoveryTool
}
}
string fh;
using (var fs = File.OpenRead(tf))
fh = Convert.ToBase64String(filehasher.ComputeHash(fs));
restoredCount++;
if (fh == f.Hash)
if (disableFileVerify)
{
Console.WriteLine(" done!");
systemIO.FileCopy(tf, targetfile, true);
Console.WriteLine(" done! (not verified)");
if (systemIO.FileExists(targetfile))
systemIO.FileCopy(tf, targetfile, true);
else
systemIO.FileMove(tf, targetfile);
}
else
{
Console.Write(" - Restored file hash mismatch");
if (systemIO.FileExists(targetfile))
Console.WriteLine(" - not overwriting existing file: {0}", targetfile);
string fh;
using (var fs = File.OpenRead(tf))
fh = Convert.ToBase64String(filehasher.ComputeHash(fs));
if (fh == f.Hash)
{
Console.WriteLine(" done!");
if (systemIO.FileExists(targetfile))
systemIO.FileCopy(tf, targetfile, true);
else
systemIO.FileMove(tf, targetfile);
}
else
Console.WriteLine(" - restoring file in damaged condition");
{
fileErrors++;
Console.Write(" - Restored file hash mismatch");
if (systemIO.FileExists(targetfile))
Console.WriteLine(" - not overwriting existing file: {0}", targetfile);
else
Console.WriteLine(" - restoring file in damaged condition");
}
}
}
}
catch (Exception ex)
{
errorCount++;
Console.WriteLine(" error: {0}", ex);
}
i++;
@@ -249,11 +250,80 @@ namespace Duplicati.CommandLine.RecoveryTool
}
}
return 0;
if (fileErrors > 0 || restoredCount != startedCount)
Console.WriteLine($"Restored {restoredCount} files of {startedCount} attempted ({Library.Utility.Utility.FormatSizeString(totalSize)}); {fileErrors} files had verification errors");
else
Console.WriteLine($"Restored {restoredCount} files ({Library.Utility.Utility.FormatSizeString(totalSize)})");
if (errorCount > 0)
Console.WriteLine("Errors: {0}", errorCount);
return errorCount != 0 || restoredCount != startedCount || fileErrors > 0
? 1
: 0;
}
private static string MapToRestorePath(string path, string prefixpath, string restorepath)
public static string GetLargestPrefix(IEnumerable<string> filePaths, out string sourceDirsep, out long filecount)
{
// Get dir separator like in LocalRestoreDatabase.GetLargestPrefix():
string largestprefix = filePaths.OrderByDescending(p => p.Length).FirstOrDefault() ?? string.Empty;
sourceDirsep = Util.GuessDirSeparator(largestprefix);
string[] dirsepSplit = new string[] { sourceDirsep };
string[] largestprefixparts = largestprefix.Split(dirsepSplit, StringSplitOptions.None);
// Because only files are in the list, need to remove filename from prefix
// Otherwise, in case of a single file the prefix is not a directory
if (largestprefixparts.Length > 0)
Array.Resize(ref largestprefixparts, largestprefixparts.Length - 1);
largestprefix = string.Join(sourceDirsep, largestprefixparts);
filecount = 0;
foreach (var path in filePaths)
{
if (largestprefix.Length > 1)
{
// Unix paths starting with / have an empty string in paths[0]
// Completely empty strings should not combine with that, so should have no parts at all
var parts = path.Length == 0 ? new string[0] : path.Split(dirsepSplit, StringSplitOptions.None);
var ni = 0;
for (; ni < Math.Min(parts.Length, largestprefixparts.Length); ni++)
if (!Library.Utility.Utility.ClientFilenameStringComparer.Equals(parts[ni], largestprefixparts[ni]))
break;
if (ni != largestprefixparts.Length)
{
if (ni == 0)
{
largestprefixparts = new string[0];
largestprefix = string.Empty;
}
else
{
// Only the first ni parts match
Array.Resize(ref largestprefixparts, ni);
largestprefix = string.Join(sourceDirsep, largestprefixparts);
}
}
}
filecount++;
}
return largestprefixparts.Length == 0 ? "" : Util.AppendDirSeparator(largestprefix, sourceDirsep);
}
public static string MapToRestorePath(string path, string prefixpath, string restorepath, string sourceDirsep)
{
if (sourceDirsep != null && sourceDirsep != Util.DirectorySeparatorString && sourceDirsep != Util.AltDirectorySeparatorString)
{
// Replace directory separator in source and prefix path
path = path.Replace(sourceDirsep, Util.DirectorySeparatorString);
if (!string.IsNullOrWhiteSpace(prefixpath))
{
prefixpath = prefixpath.Replace(sourceDirsep, Util.DirectorySeparatorString);
}
}
if (string.IsNullOrWhiteSpace(restorepath))
return path;
@@ -283,53 +353,69 @@ namespace Duplicati.CommandLine.RecoveryTool
private Stream m_indexfile;
private readonly List<string> m_lookup = new List<string>();
private readonly List<long> m_offsets = new List<long>();
private readonly Dictionary<string, string> m_indexMap;
private readonly byte[] m_linebuf = new byte[128];
private readonly byte[] m_newline = Environment.NewLine.Select(x => (byte)x).ToArray();
public HashLookupHelper(string indexfile, CompressedFileMRUCache cache, int blocksize, int hashsize)
public HashLookupHelper(string indexfile, CompressedFileMRUCache cache, int blocksize, int hashsize, bool useIndexMap)
{
m_cache = cache;
m_blocksize = blocksize;
m_hashsize = hashsize;
m_indexfile = File.OpenRead(indexfile);
// Build index ....
var hashes = 0L;
string prev = null;
m_indexfile.Position = 0;
foreach (var n in AllFileLines())
if (useIndexMap)
{
if (n.Key != prev)
{
hashes++;
prev = n.Key;
}
m_indexMap = new Dictionary<string, string>();
Console.WriteLine("Building in-memory index table ... ");
m_indexfile.Position = 0;
foreach (var n in AllFileLines())
m_indexMap[n.Key] = n.Value;
Console.WriteLine("Index table has {0} unique hashes", m_indexMap.Count);
}
Console.WriteLine("Index file has {0} hashes in total", hashes);
var lookuptablesize = Math.Max(1, Math.Min(LOOKUP_TABLE_SIZE, hashes) - 1);
var lookupincrements = hashes / lookuptablesize;
Console.WriteLine("Building lookup table with {0} entries, giving increments of {1}", lookuptablesize, lookupincrements);
prev = null;
var prevoff = 0L;
var hc = 0L;
m_indexfile.Position = 0;
foreach (var n in AllFileLines())
else
{
if (n.Key != prev)
// Build index ....
var hashes = 0L;
string prev = null;
m_indexfile.Position = 0;
foreach (var n in AllFileLines())
{
if ((hc % lookupincrements) == 0)
if (n.Key != prev)
{
m_lookup.Add(n.Key);
m_offsets.Add(prevoff);
hashes++;
prev = n.Key;
}
}
hc++;
prev = n.Key;
prevoff = m_indexfile.Position;
Console.WriteLine("Index file has {0} hashes in total", hashes);
var lookuptablesize = Math.Max(1, Math.Min(LOOKUP_TABLE_SIZE, hashes) - 1);
var lookupincrements = hashes / lookuptablesize;
Console.WriteLine("Building lookup table with {0} entries, giving increments of {1}", lookuptablesize, lookupincrements);
m_indexfile.Position = 0;
prev = null;
var prevoff = 0L;
var hc = 0L;
foreach (var n in AllFileLines())
{
if (n.Key != prev)
{
if ((hc % lookupincrements) == 0)
{
m_lookup.Add(n.Key);
m_offsets.Add(prevoff);
}
hc++;
prev = n.Key;
prevoff = m_indexfile.Position;
}
}
}
}
@@ -382,49 +468,55 @@ namespace Duplicati.CommandLine.RecoveryTool
}
}
public IEnumerable<string> ReadBlocklistHashes(string hash)
public IEnumerable<string> ReadBlocklistHashes(string hash, byte[] buffer)
{
var bytes = ReadHash(hash);
for (var i = 0; i < bytes.Length; i += m_hashsize)
yield return Convert.ToBase64String(bytes, i, m_hashsize);
var bytes = ReadHash(hash, buffer);
for (var i = 0; i < bytes; i += m_hashsize)
yield return Convert.ToBase64String(buffer, i, m_hashsize);
}
public byte[] ReadHash(string hash)
public int ReadHash(string hash, byte[] buffer)
{
var ix = m_lookup.BinarySearch(hash, StringComparer.Ordinal);
if (ix < 0)
// Use in-memory map if available
if (m_indexMap != null)
{
ix = ~ix;
if (ix != 0)
ix--;
if (!m_indexMap.TryGetValue(hash, out var filename))
throw new Exception(string.Format("Unable to locate block with hash: {0}", hash));
using (var fs = m_cache.ReadBlock(filename, hash))
return Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buffer, buffer.Length);
}
m_indexfile.Position = m_offsets[ix];
foreach (var v in AllFileLines())
else
{
if (v.Key == hash)
using (var fs = m_cache.ReadBlock(v.Value, hash))
{
var buf = new byte[m_blocksize];
var l = Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buf, buf.Length);
Array.Resize(ref buf, l);
var ix = m_lookup.BinarySearch(hash, StringComparer.Ordinal);
if (ix < 0)
{
ix = ~ix;
if (ix != 0)
ix--;
}
return buf;
}
m_indexfile.Position = m_offsets[ix];
foreach (var v in AllFileLines())
{
if (v.Key == hash)
using (var fs = m_cache.ReadBlock(v.Value, hash))
return Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buffer, buffer.Length);
if (StringComparer.Ordinal.Compare(hash, v.Key) < 0)
break;
if (StringComparer.Ordinal.Compare(hash, v.Key) < 0)
break;
}
throw new Exception(string.Format("Unable to locate block with hash: {0}", hash));
}
throw new Exception(string.Format("Unable to locate block with hash: {0}", hash));
}
public void WriteHash(Stream sw, string hash)
public void WriteHash(Stream sw, string hash, byte[] buffer)
{
var h = ReadHash(hash);
sw.Write(h, 0, h.Length);
var h = ReadHash(hash, buffer);
sw.Write(buffer, 0, h);
}
@@ -436,28 +528,59 @@ namespace Duplicati.CommandLine.RecoveryTool
}
}
private class WrappedZipReader : IArchiveReader
{
private readonly ZipArchive m_zip;
public WrappedZipReader(Stream stream)
{
m_zip = new ZipArchive(stream, ZipArchiveMode.Read, true);
}
public void Dispose()
{
m_zip.Dispose();
}
public bool FileExists(string file) => throw new NotImplementedException();
public DateTime GetLastWriteTime(string file) => throw new NotImplementedException();
public string[] ListFiles(string prefix) => throw new NotImplementedException();
public IEnumerable<KeyValuePair<string, long>> ListFilesWithSize(string prefix) => throw new NotImplementedException();
public Stream OpenRead(string file) => m_zip.GetEntry(file).Open();
}
private class CompressedFileMRUCache : IDisposable
{
private Dictionary<string, Library.Interface.ICompression> m_lookup = new Dictionary<string, Duplicati.Library.Interface.ICompression>();
private Dictionary<string, Library.Interface.IArchiveReader> m_lookup = new Dictionary<string, Library.Interface.IArchiveReader>();
private Dictionary<string, Stream> m_streams = new Dictionary<string, Stream>();
private List<string> m_mru = new List<string>();
private readonly Dictionary<string, string> m_options;
private readonly bool m_useWrappedZip;
private const int MAX_OPEN_ARCHIVES = 20;
private readonly int MAX_OPEN_ARCHIVES = 200;
public CompressedFileMRUCache(Dictionary<string, string> options)
{
m_options = options;
m_useWrappedZip = !Library.Utility.Utility.ParseBoolOption(options, "disable-wrapped-zip");
if (options.TryGetValue("max-open-archives", out var maxopenarchives))
MAX_OPEN_ARCHIVES = int.Parse(maxopenarchives);
}
public Stream ReadBlock(string filename, string hash)
{
Library.Interface.ICompression cf;
Stream stream;
if (!m_lookup.TryGetValue(filename, out cf) || !m_streams.TryGetValue(filename, out stream))
if (!m_lookup.TryGetValue(filename, out var cf) || !m_streams.ContainsKey(filename))
{
stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete);
cf = Library.DynamicLoader.CompressionLoader.GetModule(Path.GetExtension(filename).Trim('.'), stream, Library.Interface.ArchiveMode.Read, m_options);
var stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete);
var ext = Path.GetExtension(filename).Trim('.');
cf = m_useWrappedZip && ext.Equals("zip", StringComparison.OrdinalIgnoreCase)
? new WrappedZipReader(stream)
: Library.DynamicLoader.CompressionLoader.GetModule(ext, stream, Library.Interface.ArchiveMode.Read, m_options);
if (cf == null)
{
stream.Dispose();
@@ -467,7 +590,7 @@ namespace Duplicati.CommandLine.RecoveryTool
m_streams[filename] = stream;
}
if (m_mru.Count > 0 && m_mru[0] != filename)
if (m_mru.Count == 0 || m_mru[0] != filename)
{
m_mru.Remove(filename);
m_mru.Insert(0, filename);
@@ -476,7 +599,7 @@ namespace Duplicati.CommandLine.RecoveryTool
while (m_mru.Count > MAX_OPEN_ARCHIVES)
{
var f = m_mru.Last();
m_lookup[f].Dispose();
(m_lookup[f] as IDisposable)?.Dispose();
m_lookup.Remove(f);
m_streams[f].Dispose();
m_streams.Remove(f);
@@ -491,7 +614,7 @@ namespace Duplicati.CommandLine.RecoveryTool
public void Dispose()
{
foreach (var v in m_lookup.Values)
try { v.Dispose(); }
try { (v as IDisposable)?.Dispose(); }
catch { }
foreach (var v in m_streams.Values)
+16 -8
View File
@@ -2,7 +2,7 @@
=======================
This tool performs a recovery of as much data as possible in small steps that must be performed in order.
We recommend that you use Duplicati.CommandLine.exe to do the restore, and rely only on this tool if all else fails.
We recommend that you use %CLI_EXE% to do the restore, and rely only on this tool if all else fails.
The steps to perform are:
@@ -38,17 +38,17 @@ Download
--------
Usage:
Duplicati.RecoveryTool.exe download <remoteurl> <localfolder> [options]
%RECOVERY_EXE% download <remoteurl> <localfolder> [options]
Downloads all files matching the Duplicati filenames from <remoteurl> to <localfolder>, and decrypts them in the process.
The remote url must be one supported by Duplicati.
Use `Duplicati.CommandLine.exe help backends` to see backends and options.
Use `%CLI_EXE% help backends` to see backends and options.
Index
-----
Duplicati.RecoveryTool.exe index <localfolder> [options]
%RECOVERY_EXE% index <localfolder> [options]
Examines all files found in localfolder and produces an index.txt file, which is a list of all block hashes found in the files.
The index file can be rather large. It defaults to being stored in <localfolder> but can be specified with --indexfile.
@@ -58,17 +58,25 @@ Some files are created in the system temporary folder, use --tempdir to set an a
Restore
-------
Duplicati.RecoveryTool.exe restore <localfolder> [version] [options]
%RECOVERY_EXE% restore <localfolder> [version] [options]
Restores all files to their respective destinations. Use --targetpath to choose another folder where the files are restored into.
Use the filters, --exclude, to perform a partial restore.
Version can be either a number, a filename or a date. If omitted the most recent backup is used.
The restore process requires a fast lookup, which is optimal if all the hashes can be kept in memory.
Use the option to --reduce-memory-use=true to toggle a slower low-memory restore.
If the process is interrupted for any reason, note the file counter and use --offset=1 to start the restore after the first file.
Advanced performance options are:
--reduce-memory-use: Disables keeping all hashes in memory; use if memory is limited on the restoring machine
--disable-file-verify: Disables the initial hashing of the restored file
--disable-wrapped-zip: Disable using the faster .NET native Zip archive in favor of the more resilient one in Duplicati
--max-open-archives: Sets the number of archives to keep open for faster access (uses some memory pr. archive); default 200
List
----
Duplicati.RecoveryTool.exe list <localfolder> [version] [options]
%RECOVERY_EXE% list <localfolder> [version] [options]
Lists contents of <localfolder> backups.
Version can be either a number, a filename or a date.
@@ -79,7 +87,7 @@ Use the filters, --exclude, to show a subset of files.
Recompress
----
Duplicati.RecoveryTool.exe recompress <targetcompression> <remoteurl> <localfolder> --reupload --reencrypt [options]
%RECOVERY_EXE% recompress <targetcompression> <remoteurl> <localfolder> --reupload --reencrypt [options]
1) Downloads whole remote storage to <localfolder>.
2) Recompress from existing compression type to <targetcompression>
@@ -96,7 +104,7 @@ Supported Options
-----------------
The backend modules support all their normal options.
To see what options a specific backend supports, type:
Duplicati.CommandLine.exe help <backend>
%CLI_EXE% help <backend>
The environment variables AUTH_USERNAME and AUTH_PASSWORD are supported.
The options --parameters-file and --tempdir are supported.
@@ -0,0 +1,133 @@
using System;
using System.Threading;
using OpenQA.Selenium;
using OpenQA.Selenium.Edge;
namespace Duplicati.Browser.Test.Drivers;
/// <summary>
/// Manages a browser instance using Selenium
/// </summary>
public class BrowserDriver : IDisposable
{
private readonly Lazy<IWebDriver> _currentWebDriverLazy = new(CreateWebDriver);
private bool _isDisposed;
/// <summary>
/// The Selenium IWebDriver instance
/// </summary>
public IWebDriver Current => _currentWebDriverLazy.Value;
/// <summary>
/// Creates the Selenium web driver (opens a browser)
/// </summary>
/// <returns></returns>
private static EdgeDriver CreateWebDriver()
{
//We use the Chrome browser
var chromeDriverService = EdgeDriverService.CreateDefaultService(".", "msedgedriver");
return new EdgeDriver(chromeDriverService, new EdgeOptions());
}
/// <summary>
/// Disposes the Selenium web driver (closing the browser)
/// </summary>
public void Dispose()
{
if (_isDisposed)
{
return;
}
if (_currentWebDriverLazy.IsValueCreated)
{
Current.Quit();
}
_isDisposed = true;
}
}
public static class DriverExtensions
{
public static int SingleLoopWait = 100;
public static TimeSpan DefaultMaxWait = TimeSpan.FromSeconds(30);
public static IWebElement WaitForElement(this ISearchContext driver, By selector, TimeSpan? maxWait = null)
{
maxWait ??= DefaultMaxWait;
var i = 0;
while (i < maxWait.Value.TotalMilliseconds)
{
try
{
return driver.FindElement(selector);
}
catch (NoSuchElementException)
{
//swallow and wait for another loop
}
i += SingleLoopWait;
Thread.Sleep(TimeSpan.FromMilliseconds(SingleLoopWait));
}
return driver.FindElement(selector);
}
public static bool WaitForElementBeing(this IWebElement element, Func<IWebElement, bool> predicate,
TimeSpan? maxWait = null)
{
maxWait ??= DefaultMaxWait;
var i = 0;
while (i < maxWait.Value.TotalMilliseconds)
{
try
{
if (predicate.Invoke(element))
{
return true;
}
}
catch (NoSuchElementException)
{
//swallow and wait for another loop
}
i += SingleLoopWait;
Thread.Sleep(TimeSpan.FromMilliseconds(SingleLoopWait));
}
throw new Exception($"Timout of waiting for element to satisfy predicate: {predicate}!");
}
public static void SendKeysAndCheck(this IWebElement element, string text, TimeSpan? maxWait = null)
{
maxWait ??= DefaultMaxWait;
var i = 0;
while (i < maxWait.Value.TotalMilliseconds)
{
try
{
if (element.GetAttribute("value") != text)
{
element.SendKeys(text);
}
else
{
return;
}
}
catch (NoSuchElementException)
{
//swallow and wait for another loop
}
i += SingleLoopWait;
Thread.Sleep(TimeSpan.FromMilliseconds(SingleLoopWait));
}
throw new Exception("Could not set text value of element!");
}
}
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="SpecFlow.Plus.LivingDocPlugin" Version="3.7.10" />
<PackageReference Include="SpecFlow.NUnit" Version="3.7.13" />
<PackageReference Include="nunit" Version="3.13.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="95.0.4638.5401" />
</ItemGroup>
<ItemGroup>
<Folder Include="Drivers\" />
</ItemGroup>
<ItemGroup>
<None Update="msedgedriver">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,22 @@
@Duplicati
Feature: Duplicati local backup
Link to a feature: [Calculator](Duplicati.Browser.Test/Features/LocalBackup.feature)
***Further read***: **[Learn more about how to generate Living Documentation](https://docs.specflow.org/projects/specflow-livingdoc/en/latest/LivingDocGenerator/Generating-Documentation.html)**
Scenario: Configure Backup
Given there is local backup defined
When you run the backup
Then the result should be 120
Scenario Outline: Add two numbers permutations
Given the first number is <First number>
And the second number is <Second number>
When the two numbers are added
Then the result should be <Expected result>
Examples:
| First number | Second number | Expected result |
| 0 | 0 | 0 |
| -1 | 10 | 9 |
| 6 | 9 | 15 |
@@ -0,0 +1,171 @@
// ------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by SpecFlow (https://www.specflow.org/).
// SpecFlow Version:3.7.0.0
// SpecFlow Generator Version:3.7.0.0
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
// ------------------------------------------------------------------------------
#region Designer generated code
#pragma warning disable
namespace Duplicati.Browser.Test.Features
{
using TechTalk.SpecFlow;
using System;
using System.Linq;
[System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.7.0.0")]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[NUnit.Framework.TestFixtureAttribute()]
[NUnit.Framework.DescriptionAttribute("Duplicati local backup")]
[NUnit.Framework.CategoryAttribute("Duplicati")]
public partial class DuplicatiLocalBackupFeature
{
private TechTalk.SpecFlow.ITestRunner testRunner;
private string[] _featureTags = new string[] {
"Duplicati"};
#line 1 "LocalBackup.feature"
#line hidden
[NUnit.Framework.OneTimeSetUpAttribute()]
public virtual void FeatureSetup()
{
testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner();
TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "Duplicati local backup", @"Link to a feature: [Calculator](Duplicati.Browser.Test/Features/LocalBackup.feature)
***Further read***: **[Learn more about how to generate Living Documentation](https://docs.specflow.org/projects/specflow-livingdoc/en/latest/LivingDocGenerator/Generating-Documentation.html)**", ProgrammingLanguage.CSharp, new string[] {
"Duplicati"});
testRunner.OnFeatureStart(featureInfo);
}
[NUnit.Framework.OneTimeTearDownAttribute()]
public virtual void FeatureTearDown()
{
testRunner.OnFeatureEnd();
testRunner = null;
}
[NUnit.Framework.SetUpAttribute()]
public virtual void TestInitialize()
{
}
[NUnit.Framework.TearDownAttribute()]
public virtual void TestTearDown()
{
testRunner.OnScenarioEnd();
}
public virtual void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo)
{
testRunner.OnScenarioInitialize(scenarioInfo);
testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs<NUnit.Framework.TestContext>(NUnit.Framework.TestContext.CurrentContext);
}
public virtual void ScenarioStart()
{
testRunner.OnScenarioStart();
}
public virtual void ScenarioCleanup()
{
testRunner.CollectScenarioErrors();
}
[NUnit.Framework.TestAttribute()]
[NUnit.Framework.DescriptionAttribute("Configure Backup")]
public virtual void ConfigureBackup()
{
string[] tagsOfScenario = ((string[])(null));
System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Configure Backup", null, tagsOfScenario, argumentsOfScenario, this._featureTags);
#line 6
this.ScenarioInitialize(scenarioInfo);
#line hidden
bool isScenarioIgnored = default(bool);
bool isFeatureIgnored = default(bool);
if ((tagsOfScenario != null))
{
isScenarioIgnored = tagsOfScenario.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any();
}
if ((this._featureTags != null))
{
isFeatureIgnored = this._featureTags.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any();
}
if ((isScenarioIgnored || isFeatureIgnored))
{
testRunner.SkipScenario();
}
else
{
this.ScenarioStart();
#line 7
testRunner.Given("there is local backup defined", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given ");
#line hidden
#line 8
testRunner.When("you run the backup", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When ");
#line hidden
#line 9
testRunner.Then("the result should be 120", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
#line hidden
}
this.ScenarioCleanup();
}
[NUnit.Framework.TestAttribute()]
[NUnit.Framework.DescriptionAttribute("Add two numbers permutations")]
[NUnit.Framework.TestCaseAttribute("0", "0", "0", null)]
[NUnit.Framework.TestCaseAttribute("-1", "10", "9", null)]
[NUnit.Framework.TestCaseAttribute("6", "9", "15", null)]
public virtual void AddTwoNumbersPermutations(string firstNumber, string secondNumber, string expectedResult, string[] exampleTags)
{
string[] tagsOfScenario = exampleTags;
System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
argumentsOfScenario.Add("First number", firstNumber);
argumentsOfScenario.Add("Second number", secondNumber);
argumentsOfScenario.Add("Expected result", expectedResult);
TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Add two numbers permutations", null, tagsOfScenario, argumentsOfScenario, this._featureTags);
#line 12
this.ScenarioInitialize(scenarioInfo);
#line hidden
bool isScenarioIgnored = default(bool);
bool isFeatureIgnored = default(bool);
if ((tagsOfScenario != null))
{
isScenarioIgnored = tagsOfScenario.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any();
}
if ((this._featureTags != null))
{
isFeatureIgnored = this._featureTags.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any();
}
if ((isScenarioIgnored || isFeatureIgnored))
{
testRunner.SkipScenario();
}
else
{
this.ScenarioStart();
#line 13
testRunner.Given(string.Format("the first number is {0}", firstNumber), ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given ");
#line hidden
#line 14
testRunner.And(string.Format("the second number is {0}", secondNumber), ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And ");
#line hidden
#line 15
testRunner.When("the two numbers are added", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When ");
#line hidden
#line 16
testRunner.Then(string.Format("the result should be {0}", expectedResult), ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
#line hidden
}
this.ScenarioCleanup();
}
}
}
#pragma warning restore
#endregion
@@ -0,0 +1,23 @@
using Duplicati.Browser.Test.Drivers;
using Duplicati.Browser.Test.PageObjects;
using TechTalk.SpecFlow;
namespace Duplicati.Browser.Test.Hooks
{
/// <summary>
/// Calculator related hooks
/// </summary>
[Binding]
public class DuplicatiHooks
{
///<summary>
/// Reset the calculator before each scenario tagged with "Calculator"
/// </summary>
[BeforeScenario("Duplicati")]
public static void BeforeScenario(BrowserDriver browserDriver)
{
var calculatorPageObject = new DuplicatiPageObject(browserDriver.Current);
calculatorPageObject.Open();
}
}
}
@@ -0,0 +1,26 @@
using BoDi;
using Duplicati.Browser.Test.Drivers;
using TechTalk.SpecFlow;
namespace Duplicati.Browser.Test.Hooks
{
/// <summary>
/// Share the same browser window for all scenarios
/// </summary>
/// <remarks>
/// This makes the sequential execution of scenarios faster (opening a new browser window each time would take more time)
/// As a tradeoff:
/// - we cannot run the tests in parallel
/// - we have to "reset" the state of the browser before each scenario
/// </remarks>
[Binding]
public class SharedBrowserHooks
{
[BeforeTestRun]
public static void BeforeTestRun(ObjectContainer testThreadContainer)
{
//Initialize a shared BrowserDriver in the global container
testThreadContainer.BaseContainer.Resolve<BrowserDriver>();
}
}
}
@@ -0,0 +1,149 @@
using System.Threading;
using Duplicati.Browser.Test.Drivers;
using OpenQA.Selenium;
namespace Duplicati.Browser.Test.PageObjects;
public class AddBackupPage(IWebDriver webDriver)
{
private IWebElement NextButtonElement => webDriver.WaitForElement(By.ClassName("submit"));
public GeneralBackupSettingsPage ToGeneralSettings()
{
NextButtonElement.Click();
return new GeneralBackupSettingsPage(webDriver);
}
}
public class GeneralBackupSettingsPage(IWebDriver webDriver)
{
private IWebElement NameTextboxElement => webDriver.WaitForElement(By.Id("name"));
private IWebElement NextButtonElement => webDriver.WaitForElement(By.ClassName("submit"));
private IWebElement GeneratePasswordLink => webDriver.WaitForElement(By.PartialLinkText("Generuj"));
public GeneralBackupSettingsPage SetName(string name)
{
if (NameTextboxElement.WaitForElementBeing(e => e.GetAttribute("placeholder") is "Moje Zdjęcia" or "My Photos"))
{
NameTextboxElement.Clear();
NameTextboxElement.SendKeysAndCheck(name);
Thread.Sleep(1000);
NameTextboxElement.SendKeysAndCheck(name);
}
return this;
}
public GeneralBackupSettingsPage GenerateRandomPassword()
{
GeneratePasswordLink.Click();
return this;
}
public BackupTargetSettingsPage ToBackupTarget()
{
NextButtonElement.Click();
return new BackupTargetSettingsPage(webDriver);
}
}
public class BackupTargetSettingsPage(IWebDriver webDriver)
{
private IWebElement NextButtonElement => webDriver.WaitForElement(By.CssSelector("#nextStep2.submit"));
private IWebElement ManualPathLink => webDriver.WaitForElement(By.LinkText("Podaj ścieżkę ręcznie"));
private IWebElement FilePathTextbox => webDriver.WaitForElement(By.Id("file_path"));
public BackupTargetSettingsPage ChoosePathManually()
{
ManualPathLink.Click();
return this;
}
public BackupTargetSettingsPage SetManualPath(string path)
{
if (FilePathTextbox.WaitForElementBeing(e => e.GetAttribute("placeholder") is "Wprowadź ścieżkę docelową"))
{
FilePathTextbox.Clear();
FilePathTextbox.SendKeysAndCheck(path);
}
return this;
}
public BackupSourceSettingsPage ToBackupSource()
{
NextButtonElement.Click();
return new BackupSourceSettingsPage(webDriver);
}
}
public class BackupSourceSettingsPage(IWebDriver webDriver)
{
private IWebElement NextButtonElement => webDriver.WaitForElement(By.CssSelector("#nextStep3.submit"));
private IWebElement FilePathTextbox => webDriver.WaitForElement(By.Id("sourcePath"));
private IWebElement AddFilePathButton => webDriver.WaitForElement(By.Id("sourceFolderPathAdd"));
private IWebElement ConfirmAddPathButton => webDriver.WaitForElement(By.PartialLinkText("Tak"));
public BackupSourceSettingsPage AddSourceManually(string path)
{
if (FilePathTextbox.WaitForElementBeing(e => e.GetAttribute("placeholder") is "Dodaj ścieżkę bezpośrednio"))
{
FilePathTextbox.Clear();
FilePathTextbox.SendKeysAndCheck(path);
}
AddFilePathButton.Click();
ConfirmAddPathButton.Click();
return this;
}
public ScheduleSettingsPage ToSchedule()
{
Thread.Sleep(20000);
NextButtonElement.Click();
return new ScheduleSettingsPage(webDriver);
}
}
public class ScheduleSettingsPage(IWebDriver webDriver)
{
private IWebElement NextButtonElement => webDriver.WaitForElement(By.CssSelector("#nextStep4.submit"));
private IWebElement AutoRunCheckbox => webDriver.WaitForElement(By.Id("useScheduleRun"));
public ScheduleSettingsPage DisableAutorun()
{
if (AutoRunCheckbox.Selected)
{
AutoRunCheckbox.Click();
}
return this;
}
public ToOptionsSettingsPage ToOptions()
{
NextButtonElement.Click();
return new ToOptionsSettingsPage(webDriver);
}
}
public class ToOptionsSettingsPage(IWebDriver webDriver)
{
private IWebElement SaveButtonElement => webDriver.WaitForElement(By.CssSelector("#save"));
private IWebElement ConfirmationModal => webDriver.WaitForElement(By.CssSelector(".content.buttons"));
public ToOptionsSettingsPage Save()
{
SaveButtonElement.Click();
ConfirmationModal.WaitForElement(By.CssSelector("ul>li:nth-child(2)>a")).Click();
return this;
}
}

Some files were not shown because too many files have changed in this diff Show More