diff --git a/.appveyor.yml b/.appveyor.yml
deleted file mode 100644
index aa650fb0a..000000000
--- a/.appveyor.yml
+++ /dev/null
@@ -1,66 +0,0 @@
-version: 1.0.{build}
-image: Visual Studio 2017
-configuration: Debug
-platform: Any CPU
-environment:
- AUTOUPDATER_Duplicati_SKIP_UPDATE: 1
- COVERALLS_REPO_TOKEN:
- secure: szQsrkP5rvra8L60SomD73/dFvRwot0UuyA566zqzI2qmLOr+MhLN0AyC8BTbhYY
-before_build:
- - ps: $env:UNITTEST_BASEFOLDER = "$Env:APPVEYOR_BUILD_FOLDER" + "\testdata"
- - cmd: nuget restore Duplicati.sln
-build:
- project: Duplicati.sln
- verbosity: minimal
-test_script:
- - ps: |
- nuget install OpenCover -Version 4.7.922 -OutputDirectory packages
- nuget install coveralls.net -Version 0.7.0 -OutputDirectory packages
- nuget install NUnit.Runners -Version 3.10.0 -OutputDirectory packages
-
- $testDir = ".\Duplicati\UnitTest\bin\Debug"
- $tests = @("$testDir\Duplicati.UnitTest.dll")
- $testsPassed = $true
-
- foreach ($elem in $tests) {
- $testResultsFile = "$elem" + ".xml"
-
- .\packages\OpenCover.4.7.922\tools\OpenCover.Console.exe `
- -register:user `
- -target:.\packages\NUnit.ConsoleRunner.3.10.0\tools\nunit3-console.exe `
- "-targetargs:""$elem"" /result:""$testResultsFile""" `
- "-filter:+[Duplicati.Library.Main]* -[UnitTest]*" `
- -output:opencover.xml `
- -returntargetcode `
-
- # Keep track of test failures so that we can throw an exception after
- # uploading coverage and test results.
- $testsPassed = $testsPassed -and $?
-
- # Upload test results to AppVeyor.
- $wc = New-Object "System.Net.WebClient"
- $wc.UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\""$testResultsFile""))
- }
-
- if ($Env:COVERALLS_REPO_TOKEN) {
- $revision = git rev-parse HEAD
- $branch = git rev-parse --abbrev-ref HEAD
- $commitAuthor = git show --quiet --format="%aN" $revision
- $commitEmail = git show --quiet --format="%aE" $revision
- $commitMessage = git show --quiet --format="%s" $revision
-
- .\packages\coveralls.net.0.7.0\tools\csmacnz.Coveralls.exe `
- --opencover -i opencover.xml `
- --repoToken "$Env:COVERALLS_REPO_TOKEN" `
- --commitId "$revision" `
- --commitBranch "$branch" `
- --commitAuthor "$commitAuthor" `
- --commitEmail "$commitEmail" `
- --commitMessage "$commitMessage" `
- --useRelativePaths `
- --basePath .\Duplicati\UnitTest\bin\Debug
- }
-
- if (!$testsPassed) {
- throw "Tests failed."
- }
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..a64fc502b
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+*.csproj text
+*.sh text eol=lf
+#The #! in rules won't work if CRLF
+/ReleaseBuilder/Resources/debian/* text eol=lf
+/ReleaseBuilder/Resources/fedora/*.sh text eol=lf
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index a3647f601..3d627b1dc 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,5 +1,6 @@
-
+
+
diff --git a/.github/workflows/buildsrelease.yml b/.github/workflows/buildsrelease.yml
deleted file mode 100644
index e3a22c463..000000000
--- a/.github/workflows/buildsrelease.yml
+++ /dev/null
@@ -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/**/*
-
diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml
new file mode 100644
index 000000000..977a0a9ea
--- /dev/null
+++ b/.github/workflows/freebsd.yml
@@ -0,0 +1,143 @@
+name: Freebsd
+on:
+ push:
+ branches: "**freebsd**"
+ pull_request:
+ branches: "**freebsd**"
+
+jobs:
+ zip:
+ runs-on: macos-12
+ steps:
+ - uses: actions/checkout@v2
+ - name: Download dotnet
+ # See also https://github.com/sec/dotnet-core-freebsd-source-build
+ # See also https://github.com/Thefrank/dotnet-freebsd-crossbuild
+ run: curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/v6.0.401-11/dotnet-sdk-6.0.401-freebsd-x64.tar.gz -o dn.tgz
+ - name: Build
+ id: test
+ uses: vmactions/freebsd-vm@v0
+ with:
+ usesh: true
+ # sync: sshfs
+ prepare: pkg install -y curl libinotify libunwind git cmake ninja bash wget icu
+ run: |
+ #!/bin/bash
+ set -x
+ set -e
+ pwd
+ ls -lah
+ whoami
+ env
+ freebsd-version
+ df -h
+ dmesg | grep memory
+ swapinfo
+ dd if=/dev/zero of=/root/swap1 bs=1m count=20480
+ chmod 0600 /root/swap1
+ mdconfig -a -t vnode -f /root/swap1 -u 0
+ swapon /dev/md0
+ df -h
+ swapinfo
+
+ export DOTNET_CLI_TELEMETRY_OPTOUT=1
+ BUILD_DIR=`pwd`
+ cd /var/tmp
+ mkdir dn
+ cd dn
+ echo Unpacking .NET
+ tar -xzf ${BUILD_DIR}/dn.tgz
+ cd ${BUILD_DIR}
+ mkdir /var/tmp/nuget
+ export RVERSION=v6.0.401-11
+ export VERSION=6.0.9
+ curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.AspNetCore.App.Runtime.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.AspNetCore.App.Runtime.freebsd-x64.${VERSION}.nupkg
+ curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.NETCore.App.Host.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.NETCore.App.Host.freebsd-x64.${VERSION}.nupkg
+ curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.NETCore.App.Runtime.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.NETCore.App.Runtime.freebsd-x64.${VERSION}.nupkg
+ curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.NETCore.App.Crossgen2.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.NETCore.App.Crossgen2.freebsd-x64.${VERSION}.nupkg
+ /var/tmp/dn/dotnet nuget add source /var/tmp/nuget/
+
+ cp -R `pwd` /var/tmp/build
+ cd /var/tmp/build
+
+ echo Restore
+ /var/tmp/dn/dotnet restore --runtime=freebsd-x64 Duplicati.sln
+ echo Build
+ /var/tmp/dn/dotnet build Duplicati.sln
+ echo Publish
+ /var/tmp/dn/dotnet publish -c Release --runtime=freebsd-x64 -o publish Duplicati.sln
+
+ cp -R ./publish ${BUILD_DIR}/publish
+ - name: Save Artifacts
+ uses: actions/upload-artifact@v2
+ with:
+ name: duplicati-freebsd-x64
+ path: publish
+ tests:
+ runs-on: macos-12
+ steps:
+ - uses: actions/checkout@v2
+ - name: Download dotnet
+ # See also https://github.com/sec/dotnet-core-freebsd-source-build
+ # See also https://github.com/Thefrank/dotnet-freebsd-crossbuild
+ run: curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/v6.0.401-11/dotnet-sdk-6.0.401-freebsd-x64.tar.gz -o dn.tgz
+ - name: Run test
+ id: test
+ uses: vmactions/freebsd-vm@v0
+ with:
+ usesh: true
+ # sync: sshfs
+ prepare: pkg install -y curl libinotify libunwind git cmake ninja bash wget icu
+ run: |
+ #!/bin/bash
+ set -x
+ set -e
+ pwd
+ ls -lah
+ whoami
+ env
+ freebsd-version
+ df -h
+ dmesg | grep memory
+ swapinfo
+ dd if=/dev/zero of=/root/swap1 bs=1m count=20480
+ chmod 0600 /root/swap1
+ mdconfig -a -t vnode -f /root/swap1 -u 0
+ swapon /dev/md0
+ df -h
+ swapinfo
+
+ export DOTNET_CLI_TELEMETRY_OPTOUT=1
+ BUILD_DIR=`pwd`
+ cd /var/tmp
+ mkdir dn
+ cd dn
+ echo Unpacking .NET
+ tar -xzf ${BUILD_DIR}/dn.tgz
+ cd ${BUILD_DIR}
+ mkdir /var/tmp/nuget
+ export RVERSION=v6.0.401-11
+ export VERSION=6.0.9
+ curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.AspNetCore.App.Runtime.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.AspNetCore.App.Runtime.freebsd-x64.${VERSION}.nupkg
+ curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.NETCore.App.Host.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.NETCore.App.Host.freebsd-x64.${VERSION}.nupkg
+ curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.NETCore.App.Runtime.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.NETCore.App.Runtime.freebsd-x64.${VERSION}.nupkg
+ curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.NETCore.App.Crossgen2.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.NETCore.App.Crossgen2.freebsd-x64.${VERSION}.nupkg
+ /var/tmp/dn/dotnet nuget add source /var/tmp/nuget/
+
+ cp -R `pwd` /var/tmp/build
+ cd /var/tmp/build
+
+ echo Restore
+ /var/tmp/dn/dotnet restore --runtime=freebsd-x64 Duplicati.sln
+ echo Testing
+ mkdir -p ${BUILD_DIR}/duplicati_testdata/logs
+ mkdir -p /var/tmp/duplicati_testdata
+ ln -s ${BUILD_DIR}/duplicati_testdata/logs /var/tmp/duplicati_testdata/logs
+ export UNITTEST_BASEFOLDER=/var/tmp/duplicati_testdata
+ /var/tmp/dn/dotnet test --runtime=freebsd-x64 --verbosity minimal Duplicati.sln
+ - name: Archive Test Logs
+ if: always()
+ uses: actions/upload-artifact@v2
+ with:
+ name: test-logs
+ path: duplicati_testdata/logs/*
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
new file mode 100644
index 000000000..c7e693e2f
--- /dev/null
+++ b/.github/workflows/stale.yml
@@ -0,0 +1,25 @@
+name: Close inactive issues
+on:
+ schedule:
+ - cron: "30 1 * * *"
+
+jobs:
+ close-issues:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ steps:
+ - uses: actions/stale@v8
+ with:
+ days-before-issue-stale: 15
+ days-before-issue-close: 15
+ stale-issue-label: "stale"
+ stale-issue-message: "This issue is stale because it has been open for 15 days with no activity."
+ close-issue-message: "This issue was closed because it has been inactive for 15 days since being marked as stale."
+ days-before-pr-stale: -1
+ days-before-pr-close: -1
+ exempt-all-assignees: true
+ 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 }}
+
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 9e5d0c0b1..fae5c3ea5 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -2,9 +2,6 @@ name: Tests
on: [push, pull_request]
-env:
- build_directory: "Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release"
-
jobs:
unit_tests:
name: Unit tests
@@ -12,84 +9,29 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [ ubuntu-latest, windows-latest, macos-latest ]
+ os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- - name: Add msbuild to PATH (Windows)
- if: matrix.os == 'windows-latest'
- uses: microsoft/setup-msbuild@v1
+ - name: Set up .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.x
- - name: Install NuGet
- uses: nuget/setup-nuget@v1
+ - name: Checkout source
+ uses: actions/checkout@v4
- - name: Checkout Source
- uses: actions/checkout@v3
+ - name: Restore NuGet dependencies
+ run: dotnet restore Duplicati.sln
- - name: Restore NuGet Packages
- run: nuget restore Duplicati.sln
+ - name: Build Duplicati
+ run: dotnet build --no-restore Duplicati.sln
- - name: Install Additional Dependencies
- if: matrix.os == 'ubuntu-latest'
- run: sudo apt-get install gtk-sharp2
+ - name: Run unit tests
+ run: dotnet test --no-build --verbosity minimal Duplicati.sln
- - name: Build Duplicati (Linux and macOS)
- if: matrix.os != 'windows-latest'
- run: msbuild -p:Configuration=Release -p:DefineConstants=ENABLE_GTK Duplicati.sln
-
- - name: Build Duplicati (Windows)
- if: matrix.os == 'windows-latest'
- run: msbuild -p:Configuration=Release Duplicati.sln
-
- - name: Run Unit Tests (Linux and macOS)
- if: matrix.os != 'windows-latest'
- run: |
- export DEBUG_OUTPUT=1
- nuget install NUnit.Runners -Version 3.12.0 -OutputDirectory nunit
- mono nunit/NUnit.ConsoleRunner.3.12.0/tools/nunit3-console.exe Duplicati/UnitTest/bin/Release/Duplicati.UnitTest.dll
-
- - name: Run Unit Tests (Windows)
- if: matrix.os == 'windows-latest'
- run: |
- nuget install NUnit.Runners -Version 3.12.0 -OutputDirectory nunit
- nunit/NUnit.ConsoleRunner.3.12.0/tools/nunit3-console.exe Duplicati/UnitTest/bin/Release/Duplicati.UnitTest.dll
-
- ui_tests:
- name: UI tests
+ selenium:
runs-on: ubuntu-latest
-
- # Some of these steps duplicate those in the unit_tests job above. This
- # can be cleaned up using a composite run steps action once it supports
- # what we need (https://github.com/actions/runner/issues/646).
steps:
- - name: Checkout Source
- uses: actions/checkout@v3
-
- - name: Install NuGet
- uses: nuget/setup-nuget@v1
-
- - name: Restore NuGet Packages
- run: nuget restore Duplicati.sln
-
- - name: Install Additional Dependencies
- run: sudo apt-get install gtk-sharp2
-
- - name: Build Duplicati
- run: msbuild -p:Configuration=Release -p:DefineConstants=ENABLE_GTK Duplicati.sln
-
- - name: Set up Python
- uses: actions/setup-python@v4
- with:
- python-version: 3.x
-
- - name: Install dependencies
- run: |
- pip3 install selenium
- pip3 install --upgrade urllib3
- pip3 install chromedriver-autoinstaller
-
- - name: Run UI tests
- run: |
- cp -r Duplicati/Server/webroot "${{ env.build_directory }}"/webroot
- mono "${{ env.build_directory }}"/Duplicati.Server.exe &
- sleep 30
- python3 guiTests/guiTest.py --use-chrome
+ - uses: actions/checkout@v4
+ - name: Selenium
+ run: pipeline/selenium/test.sh
diff --git a/.gitignore b/.gitignore
index 7ba91052e..2e3ba0263 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,24 +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/
-
-Installer/debian/*.buildinfo
+/publish/
+ReleaseBuilder/build-temp/
+ReleaseBuilder/Resources/Windows/binfiles.wxs
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 225209504..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,83 +0,0 @@
-language: minimal
-services: docker
-before_install:
- - sudo apt-get install moreutils
- - set -o pipefail
-addons:
- coverity_scan:
- project:
- name: "duplicati/duplicati"
- description: "Duplicati Build submitted via Travis CI"
- notification_email: kenneth@duplicati.com
- build_command_prepend: "msbuild /t:Clean"
- build_command: "msbuild /t:Rebuild"
- branch_pattern: coverity_scan
-env:
- global:
- # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created
- # via the "travis encrypt" command using the project repo's public key
- - secure: "ijaQDcVcMo/utuP514FiDZVGR5LJbATWmYDmgRIIaI0YG2Rk6uuJp9XXDXr8sx66JWttPGgs7Htqfy50VKHdr7nsfyCQCRNGmqF+OJeUgAQMj5spxn9A7PZY9+GVhnXoJ06wvYjCv5MPeLBMDK/KpIM5Ny5IG+TKzQy4U7Dyg0riMUT61aqzxSjTGfHZPmaqBICJTzsRmeGHglemFUz/1B4NAN+IeAtXoH9YIfA2LjJfYmMLckPgFQLKN/tQRaGPWXh77y0qVgA++K8UVu5W3yGo3JXBPDdabGgKs9BPlfN5/Ixnkcy6ekAubDA/Aspur9x7/szj4/AnGMY6frNB33nQcnJQZ/51GYKr6CXCmjoJQuWc8ZKdEMqDn3w/kyEzvluSjAsJNRVboeWPE4VfpAeUBsjQ+TuEQi8WXhN8VJgZV7yCkVlZTVbhtOxMPsBiFb74lpWC2iQuWLJv9ED6vyXZVzZhNtfjXNXNZzIwYxbcd7I0ZQ8fmDstFnvBJveJby2ZRpECXTbc03eIUq1SAaVJJY2r47RD1PwVqM3xvUTljIqtKdyIz915mEOjqG1BnCDaiGHYJ4byuDDeIUZO9X70s60Gh4hupZA2Ruup0v7SfYgkto6aNcStd0NYd716Vd7hqXNTp6nu01L5YYJDY4h5sHyBXVXbxCKlUb01kUw="
- - USAGEREPORTER_Duplicati_LEVEL=none
- - AUTOUPDATER_Duplicati_SKIP_UPDATE=1
- - BUILD_DIR=${TRAVIS_BUILD_DIR}/../.build
- - TEST_DIR=${TRAVIS_BUILD_DIR}/../.test
- - ROOT_DIR="$TRAVIS_BUILD_DIR"
-
-# matrix expansion within jobs would be good:
-# https://github.com/travis-ci/travis-ci/issues/8295
-
-# There is one cache per branch, so concurrent builds on different branches possible!
-# DO NOT SET ENVIRONMENT VARIABLES WITHIN JOBS, OR CACHES WILL NOT BE SHARED AMONGST JOBS!
-
-
-jobs:
- include:
- - stage: build
- cache:
- directories:
- - $BUILD_DIR
- script:
- - ${ROOT_DIR}/pipeline/jobs/build_job.sh
-
-
- - stage: tests
- cache:
- directories:
- - $BUILD_DIR
- script:
- - ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories BulkNormal
- - stage: tests
- cache:
- directories:
- - $BUILD_DIR
- script:
- - ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories BulkNoSize
- - stage: tests
- cache:
- directories:
- - $BUILD_DIR
- script:
- - ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories SVNDataLong,SVNData,RecoveryTool,ProblematicPath,SymLink
- - stage: tests
- cache:
- directories:
- - $BUILD_DIR
- script:
- - travis_wait ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Border
- - stage: tests
- cache:
- directories:
- - $BUILD_DIR
- script:
- - ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Filter,Targeted,Purge,Serialization,WebApi,Utility,UriUtility,IO,ImportExport,Disruption,RestoreHandler,RepairHandler,DeleteHandler,Controller
-
- - stage: tests
- cache:
- directories:
- - $BUILD_DIR
- script:
- - ${ROOT_DIR}/pipeline/jobs/selenium_job.sh
- addons:
- sauce_connect:
- username: $SAUCE_USERNAME
- access_key: $SAUCE_ACCESS_KEY
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..bbcfbf328
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,59 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch TrayIcon executable",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ "program": "${workspaceFolder}/Executables/net8/Duplicati.GUI.TrayIcon/bin/Debug/net8.0/Duplicati.GUI.TrayIcon",
+ "args": [],
+ "cwd": "${workspaceFolder}",
+ "stopAtEntry": false,
+ "console": "internalConsole"
+ },
+ {
+ "name": "Launch Server executable",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ "program": "${workspaceFolder}/Executables/net8/Duplicati.Server/bin/Debug/net8.0/Duplicati.Server",
+ "args": [],
+ "cwd": "${workspaceFolder}",
+ "stopAtEntry": false,
+ "console": "internalConsole"
+ },
+ {
+ "name": "Launch CommandLine executable",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ "program": "${workspaceFolder}/Executables/net8/Duplicati.Server/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": "C#: Duplicati.GUI.TrayIcon Debug",
+ "type": "dotnet",
+ "request": "launch",
+ "projectPath": "${workspaceFolder}/Executables/net8/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj"
+ }
+
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 000000000..31c32bd34
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,24 @@
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "dotnet",
+ "type": "shell",
+ "args": [
+ "build",
+ // Ask dotnet build to generate full paths for file names.
+ "/property:GenerateFullPaths=true",
+ // Do not generate summary otherwise it leads to duplicate errors in Problems panel
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "group": "build",
+ "presentation": {
+ "reveal": "silent"
+ },
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.csproj b/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.csproj
deleted file mode 100644
index 4a6f240d5..000000000
--- a/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.csproj
+++ /dev/null
@@ -1,77 +0,0 @@
-
-
-
- Debug
- AnyCPU
- {17E3EB29-87FD-4BE9-A531-8DAD8B295494}
- Exe
- False
- AutoUpdateBuilder
- AutoUpdateBuilder
- v4.7.1
-
-
-
- true
- full
- false
- bin\Debug
- DEBUG;
- prompt
- 4
- false
-
-
- full
- true
- bin\Release
- prompt
- 4
- false
-
-
-
- packages\Newtonsoft.Json.13.0.2\lib\net45\Newtonsoft.Json.dll
-
-
-
-
-
-
-
-
-
-
- {19ECCE09-B5EB-406C-8C57-BAC66997D469}
- Duplicati.Library.Compression
-
-
- {94484FDB-2EFA-4CF0-9BE6-A561157B4F87}
- Duplicati.Library.Encryption
-
-
- {C5899F45-B0FF-483C-9D38-24A9FCAAB237}
- Duplicati.Library.Interface
-
-
- {D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}
- Duplicati.Library.Logging
-
-
- {DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}
- Duplicati.Library.Utility
-
-
- {7E119745-1F62-43F0-936C-F312A1912C0B}
- Duplicati.Library.AutoUpdater
-
-
- {4D012CB1-4B92-47F4-89B7-BF80A73A2E99}
- Duplicati.License
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.sln b/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.sln
deleted file mode 100644
index 667b5cf3a..000000000
--- a/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.sln
+++ /dev/null
@@ -1,74 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 2012
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoUpdateBuilder", "AutoUpdateBuilder.csproj", "{17E3EB29-87FD-4BE9-A531-8DAD8B295494}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Compression", "..\..\Duplicati\Library\Compression\Duplicati.Library.Compression.csproj", "{19ECCE09-B5EB-406C-8C57-BAC66997D469}"
-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.Interface", "..\..\Duplicati\Library\Interface\Duplicati.Library.Interface.csproj", "{C5899F45-B0FF-483C-9D38-24A9FCAAB237}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Logging", "..\..\Duplicati\Library\Logging\Duplicati.Library.Logging.csproj", "{D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Encryption", "..\..\Duplicati\Library\Encryption\Duplicati.Library.Encryption.csproj", "{94484FDB-2EFA-4CF0-9BE6-A561157B4F87}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.AutoUpdater", "..\..\Duplicati\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj", "{7E119745-1F62-43F0-936C-F312A1912C0B}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.License", "..\..\Duplicati\License\Duplicati.License.csproj", "{4D012CB1-4B92-47F4-89B7-BF80A73A2E99}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Localization", "..\..\Duplicati\Library\Localization\Duplicati.Library.Localization.csproj", "{B68F2214-951F-4F78-8488-66E1ED3F50BF}"
-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
- {17E3EB29-87FD-4BE9-A531-8DAD8B295494}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {17E3EB29-87FD-4BE9-A531-8DAD8B295494}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {17E3EB29-87FD-4BE9-A531-8DAD8B295494}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {17E3EB29-87FD-4BE9-A531-8DAD8B295494}.Release|Any CPU.Build.0 = Release|Any CPU
- {19ECCE09-B5EB-406C-8C57-BAC66997D469}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {19ECCE09-B5EB-406C-8C57-BAC66997D469}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {19ECCE09-B5EB-406C-8C57-BAC66997D469}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {19ECCE09-B5EB-406C-8C57-BAC66997D469}.Release|Any CPU.Build.0 = Release|Any CPU
- {4D012CB1-4B92-47F4-89B7-BF80A73A2E99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4D012CB1-4B92-47F4-89B7-BF80A73A2E99}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4D012CB1-4B92-47F4-89B7-BF80A73A2E99}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4D012CB1-4B92-47F4-89B7-BF80A73A2E99}.Release|Any CPU.Build.0 = Release|Any CPU
- {7E119745-1F62-43F0-936C-F312A1912C0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7E119745-1F62-43F0-936C-F312A1912C0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7E119745-1F62-43F0-936C-F312A1912C0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7E119745-1F62-43F0-936C-F312A1912C0B}.Release|Any CPU.Build.0 = Release|Any CPU
- {94484FDB-2EFA-4CF0-9BE6-A561157B4F87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {94484FDB-2EFA-4CF0-9BE6-A561157B4F87}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {94484FDB-2EFA-4CF0-9BE6-A561157B4F87}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {94484FDB-2EFA-4CF0-9BE6-A561157B4F87}.Release|Any CPU.Build.0 = Release|Any CPU
- {B68F2214-951F-4F78-8488-66E1ED3F50BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B68F2214-951F-4F78-8488-66E1ED3F50BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B68F2214-951F-4F78-8488-66E1ED3F50BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B68F2214-951F-4F78-8488-66E1ED3F50BF}.Release|Any CPU.Build.0 = Release|Any CPU
- {C5899F45-B0FF-483C-9D38-24A9FCAAB237}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C5899F45-B0FF-483C-9D38-24A9FCAAB237}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C5899F45-B0FF-483C-9D38-24A9FCAAB237}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C5899F45-B0FF-483C-9D38-24A9FCAAB237}.Release|Any CPU.Build.0 = Release|Any CPU
- {D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}.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 = AutoUpdateBuilder.csproj
- EndGlobalSection
-EndGlobal
diff --git a/BuildTools/AutoUpdateBuilder/Program.cs b/BuildTools/AutoUpdateBuilder/Program.cs
deleted file mode 100644
index 4737406d2..000000000
--- a/BuildTools/AutoUpdateBuilder/Program.cs
+++ /dev/null
@@ -1,163 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Security.Cryptography;
-
-namespace AutoUpdateBuilder
-{
- public class Program
- {
- private static RSACryptoServiceProvider privkey;
-
- private static string keyfile;
- private static string keyfilepassword;
-
- private static void CompareToManifestPublicKey()
- {
- if (Duplicati.Library.AutoUpdater.AutoUpdateSettings.SignKey == null || privkey.ToXmlString(false) != Duplicati.Library.AutoUpdater.AutoUpdateSettings.SignKey.ToXmlString(false))
- {
- Console.WriteLine("The public key in the project is not the same as the public key from the file");
- Console.WriteLine("Try setting the key to: ");
- Console.WriteLine(privkey.ToXmlString(false));
- System.Environment.Exit(5);
- }
- }
-
- private static void LoadKeyFromFile()
- {
- using (var enc = new Duplicati.Library.Encryption.AESEncryption(keyfilepassword, new Dictionary()))
- using (var ms = new System.IO.MemoryStream())
- using (var fs = System.IO.File.OpenRead(keyfile))
- {
- enc.Decrypt(fs, ms);
- ms.Position = 0;
-
- using (var sr = new System.IO.StreamReader(ms))
- privkey.FromXmlString(sr.ReadToEnd());
- }
- }
-
- public static int Main(string[] _args)
- {
- var args = new List(_args);
- var opts = Duplicati.Library.Utility.CommandLineParser.ExtractOptions(args);
-
- opts.TryGetValue("input", out string inputfolder);
- opts.TryGetValue("output", out string outputfolder);
- opts.TryGetValue("allow-new-key", out string allowNewKey);
- opts.TryGetValue("keyfile", out keyfile);
- opts.TryGetValue("manifest", out string manifestfile);
- opts.TryGetValue("keyfile-password", out keyfilepassword);
-
- var usedoptions = new [] { "allow-new-key", "input", "output", "keyfile", "manifest", "keyfile-password" };
-
- if (string.IsNullOrWhiteSpace(inputfolder))
- {
- Console.WriteLine("Missing input folder");
- return 4;
- }
-
- if (string.IsNullOrWhiteSpace(outputfolder))
- {
- Console.WriteLine("Missing output folder");
- return 4;
- }
-
- if (string.IsNullOrWhiteSpace(keyfile))
- {
- Console.WriteLine("Missing keyfile");
- return 4;
- }
-
- if (!System.IO.Directory.Exists(inputfolder))
- {
- Console.WriteLine("Input folder not found");
- return 4;
- }
-
- if (string.IsNullOrWhiteSpace(keyfilepassword))
- {
- Console.WriteLine("Enter keyfile passphrase: ");
- keyfilepassword = Console.ReadLine().Trim();
- }
-
- if (!System.IO.File.Exists(keyfile))
- {
- Console.WriteLine("Keyfile not found, creating new");
- var newkey = RSA.Create().ToXmlString(true);
- using (var enc = new Duplicati.Library.Encryption.AESEncryption(keyfilepassword, new Dictionary()))
- using (var fs = System.IO.File.OpenWrite(keyfile))
- using (var ms = new System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(newkey)))
- enc.Encrypt(ms, fs);
- }
-
- if (!System.IO.Directory.Exists(outputfolder))
- System.IO.Directory.CreateDirectory(outputfolder);
-
- privkey = (RSACryptoServiceProvider) RSA.Create();
-
- LoadKeyFromFile();
-
- if (!Boolean.TryParse(allowNewKey, out Boolean newKeyAllowed) || !newKeyAllowed)
- {
- CompareToManifestPublicKey();
- }
-
- Duplicati.Library.AutoUpdater.UpdateInfo updateInfo;
-
- using (var fs = System.IO.File.OpenRead(manifestfile))
- using (var sr = new System.IO.StreamReader(fs))
- using (var jr = new Newtonsoft.Json.JsonTextReader(sr))
- updateInfo = new Newtonsoft.Json.JsonSerializer().Deserialize(jr);
-
- var isopts = new Dictionary(opts, StringComparer.InvariantCultureIgnoreCase);
- foreach (var usedopt in usedoptions)
- {
- isopts.Remove(usedopt);
- }
-
- foreach (var k in updateInfo.GetType().GetFields())
- {
- if (!isopts.ContainsKey(k.Name))
- {
- continue;
- }
- try
- {
- //Console.WriteLine("Setting {0} to {1}", k.Name, isopts[k.Name]);
- if (k.FieldType == typeof(string[]))
- k.SetValue(updateInfo, isopts[k.Name].Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries));
- else if (k.FieldType == typeof(Version))
- k.SetValue(updateInfo, new Version(isopts[k.Name]));
- else if (k.FieldType == typeof(int))
- k.SetValue(updateInfo, int.Parse(isopts[k.Name]));
- else if (k.FieldType == typeof(long))
- k.SetValue(updateInfo, long.Parse(isopts[k.Name]));
- else
- k.SetValue(updateInfo, isopts[k.Name]);
- }
- catch (Exception ex)
- {
- Console.WriteLine("Failed setting {0} to {1}: {2}", k.Name, isopts[k.Name], ex.Message);
- }
-
- isopts.Remove(k.Name);
- }
-
- foreach (var opt in isopts)
- {
- Console.WriteLine("Warning! unused option: {0} = {1}", opt.Key, opt.Value);
- }
-
- using (var tf = new Duplicati.Library.Utility.TempFile())
- {
- using (var fs = System.IO.File.OpenWrite(tf))
- using (var tw = new System.IO.StreamWriter(fs))
- new Newtonsoft.Json.JsonSerializer().Serialize(tw, updateInfo);
-
- Duplicati.Library.AutoUpdater.UpdaterManager.CreateUpdatePackage(privkey, inputfolder, outputfolder, tf);
- }
-
- return 0;
- }
- }
-}
\ No newline at end of file
diff --git a/BuildTools/AutoUpdateBuilder/Properties/AssemblyInfo.cs b/BuildTools/AutoUpdateBuilder/Properties/AssemblyInfo.cs
deleted file mode 100644
index d053718e1..000000000
--- a/BuildTools/AutoUpdateBuilder/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-
-// Information about this assembly is defined by the following attributes.
-// Change them to the values specific to your project.
-
-[assembly: AssemblyTitle("AutoUpdateBuilder")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("")]
-[assembly: AssemblyCopyright("")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
-// The form "{Major}.{Minor}.*" will automatically update the build and revision,
-// and "{Major}.{Minor}.{Build}.*" will update just the revision.
-
-[assembly: AssemblyVersion("2.0.0.7")]
-
-// The following attributes are used to specify the signing key for the assembly,
-// if desired. See the Mono documentation for more information about signing.
-
-//[assembly: AssemblyDelaySign(false)]
-//[assembly: AssemblyKeyFile("")]
-
diff --git a/BuildTools/AutoUpdateBuilder/app.config b/BuildTools/AutoUpdateBuilder/app.config
deleted file mode 100644
index d7c60fd53..000000000
--- a/BuildTools/AutoUpdateBuilder/app.config
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/BuildTools/AutoUpdateBuilder/packages.config b/BuildTools/AutoUpdateBuilder/packages.config
deleted file mode 100644
index c241b9af5..000000000
--- a/BuildTools/AutoUpdateBuilder/packages.config
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/BuildTools/DependencyFinder/DependencyFinder.csproj b/BuildTools/DependencyFinder/DependencyFinder.csproj
deleted file mode 100644
index 4274d9a60..000000000
--- a/BuildTools/DependencyFinder/DependencyFinder.csproj
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
- Debug
- x86
- {3E891818-579A-4C06-8066-4C702BFA5921}
- Exe
- False
- DependencyFinder
- DependencyFinder
- v4.7.1
-
-
-
- true
- full
- false
- bin\Debug
- DEBUG;
- prompt
- 4
- x86
- false
- ../../../../Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Debug
-
-
- full
- true
- bin\Release
- prompt
- 4
- true
- x86
-
-
-
- packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll
-
-
- packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll
-
-
- packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll
-
-
- packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Rocks.dll
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/BuildTools/DependencyFinder/DependencyFinder.sln b/BuildTools/DependencyFinder/DependencyFinder.sln
deleted file mode 100644
index cd8aea67c..000000000
--- a/BuildTools/DependencyFinder/DependencyFinder.sln
+++ /dev/null
@@ -1,17 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 2012
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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
diff --git a/BuildTools/DependencyFinder/Program.cs b/BuildTools/DependencyFinder/Program.cs
deleted file mode 100644
index e819cac0b..000000000
--- a/BuildTools/DependencyFinder/Program.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-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 assemblies, TextWriter writer, IEnumerable searchfolders)
- {
- var resolver = new DefaultAssemblyResolver();
- searchfolders.ToList().ForEach(x => resolver.AddSearchDirectory(x));
-
- //writer.WriteLine("digraph Dependencies {");
- var loaded = assemblies
- .SelectMany(a => a.Modules.Cast())
- .SelectMany(m => m.AssemblyReferences.Cast())
- .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.FullName); }
- 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())
- writer.WriteLine(@"""{0}"" -> ""{1}"";", r.Name, a.Name.Name);
- } );*/
-
- //writer.WriteLine("}");
- }
-
-
- }
-}
diff --git a/BuildTools/DependencyFinder/Properties/AssemblyInfo.cs b/BuildTools/DependencyFinder/Properties/AssemblyInfo.cs
deleted file mode 100644
index 334f017e8..000000000
--- a/BuildTools/DependencyFinder/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-
-// Information about this assembly is defined by the following attributes.
-// Change them to the values specific to your project.
-
-[assembly: AssemblyTitle("DependencyFinder")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("")]
-[assembly: AssemblyCopyright("kenneth")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
-// The form "{Major}.{Minor}.*" will automatically update the build and revision,
-// and "{Major}.{Minor}.{Build}.*" will update just the revision.
-
-[assembly: AssemblyVersion("2.0.0.7")]
-
-// The following attributes are used to specify the signing key for the assembly,
-// if desired. See the Mono documentation for more information about signing.
-
-//[assembly: AssemblyDelaySign(false)]
-//[assembly: AssemblyKeyFile("")]
-
diff --git a/BuildTools/DependencyFinder/app.config b/BuildTools/DependencyFinder/app.config
deleted file mode 100644
index d7c60fd53..000000000
--- a/BuildTools/DependencyFinder/app.config
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/BuildTools/DependencyFinder/packages.config b/BuildTools/DependencyFinder/packages.config
deleted file mode 100644
index 0cb2b0939..000000000
--- a/BuildTools/DependencyFinder/packages.config
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/BuildTools/GnupgSigningTool/GnupgSigningTool.csproj b/BuildTools/GnupgSigningTool/GnupgSigningTool.csproj
deleted file mode 100644
index e416fc8c0..000000000
--- a/BuildTools/GnupgSigningTool/GnupgSigningTool.csproj
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
- Debug
- x86
- {72B910F2-8E80-4955-A0D6-ED4C35CEE665}
- Exe
- GnupgSigningTool
- GnupgSigningTool
- v4.7.1
-
-
-
- true
- full
- false
- bin\Debug
- DEBUG;
- prompt
- 4
- x86
-
-
- true
- bin\Release
- prompt
- 4
- x86
-
-
- true
-
-
-
-
-
-
-
-
-
-
- {2cf2d90e-c25b-47ad-91e0-98451bab8058}
- Duplicati.Library.Encryption
-
-
- {DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}
- Duplicati.Library.Utility
-
-
- {C5899F45-B0FF-483C-9D38-24A9FCAAB237}
- Duplicati.Library.Interface
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/BuildTools/GnupgSigningTool/GnupgSigningTool.sln b/BuildTools/GnupgSigningTool/GnupgSigningTool.sln
deleted file mode 100644
index 488ebd3d4..000000000
--- a/BuildTools/GnupgSigningTool/GnupgSigningTool.sln
+++ /dev/null
@@ -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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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
diff --git a/BuildTools/GnupgSigningTool/Program.cs b/BuildTools/GnupgSigningTool/Program.cs
deleted file mode 100644
index 3fd35b5d9..000000000
--- a/BuildTools/GnupgSigningTool/Program.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-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()))
- 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(_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();
- }
- }
-}
diff --git a/BuildTools/GnupgSigningTool/app.config b/BuildTools/GnupgSigningTool/app.config
deleted file mode 100644
index d7c60fd53..000000000
--- a/BuildTools/GnupgSigningTool/app.config
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/BuildTools/LicenseUpdater/Fragments.cs b/BuildTools/LicenseUpdater/Fragments.cs
new file mode 100755
index 000000000..f53a635de
--- /dev/null
+++ b/BuildTools/LicenseUpdater/Fragments.cs
@@ -0,0 +1,84 @@
+// 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.Text.RegularExpressions;
+
+namespace LicenseUpdater;
+
+public static class Fragments
+{
+ public static string NEW_LICENSE = @"Copyright (C) YYYY, 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.".Replace("YYYY", DateTime.UtcNow.Year.ToString());
+
+ public static RegexOptions RE_OPTS = RegexOptions.Singleline | RegexOptions.IgnoreCase;
+
+ public static Regex CS_REGION_MATCH = new Regex(@"(?#region Disclaimer / License(?.+(?:Free Software Foundation)[^#]+)#endregion)", RE_OPTS);
+
+ public static Regex CS_LGPL_MATCH = new Regex(@"(?//\s+Copyright \(C\) \d{4}, The Duplicati Team.+(?:Free Software Foundation).+Boston,\s+MA\s+0211\d-130\d\s+USA)", RE_OPTS);
+
+ public static Regex CS_MIT_MATCH = new Regex(@"(?//\s+Copyright \(C\) \d{4}, The Duplicati Team.+(?:copyright notice and this permission notice).+IN THE SOFTWARE\.)", RE_OPTS);
+
+ public static string GetLicenseTextWithPrefixedLines(string linePrefix = "// ")
+ => string.Join("\n", Fragments.NEW_LICENSE.Split("\n").Select(x => linePrefix + x).Append(string.Empty));
+
+ public static bool MatchAndReplace(ref string data)
+ {
+ foreach (var m in new[] { CS_REGION_MATCH, CS_MIT_MATCH })
+ {
+ var match = m.Match(data);
+ if (match.Success && match.Groups["license"].Success)
+ {
+ var g = match.Groups["license"];
+ data = string.Join(
+ string.Empty,
+
+ data.Substring(0, g.Index),
+ GetLicenseTextWithPrefixedLines(),
+ data.Substring(g.Index + g.Length)
+ );
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
+
+
diff --git a/BuildTools/LicenseUpdater/LicenseUpdater.csproj b/BuildTools/LicenseUpdater/LicenseUpdater.csproj
new file mode 100755
index 000000000..a44450dc4
--- /dev/null
+++ b/BuildTools/LicenseUpdater/LicenseUpdater.csproj
@@ -0,0 +1,11 @@
+
+
+
+ Exe
+ net8.0
+ license_upgrader
+ enable
+ enable
+
+
+
diff --git a/BuildTools/LicenseUpdater/LicenseUpdater.sln b/BuildTools/LicenseUpdater/LicenseUpdater.sln
new file mode 100755
index 000000000..35f42918d
--- /dev/null
+++ b/BuildTools/LicenseUpdater/LicenseUpdater.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.002.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LicenseUpdater", "LicenseUpdater.csproj", "{37205C47-20D1-4E7B-9AFB-B833C7FC949F}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {37205C47-20D1-4E7B-9AFB-B833C7FC949F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {37205C47-20D1-4E7B-9AFB-B833C7FC949F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {37205C47-20D1-4E7B-9AFB-B833C7FC949F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {37205C47-20D1-4E7B-9AFB-B833C7FC949F}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {F9144130-7643-4C0E-9545-ECEF7EEF0423}
+ EndGlobalSection
+EndGlobal
diff --git a/BuildTools/LicenseUpdater/Program.cs b/BuildTools/LicenseUpdater/Program.cs
new file mode 100755
index 000000000..97603905a
--- /dev/null
+++ b/BuildTools/LicenseUpdater/Program.cs
@@ -0,0 +1,79 @@
+// 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 LicenseUpdater;
+
+var cmdargs = Environment.GetCommandLineArgs();
+if (cmdargs.Length != 2)
+ throw new Exception($"Usage: dotnet run ");
+
+var startpath = Path.GetFullPath(Environment.GetCommandLineArgs().Skip(1).First());
+if (!Directory.Exists(startpath))
+ throw new Exception($"Start path not found: {startpath}");
+
+var target_extensions = new[] {
+ ".cs"
+ // ".html",
+ // ".js",
+ // ".css"
+}.ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+File.WriteAllText(Path.Combine(startpath, "LICENSE.txt"), Fragments.GetLicenseTextWithPrefixedLines(string.Empty));
+
+var candidates = Directory.EnumerateFiles(startpath, "*", SearchOption.AllDirectories)
+ .Where(x => target_extensions.Contains(Path.GetExtension(x) ?? string.Empty));
+
+var blacklistSubFolders = new[] { "bin", "obj", "packages" };
+
+foreach (var file in candidates)
+{
+ if (string.Equals("AssemblyInfo.cs", Path.GetFileName(file), StringComparison.OrdinalIgnoreCase))
+ continue;
+ if (blacklistSubFolders.Any(x => file.Contains($"{Path.DirectorySeparatorChar}{x}{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase)))
+ continue;
+
+ if (new FileInfo(file).Length > 10 * 1024 * 1024)
+ {
+ Console.WriteLine($"{file} skipped due to size");
+ continue;
+ }
+
+ var data = File.ReadAllText(file);
+ if (Fragments.MatchAndReplace(ref data))
+ {
+ File.WriteAllText(file, data);
+ // Console.WriteLine($"{file} updated!");
+ continue;
+ }
+
+ if (string.Equals(Path.GetExtension(file), ".cs", StringComparison.OrdinalIgnoreCase) && !data.StartsWith("// Copyright", StringComparison.OrdinalIgnoreCase))
+ {
+ data = Fragments.GetLicenseTextWithPrefixedLines() + data;
+ File.WriteAllText(file, data);
+ // Console.WriteLine($"{file} updated!");
+ continue;
+ }
+
+ Console.WriteLine($"{file} skipped, no match");
+}
+
+
+
diff --git a/BuildTools/LicenseUpdater/README.md b/BuildTools/LicenseUpdater/README.md
new file mode 100644
index 000000000..0fa38f9bc
--- /dev/null
+++ b/BuildTools/LicenseUpdater/README.md
@@ -0,0 +1,9 @@
+# License Updater
+
+This folder contains a small tool that can update the license in all files, using the current year as the copyright year. It is intended to be run once a year, using the root repository folder as input:
+
+```
+dotnet run ../../
+```
+
+The resulting change can then be reviewed and committed.
\ No newline at end of file
diff --git a/BuildTools/UpdateVersionStamp/Program.cs b/BuildTools/UpdateVersionStamp/Program.cs
deleted file mode 100644
index 79c03cb58..000000000
--- a/BuildTools/UpdateVersionStamp/Program.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-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 FILEMAP;
-
- static Program()
- {
- var versionre = @"(?\d+\.\d+\.(\*|(\d+(\.(\*|\d+)))?))";
- FILEMAP = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
- FILEMAP.Add("AssemblyInfo.cs", new Regex(@"(\[assembly\: AssemblyVersion\(\""" + versionre + @"""\)\])|(\[assembly\: AssemblyFileVersion\(\""" + versionre + @"\""\)\])|(\[assembly\: AssemblyFileVersionAttribute\(\""" + versionre + @"\""\)\])"));
- 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 args = new List(_args);
- Dictionary 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 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);
- }
- }
-}
diff --git a/BuildTools/UpdateVersionStamp/Properties/AssemblyInfo.cs b/BuildTools/UpdateVersionStamp/Properties/AssemblyInfo.cs
deleted file mode 100644
index 8d0207127..000000000
--- a/BuildTools/UpdateVersionStamp/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-
-// Information about this assembly is defined by the following attributes.
-// Change them to the values specific to your project.
-[assembly: AssemblyTitle("UpdateVersionStamp")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("")]
-[assembly: AssemblyCopyright("")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
-// The form "{Major}.{Minor}.*" will automatically update the build and revision,
-// and "{Major}.{Minor}.{Build}.*" will update just the revision.
-[assembly: AssemblyVersion("2.0.0.7")]
-// The following attributes are used to specify the signing key for the assembly,
-// if desired. See the Mono documentation for more information about signing.
-//[assembly: AssemblyDelaySign(false)]
-//[assembly: AssemblyKeyFile("")]
-
diff --git a/BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj b/BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj
deleted file mode 100644
index 0a9333d13..000000000
--- a/BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
- Debug
- AnyCPU
- 10.0.0
- 2.0
- {1920C3B0-98A0-410F-8972-9C19FA616FE5}
- Exe
- False
- UpdateVersionStamp
- UpdateVersionStamp
- v4.7.1
-
-
-
- true
- full
- false
- bin\Debug
- DEBUG;
- prompt
- 4
- false
- --sourcefolder=../.. --version=2.0.0.0 --version-tag=20140405
- false
-
-
- full
- true
- bin\Release
- prompt
- 4
- false
- false
-
-
-
-
-
-
-
-
-
-
-
-
- {DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}
- Duplicati.Library.Utility
-
-
- {D63E53E4-A458-4C2F-914D-92F715F58ACF}
- Duplicati.Library.Common
-
-
-
-
-
-
diff --git a/BuildTools/UpdateVersionStamp/UpdateVersionStamp.sln b/BuildTools/UpdateVersionStamp/UpdateVersionStamp.sln
deleted file mode 100644
index 9bae29432..000000000
--- a/BuildTools/UpdateVersionStamp/UpdateVersionStamp.sln
+++ /dev/null
@@ -1,35 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 11.00
-# Visual Studio 2010
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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
diff --git a/BuildTools/UpdateVersionStamp/app.config b/BuildTools/UpdateVersionStamp/app.config
deleted file mode 100644
index d7c60fd53..000000000
--- a/BuildTools/UpdateVersionStamp/app.config
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/Duplicati CommandLine Only.sln b/Duplicati CommandLine Only.sln
deleted file mode 100644
index 3d934e79d..000000000
--- a/Duplicati CommandLine Only.sln
+++ /dev/null
@@ -1,330 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26730.12
-MinimumVisualStudioVersion = 10.0.40219.1
-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.Main", "Duplicati\Library\Main\Duplicati.Library.Main.csproj", "{10D2D1B7-C664-41D8-9B3A-00040C3D421B}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.File", "Duplicati\Library\Backend\File\Duplicati.Library.Backend.File.csproj", "{FC9B7611-836F-4127-8B44-A7C31F506807}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Compression", "Duplicati\Library\Compression\Duplicati.Library.Compression.csproj", "{19ECCE09-B5EB-406C-8C57-BAC66997D469}"
-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.Logging", "Duplicati\Library\Logging\Duplicati.Library.Logging.csproj", "{D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine", "Duplicati\CommandLine\Duplicati.CommandLine.csproj", "{81765A64-3661-4E3E-B850-2F6F87A51F74}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Interface", "Duplicati\Library\Interface\Duplicati.Library.Interface.csproj", "{C5899F45-B0FF-483C-9D38-24A9FCAAB237}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.DynamicLoader", "Duplicati\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj", "{0CA86ECF-5BEC-4909-B4F6-110A03B30B92}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Snapshots", "Duplicati\Library\Snapshots\Duplicati.Library.Snapshots.csproj", "{D63E53E4-A458-4C2F-914D-92F715F58ACE}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Modules.Builtin", "Duplicati\Library\Modules\Builtin\Duplicati.Library.Modules.Builtin.csproj", "{52826615-7964-47FE-B4B3-1B2DBDF605B9}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.License", "Duplicati\License\Duplicati.License.csproj", "{4D012CB1-4B92-47F4-89B7-BF80A73A2E99}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.SSHv2", "Duplicati\Library\Backend\SSHv2\Duplicati.Library.Backend.SSHv2.csproj", "{FF2BF37C-E502-4C98-BEA0-701671DDFA08}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.WEBDAV", "Duplicati\Library\Backend\WEBDAV\Duplicati.Library.Backend.WEBDAV.csproj", "{BAE27510-8B5D-44B2-B33E-372A98908041}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.OneDrive", "Duplicati\Library\Backend\OneDrive\Duplicati.Library.Backend.OneDrive.csproj", "{CCD76347-7DC7-4B42-B7E1-E500E624CAC3}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.TahoeLAFS", "Duplicati\Library\Backend\TahoeLAFS\Duplicati.Library.Backend.TahoeLAFS.csproj", "{C0270709-2A40-43B5-8CF1-69581B9FA2A1}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.CloudFiles", "Duplicati\Library\Backend\CloudFiles\Duplicati.Library.Backend.CloudFiles.csproj", "{1BFAE226-8364-4086-825C-BB83F6F3EE4C}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.FTP", "Duplicati\Library\Backend\FTP\Duplicati.Library.Backend.FTP.csproj", "{F61679A9-E5DE-468A-B5A4-05F92D0143D2}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.S3", "Duplicati\Library\Backend\S3\Duplicati.Library.Backend.S3.csproj", "{C03F6DFD-805A-4BE0-9338-64870ADDB4A2}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.AzureBlob", "Duplicati\Library\Backend\AzureBlob\Duplicati.Library.Backend.AzureBlob.csproj", "{8E4CECFB-0413-4B00-AB93-78D1C3902BD5}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Localization", "Duplicati\Library\Localization\Duplicati.Library.Localization.csproj", "{B68F2214-951F-4F78-8488-66E1ED3F50BF}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.AutoUpdater", "Duplicati\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj", "{7E119745-1F62-43F0-936C-F312A1912C0B}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.SQLiteHelper", "Duplicati\Library\SQLiteHelper\Duplicati.Library.SQLiteHelper.csproj", "{2C838169-B187-4B09-8768-1C24C2521C8D}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.UsageReporter", "Duplicati\Library\UsageReporter\Duplicati.Library.UsageReporter.csproj", "{BB014EA5-CE2C-4444-8D30-38983A0E8553}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.OAuthHelper", "Duplicati\Library\Backend\OAuthHelper\Duplicati.Library.OAuthHelper.csproj", "{D4C37C33-5E73-4B56-B2C3-DC4A6BAA36BB}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.BackendTool", "Duplicati\CommandLine\BackendTool\Duplicati.CommandLine.BackendTool.csproj", "{2AF960C0-357D-4D44-A3D5-8B6E89DB0F11}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.BackendTester", "Duplicati\CommandLine\BackendTester\Duplicati.CommandLine.BackendTester.csproj", "{E7280DCA-7776-4A73-B9B5-41FD77FC8799}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.GoogleServices", "Duplicati\Library\Backend\GoogleServices\Duplicati.Library.Backend.GoogleServices.csproj", "{5489181D-950C-44AF-873C-45EB0A3B6BD2}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.OpenStack", "Duplicati\Library\Backend\OpenStack\Duplicati.Library.Backend.OpenStack.csproj", "{D9E4E686-423C-48EC-A392-404E7C00860C}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.HubiC", "Duplicati\Library\Backend\HubiC\Duplicati.Library.Backend.HubiC.csproj", "{D60AD540-0E7D-40CE-83AE-D26E01FFE9B8}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.RecoveryTool", "Duplicati\CommandLine\RecoveryTool\Duplicati.CommandLine.RecoveryTool.csproj", "{4A010589-76E6-4F05-A5C4-4598D5DF11F8}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.Box", "Duplicati\Library\Backend\Box\Duplicati.Library.Backend.Box.csproj", "{3FF7DD0B-5284-4BF9-97D9-1E4417FDABB2}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.AlternativeFTP", "Duplicati\Library\Backend\AlternativeFTP\Duplicati.Library.Backend.AlternativeFTP.csproj", "{3F47CB0C-CEE1-447F-9C26-7F8E419E3E3F}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.Backblaze", "Duplicati\Library\Backend\Backblaze\Duplicati.Library.Backend.Backblaze.csproj", "{61C43D61-4368-4942-84A3-1EB623F4EF2A}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.Dropbox", "Duplicati\Library\Backend\Dropbox\Duplicati.Library.Backend.Dropbox.csproj", "{B20A7CEE-9C5B-47B9-8B76-BC85ADFE8493}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.Mega", "Duplicati\Library\Backend\Mega\Duplicati.Library.Backend.Mega.csproj", "{6643A5AE-AB38-453F-ADCE-408E35A81A83}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.SharePoint", "Duplicati\Library\Backend\SharePoint\Duplicati.Library.Backend.SharePoint.csproj", "{59C8BBC5-6E42-46FB-AB3E-6C183A82459A}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Tools", "Duplicati\Tools\Duplicati.Tools.csproj", "{0797AA22-C5DD-4950-BB60-34765AB8C6DD}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.Sia", "Duplicati\Library\Backend\Sia\Duplicati.Library.Backend.Sia.csproj", "{32A74526-3E5F-413A-8CB4-1EFDAD4C8B91}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Common", "Duplicati\Library\Common\Duplicati.Library.Common.csproj", "{D63E53E4-A458-4C2F-914D-92F715F58ACF}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.Jottacloud", "Duplicati\Library\Backend\Jottacloud\Duplicati.Library.Backend.Jottacloud.csproj", "{2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.Rclone", "Duplicati\Library\Backend\Rclone\Duplicati.Library.Backend.Rclone.csproj", "{851A1CB8-3CEB-41B4-956F-34D760D2A8E5}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.IDrive", "Duplicati\Library\Backend\IDrive\Duplicati.Library.Backend.IDrive.csproj", "{C16639F6-DACC-4DD9-86CD-8B937516B340}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {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
- {10D2D1B7-C664-41D8-9B3A-00040C3D421B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {10D2D1B7-C664-41D8-9B3A-00040C3D421B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {10D2D1B7-C664-41D8-9B3A-00040C3D421B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {10D2D1B7-C664-41D8-9B3A-00040C3D421B}.Release|Any CPU.Build.0 = Release|Any CPU
- {FC9B7611-836F-4127-8B44-A7C31F506807}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FC9B7611-836F-4127-8B44-A7C31F506807}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FC9B7611-836F-4127-8B44-A7C31F506807}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FC9B7611-836F-4127-8B44-A7C31F506807}.Release|Any CPU.Build.0 = Release|Any CPU
- {19ECCE09-B5EB-406C-8C57-BAC66997D469}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {19ECCE09-B5EB-406C-8C57-BAC66997D469}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {19ECCE09-B5EB-406C-8C57-BAC66997D469}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {19ECCE09-B5EB-406C-8C57-BAC66997D469}.Release|Any CPU.Build.0 = Release|Any CPU
- {2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Release|Any CPU.Build.0 = Release|Any CPU
- {D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D10A5FC0-11B4-4E70-86AA-8AEA52BD9798}.Release|Any CPU.Build.0 = Release|Any CPU
- {81765A64-3661-4E3E-B850-2F6F87A51F74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {81765A64-3661-4E3E-B850-2F6F87A51F74}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {81765A64-3661-4E3E-B850-2F6F87A51F74}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {81765A64-3661-4E3E-B850-2F6F87A51F74}.Release|Any CPU.Build.0 = Release|Any CPU
- {C5899F45-B0FF-483C-9D38-24A9FCAAB237}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C5899F45-B0FF-483C-9D38-24A9FCAAB237}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C5899F45-B0FF-483C-9D38-24A9FCAAB237}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C5899F45-B0FF-483C-9D38-24A9FCAAB237}.Release|Any CPU.Build.0 = Release|Any CPU
- {0CA86ECF-5BEC-4909-B4F6-110A03B30B92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0CA86ECF-5BEC-4909-B4F6-110A03B30B92}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0CA86ECF-5BEC-4909-B4F6-110A03B30B92}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0CA86ECF-5BEC-4909-B4F6-110A03B30B92}.Release|Any CPU.Build.0 = Release|Any CPU
- {D63E53E4-A458-4C2F-914D-92F715F58ACE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D63E53E4-A458-4C2F-914D-92F715F58ACE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D63E53E4-A458-4C2F-914D-92F715F58ACE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D63E53E4-A458-4C2F-914D-92F715F58ACE}.Release|Any CPU.Build.0 = Release|Any CPU
- {52826615-7964-47FE-B4B3-1B2DBDF605B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {52826615-7964-47FE-B4B3-1B2DBDF605B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {52826615-7964-47FE-B4B3-1B2DBDF605B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {52826615-7964-47FE-B4B3-1B2DBDF605B9}.Release|Any CPU.Build.0 = Release|Any CPU
- {4D012CB1-4B92-47F4-89B7-BF80A73A2E99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4D012CB1-4B92-47F4-89B7-BF80A73A2E99}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4D012CB1-4B92-47F4-89B7-BF80A73A2E99}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4D012CB1-4B92-47F4-89B7-BF80A73A2E99}.Release|Any CPU.Build.0 = Release|Any CPU
- {FF2BF37C-E502-4C98-BEA0-701671DDFA08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FF2BF37C-E502-4C98-BEA0-701671DDFA08}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FF2BF37C-E502-4C98-BEA0-701671DDFA08}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FF2BF37C-E502-4C98-BEA0-701671DDFA08}.Release|Any CPU.Build.0 = Release|Any CPU
- {BAE27510-8B5D-44B2-B33E-372A98908041}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {BAE27510-8B5D-44B2-B33E-372A98908041}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {BAE27510-8B5D-44B2-B33E-372A98908041}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {BAE27510-8B5D-44B2-B33E-372A98908041}.Release|Any CPU.Build.0 = Release|Any CPU
- {CCD76347-7DC7-4B42-B7E1-E500E624CAC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CCD76347-7DC7-4B42-B7E1-E500E624CAC3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CCD76347-7DC7-4B42-B7E1-E500E624CAC3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CCD76347-7DC7-4B42-B7E1-E500E624CAC3}.Release|Any CPU.Build.0 = Release|Any CPU
- {C0270709-2A40-43B5-8CF1-69581B9FA2A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C0270709-2A40-43B5-8CF1-69581B9FA2A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C0270709-2A40-43B5-8CF1-69581B9FA2A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C0270709-2A40-43B5-8CF1-69581B9FA2A1}.Release|Any CPU.Build.0 = Release|Any CPU
- {1BFAE226-8364-4086-825C-BB83F6F3EE4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1BFAE226-8364-4086-825C-BB83F6F3EE4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1BFAE226-8364-4086-825C-BB83F6F3EE4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1BFAE226-8364-4086-825C-BB83F6F3EE4C}.Release|Any CPU.Build.0 = Release|Any CPU
- {F61679A9-E5DE-468A-B5A4-05F92D0143D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F61679A9-E5DE-468A-B5A4-05F92D0143D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F61679A9-E5DE-468A-B5A4-05F92D0143D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F61679A9-E5DE-468A-B5A4-05F92D0143D2}.Release|Any CPU.Build.0 = Release|Any CPU
- {C03F6DFD-805A-4BE0-9338-64870ADDB4A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C03F6DFD-805A-4BE0-9338-64870ADDB4A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C03F6DFD-805A-4BE0-9338-64870ADDB4A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C03F6DFD-805A-4BE0-9338-64870ADDB4A2}.Release|Any CPU.Build.0 = Release|Any CPU
- {8E4CECFB-0413-4B00-AB93-78D1C3902BD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {8E4CECFB-0413-4B00-AB93-78D1C3902BD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {8E4CECFB-0413-4B00-AB93-78D1C3902BD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {8E4CECFB-0413-4B00-AB93-78D1C3902BD5}.Release|Any CPU.Build.0 = Release|Any CPU
- {B68F2214-951F-4F78-8488-66E1ED3F50BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B68F2214-951F-4F78-8488-66E1ED3F50BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B68F2214-951F-4F78-8488-66E1ED3F50BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B68F2214-951F-4F78-8488-66E1ED3F50BF}.Release|Any CPU.Build.0 = Release|Any CPU
- {7E119745-1F62-43F0-936C-F312A1912C0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7E119745-1F62-43F0-936C-F312A1912C0B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7E119745-1F62-43F0-936C-F312A1912C0B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7E119745-1F62-43F0-936C-F312A1912C0B}.Release|Any CPU.Build.0 = Release|Any CPU
- {2C838169-B187-4B09-8768-1C24C2521C8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2C838169-B187-4B09-8768-1C24C2521C8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2C838169-B187-4B09-8768-1C24C2521C8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2C838169-B187-4B09-8768-1C24C2521C8D}.Release|Any CPU.Build.0 = Release|Any CPU
- {BB014EA5-CE2C-4444-8D30-38983A0E8553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {BB014EA5-CE2C-4444-8D30-38983A0E8553}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {BB014EA5-CE2C-4444-8D30-38983A0E8553}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {BB014EA5-CE2C-4444-8D30-38983A0E8553}.Release|Any CPU.Build.0 = Release|Any CPU
- {D4C37C33-5E73-4B56-B2C3-DC4A6BAA36BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D4C37C33-5E73-4B56-B2C3-DC4A6BAA36BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D4C37C33-5E73-4B56-B2C3-DC4A6BAA36BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D4C37C33-5E73-4B56-B2C3-DC4A6BAA36BB}.Release|Any CPU.Build.0 = Release|Any CPU
- {2AF960C0-357D-4D44-A3D5-8B6E89DB0F11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2AF960C0-357D-4D44-A3D5-8B6E89DB0F11}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2AF960C0-357D-4D44-A3D5-8B6E89DB0F11}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2AF960C0-357D-4D44-A3D5-8B6E89DB0F11}.Release|Any CPU.Build.0 = Release|Any CPU
- {E7280DCA-7776-4A73-B9B5-41FD77FC8799}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E7280DCA-7776-4A73-B9B5-41FD77FC8799}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E7280DCA-7776-4A73-B9B5-41FD77FC8799}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E7280DCA-7776-4A73-B9B5-41FD77FC8799}.Release|Any CPU.Build.0 = Release|Any CPU
- {5489181D-950C-44AF-873C-45EB0A3B6BD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5489181D-950C-44AF-873C-45EB0A3B6BD2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5489181D-950C-44AF-873C-45EB0A3B6BD2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5489181D-950C-44AF-873C-45EB0A3B6BD2}.Release|Any CPU.Build.0 = Release|Any CPU
- {D9E4E686-423C-48EC-A392-404E7C00860C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D9E4E686-423C-48EC-A392-404E7C00860C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D9E4E686-423C-48EC-A392-404E7C00860C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D9E4E686-423C-48EC-A392-404E7C00860C}.Release|Any CPU.Build.0 = Release|Any CPU
- {D60AD540-0E7D-40CE-83AE-D26E01FFE9B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D60AD540-0E7D-40CE-83AE-D26E01FFE9B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D60AD540-0E7D-40CE-83AE-D26E01FFE9B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D60AD540-0E7D-40CE-83AE-D26E01FFE9B8}.Release|Any CPU.Build.0 = Release|Any CPU
- {4A010589-76E6-4F05-A5C4-4598D5DF11F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {4A010589-76E6-4F05-A5C4-4598D5DF11F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {4A010589-76E6-4F05-A5C4-4598D5DF11F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {4A010589-76E6-4F05-A5C4-4598D5DF11F8}.Release|Any CPU.Build.0 = Release|Any CPU
- {3FF7DD0B-5284-4BF9-97D9-1E4417FDABB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3FF7DD0B-5284-4BF9-97D9-1E4417FDABB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3FF7DD0B-5284-4BF9-97D9-1E4417FDABB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3FF7DD0B-5284-4BF9-97D9-1E4417FDABB2}.Release|Any CPU.Build.0 = Release|Any CPU
- {3F47CB0C-CEE1-447F-9C26-7F8E419E3E3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3F47CB0C-CEE1-447F-9C26-7F8E419E3E3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3F47CB0C-CEE1-447F-9C26-7F8E419E3E3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3F47CB0C-CEE1-447F-9C26-7F8E419E3E3F}.Release|Any CPU.Build.0 = Release|Any CPU
- {61C43D61-4368-4942-84A3-1EB623F4EF2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {61C43D61-4368-4942-84A3-1EB623F4EF2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {61C43D61-4368-4942-84A3-1EB623F4EF2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {61C43D61-4368-4942-84A3-1EB623F4EF2A}.Release|Any CPU.Build.0 = Release|Any CPU
- {B20A7CEE-9C5B-47B9-8B76-BC85ADFE8493}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B20A7CEE-9C5B-47B9-8B76-BC85ADFE8493}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B20A7CEE-9C5B-47B9-8B76-BC85ADFE8493}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B20A7CEE-9C5B-47B9-8B76-BC85ADFE8493}.Release|Any CPU.Build.0 = Release|Any CPU
- {6643A5AE-AB38-453F-ADCE-408E35A81A83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6643A5AE-AB38-453F-ADCE-408E35A81A83}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6643A5AE-AB38-453F-ADCE-408E35A81A83}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6643A5AE-AB38-453F-ADCE-408E35A81A83}.Release|Any CPU.Build.0 = Release|Any CPU
- {59C8BBC5-6E42-46FB-AB3E-6C183A82459A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {59C8BBC5-6E42-46FB-AB3E-6C183A82459A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {59C8BBC5-6E42-46FB-AB3E-6C183A82459A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {59C8BBC5-6E42-46FB-AB3E-6C183A82459A}.Release|Any CPU.Build.0 = Release|Any CPU
- {0797AA22-C5DD-4950-BB60-34765AB8C6DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0797AA22-C5DD-4950-BB60-34765AB8C6DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0797AA22-C5DD-4950-BB60-34765AB8C6DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0797AA22-C5DD-4950-BB60-34765AB8C6DD}.Release|Any CPU.Build.0 = Release|Any CPU
- {32A74526-3E5F-413A-8CB4-1EFDAD4C8B91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {32A74526-3E5F-413A-8CB4-1EFDAD4C8B91}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {32A74526-3E5F-413A-8CB4-1EFDAD4C8B91}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {32A74526-3E5F-413A-8CB4-1EFDAD4C8B91}.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
- {2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2}.Release|Any CPU.Build.0 = Release|Any CPU
- {851A1CB8-3CEB-41B4-956F-34D760D2A8E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {851A1CB8-3CEB-41B4-956F-34D760D2A8E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {851A1CB8-3CEB-41B4-956F-34D760D2A8E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {851A1CB8-3CEB-41B4-956F-34D760D2A8E5}.Release|Any CPU.Build.0 = Release|Any CPU
- {C16639F6-DACC-4DD9-86CD-8B937516B340}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C16639F6-DACC-4DD9-86CD-8B937516B340}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C16639F6-DACC-4DD9-86CD-8B937516B340}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C16639F6-DACC-4DD9-86CD-8B937516B340}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {3221EB9F-EE08-460E-B236-CD594688FC5F}
- EndGlobalSection
- GlobalSection(MonoDevelopProperties) = preSolution
- StartupItem = Duplicati\CommandLine\Duplicati.CommandLine.csproj
- Policies = $0
- $0.StandardHeader = $1
- $1.Text = @ Copyright (C) 2013, The Duplicati Team\r\n http://www.duplicati.com, info@duplicati.com\n \n This library is free software; you can redistribute it and/or modify\n it under the terms of the GNU Lesser General Public License as\n published by the Free Software Foundation; either version 2.1 of the\n License, or (at your option) any later version.\n\n This library is distributed in the hope that it will be useful, but\n WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n Lesser General Public License for more details.\n\n You should have received a copy of the GNU Lesser General Public\n License along with this library; if not, write to the Free Software\n Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
- $1.IncludeInNewFiles = True
- $0.DotNetNamingPolicy = $2
- $2.DirectoryNamespaceAssociation = PrefixedHierarchical
- $2.ResourceNamePolicy = FileFormatDefault
- $0.TextStylePolicy = $5
- $3.inheritsSet = VisualStudio
- $3.scope = text/plain
- $3.FileWidth = 120
- $3.EolMarker = Windows
- $3.inheritsScope = text/plain
- $0.CSharpFormattingPolicy = $4
- $4.IndentSwitchBody = True
- $4.AnonymousMethodBraceStyle = NextLine
- $4.PropertyBraceStyle = NextLine
- $4.PropertyGetBraceStyle = NextLine
- $4.PropertySetBraceStyle = NextLine
- $4.EventBraceStyle = NextLine
- $4.EventAddBraceStyle = NextLine
- $4.EventRemoveBraceStyle = NextLine
- $4.StatementBraceStyle = NextLine
- $4.ElseNewLinePlacement = NewLine
- $4.ElseIfNewLinePlacement = SameLine
- $4.CatchNewLinePlacement = NewLine
- $4.FinallyNewLinePlacement = NewLine
- $4.BeforeMethodDeclarationParentheses = False
- $4.BeforeMethodCallParentheses = False
- $4.BeforeConstructorDeclarationParentheses = False
- $4.BeforeIndexerDeclarationBracket = False
- $4.BeforeDelegateDeclarationParentheses = False
- $4.NewParentheses = False
- $4.ForParentheses = False
- $4.ForeachParentheses = False
- $4.LockParentheses = False
- $4.UsingParentheses = False
- $4.SpacesBeforeBrackets = False
- $4.inheritsSet = Mono
- $4.inheritsScope = text/x-csharp
- $4.scope = text/x-csharp
- $5.FileWidth = 120
- $5.EolMarker = Windows
- $5.inheritsSet = VisualStudio
- $5.inheritsScope = text/plain
- $5.scope = text/plain
- EndGlobalSection
- GlobalSection(SubversionScc) = preSolution
- Svn-Managed = True
- Manager = AnkhSVN - Subversion Support for Visual Studio
- EndGlobalSection
-EndGlobal
diff --git a/Duplicati/Server/Database/Backup.cs b/Duplicati.Library.RestAPI/Database/Backup.cs
similarity index 100%
rename from Duplicati/Server/Database/Backup.cs
rename to Duplicati.Library.RestAPI/Database/Backup.cs
diff --git a/Duplicati/Server/Database/Connection.cs b/Duplicati.Library.RestAPI/Database/Connection.cs
similarity index 93%
rename from Duplicati/Server/Database/Connection.cs
rename to Duplicati.Library.RestAPI/Database/Connection.cs
index ce9ed53da..fc47340ef 100644
--- a/Duplicati/Server/Database/Connection.cs
+++ b/Duplicati.Library.RestAPI/Database/Connection.cs
@@ -1,1261 +1,1262 @@
-// 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.Collections.Generic;
-using System.Linq;
-using Duplicati.Server.Serialization.Interface;
-using System.Text;
-
-namespace Duplicati.Server.Database
-{
- public class Connection : IDisposable
- {
- private readonly System.Data.IDbConnection m_connection;
- private System.Data.IDbCommand m_errorcmd;
- public readonly object m_lock = new object();
- public const int ANY_BACKUP_ID = -1;
- public const int SERVER_SETTINGS_ID = -2;
- private readonly Dictionary m_temporaryBackups = new Dictionary();
-
- public Connection(System.Data.IDbConnection connection)
- {
- m_connection = connection;
- m_errorcmd = m_connection.CreateCommand();
- m_errorcmd.CommandText = @"INSERT INTO ""ErrorLog"" (""BackupID"", ""Message"", ""Exception"", ""Timestamp"") VALUES (?,?,?,?)";
- for(var i = 0; i < 4; i++)
- m_errorcmd.Parameters.Add(m_errorcmd.CreateParameter());
-
- this.ApplicationSettings = new ServerSettings(this);
- }
-
- internal void LogError(string backupid, string message, Exception ex)
- {
- lock(m_lock)
- {
- if (!long.TryParse(backupid, out long id))
- id = -1;
- ((System.Data.IDbDataParameter)m_errorcmd.Parameters[0]).Value = id;
- ((System.Data.IDbDataParameter)m_errorcmd.Parameters[1]).Value = message;
- ((System.Data.IDbDataParameter)m_errorcmd.Parameters[2]).Value = ex?.ToString();
- ((System.Data.IDbDataParameter)m_errorcmd.Parameters[3]).Value = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(DateTime.UtcNow);
- m_errorcmd.ExecuteNonQuery();
- }
- }
-
- internal void ExecuteWithCommand(Action f)
- {
- lock(m_lock)
- using(var cmd = m_connection.CreateCommand())
- f(cmd);
- }
-
- internal Serializable.ImportExportStructure PrepareBackupForExport(IBackup backup)
- {
- var scheduleId = GetScheduleIDsFromTags(new string[] { "ID=" + backup.ID });
- return new Serializable.ImportExportStructure() {
- CreatedByVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(),
- Backup = (Database.Backup)backup,
- Schedule = (Database.Schedule)(scheduleId.Any() ? GetSchedule(scheduleId.First()) : null),
- DisplayNames = SpecialFolders.GetSourceNames(backup)
- };
- }
-
- public string RegisterTemporaryBackup(IBackup backup)
- {
- lock(m_lock)
- {
- if (backup == null)
- throw new ArgumentNullException(nameof(backup));
- if (backup.ID != null)
- throw new ArgumentException("Backup is already active, cannot make temporary");
-
- backup.ID = Guid.NewGuid().ToString("D");
- m_temporaryBackups.Add(backup.ID, (Backup)backup);
- return backup.ID;
- }
- }
-
- public void UnregisterTemporaryBackup(IBackup backup)
- {
- lock(m_lock)
- m_temporaryBackups.Remove(backup.ID);
- }
-
- public void UpdateTemporaryBackup(IBackup backup)
- {
- lock(m_lock)
- if (m_temporaryBackups.Remove(backup.ID))
- m_temporaryBackups.Add(backup.ID, (Backup)backup);
- }
-
- public IBackup GetTemporaryBackup(string id)
- {
- if (string.IsNullOrEmpty(id))
- return null;
-
- lock(m_lock)
- {
- Backup b;
- m_temporaryBackups.TryGetValue(id, out b);
- return b;
- }
- }
-
- public ServerSettings ApplicationSettings { get; private set; }
-
- internal IDictionary GetMetadata(long id)
- {
- lock(m_lock)
- return ReadFromDb(
- (rd) => new KeyValuePair(
- ConvertToString(rd, 0),
- ConvertToString(rd, 1)
- ),
- @"SELECT ""Name"", ""Value"" FROM ""Metadata"" WHERE ""BackupID"" = ? ", id)
- .ToDictionary((k) => k.Key, (k) => k.Value);
- }
-
- internal void SetMetadata(IDictionary values, long id, System.Data.IDbTransaction transaction)
- {
- lock(m_lock)
- using(var tr = transaction == null ? m_connection.BeginTransaction() : null)
- {
- OverwriteAndUpdateDb(
- tr,
- @"DELETE FROM ""Metadata"" WHERE ""BackupID"" = ?", new object[] { id },
- values ?? new Dictionary(),
- @"INSERT INTO ""Metadata"" (""BackupID"", ""Name"", ""Value"") VALUES (?, ?, ?)",
- (f) => new object[] { id, f.Key, f.Value }
- );
-
- if (tr != null)
- tr.Commit();
- }
- }
-
- internal IFilter[] GetFilters(long id)
- {
- lock(m_lock)
- return ReadFromDb(
- (rd) => (IFilter)new Filter() {
- Order = ConvertToInt64(rd, 0),
- Include = ConvertToBoolean(rd, 1),
- Expression = ConvertToString(rd, 2) ?? ""
- },
- @"SELECT ""Order"", ""Include"", ""Expression"" FROM ""Filter"" WHERE ""BackupID"" = ? ORDER BY ""Order"" ", id)
- .ToArray();
- }
-
- internal void SetFilters(IEnumerable values, long id, System.Data.IDbTransaction transaction = null)
- {
- lock(m_lock)
- using(var tr = transaction == null ? m_connection.BeginTransaction() : null)
- {
- OverwriteAndUpdateDb(
- tr,
- @"DELETE FROM ""Filter"" WHERE ""BackupID"" = ?", new object[] { id },
- values,
- @"INSERT INTO ""Filter"" (""BackupID"", ""Order"", ""Include"", ""Expression"") VALUES (?, ?, ?, ?)",
- (f) => new object[] { id, f.Order, f.Include, f.Expression }
- );
-
- if (tr != null)
- tr.Commit();
- }
- }
-
- public ISetting[] GetSettings(long id)
- {
- lock(m_lock)
- return ReadFromDb(
- (rd) => (ISetting)new Setting() {
- Filter = ConvertToString(rd, 0) ?? "",
- Name = ConvertToString(rd, 1) ?? "",
- Value = ConvertToString(rd, 2) ?? ""
- //TODO: Attach the argument information
- },
- @"SELECT ""Filter"", ""Name"", ""Value"" FROM ""Option"" WHERE ""BackupID"" = ?", id)
- .ToArray();
- }
-
- internal void SetSettings(IEnumerable values, long id, System.Data.IDbTransaction transaction = null)
- {
- lock(m_lock)
- using(var tr = transaction == null ? m_connection.BeginTransaction() : null)
- {
- OverwriteAndUpdateDb(
- tr,
- @"DELETE FROM ""Option"" WHERE ""BackupID"" = ?", new object[] { id },
- values,
- @"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (?, ?, ?, ?)",
- (f) => {
- if (Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER.Equals(f.Value))
- throw new Exception("Attempted to save a property with the placeholder password");
- return new object[] { id, f.Filter ?? "", f.Name, f.Value ?? "" };
- }
- );
-
- if (tr != null)
- tr.Commit();
- }
- }
-
- internal string[] GetSources(long id)
- {
- lock(m_lock)
- return ReadFromDb(
- (rd) => ConvertToString(rd, 0),
- @"SELECT ""Path"" FROM ""Source"" WHERE ""BackupID"" = ?", id)
- .ToArray();
- }
-
- internal void SetSources(IEnumerable values, long id, System.Data.IDbTransaction transaction)
- {
- lock(m_lock)
- using(var tr = transaction == null ? m_connection.BeginTransaction() : null)
- {
- OverwriteAndUpdateDb(
- tr,
- @"DELETE FROM ""Source"" WHERE ""BackupID"" = ?", new object[] { id },
- values,
- @"INSERT INTO ""Source"" (""BackupID"", ""Path"") VALUES (?, ?)",
- (f) => new object[] { id, f }
- );
-
- if (tr != null)
- tr.Commit();
- }
- }
-
- internal long[] GetBackupIDsForTags(string[] tags)
- {
- if (tags == null || tags.Length == 0)
- return new long[0];
-
- if (tags.Length == 1 && tags[0].StartsWith("ID=", StringComparison.Ordinal))
- return new long[] { long.Parse(tags[0].Substring("ID=".Length)) };
-
- lock(m_lock)
- using(var cmd = m_connection.CreateCommand())
- {
- var sb = new StringBuilder();
-
- foreach(var t in tags)
- {
- if (sb.Length != 0)
- sb.Append(" OR ");
- sb.Append(@" ("","" || ""Tags"" || "","" LIKE ""%,"" || ? || "",%"") ");
-
- var p = cmd.CreateParameter();
- p.Value = t;
- cmd.Parameters.Add(p);
- }
-
- cmd.CommandText = @"SELECT ""ID"" FROM ""Backup"" WHERE " + sb;
-
- return Read(cmd, (rd) => ConvertToInt64(rd, 0)).ToArray();
- }
- }
-
- internal IBackup GetBackup(string id)
- {
- if (string.IsNullOrWhiteSpace(id))
- throw new ArgumentNullException(nameof(id));
-
- return long.TryParse(id, out long lid) ? GetBackup(lid) : GetTemporaryBackup(id);
- }
-
- internal IBackup GetBackup(long id)
- {
- lock(m_lock)
- {
- var bk = ReadFromDb(
- (rd) => new Backup {
- ID = ConvertToInt64(rd, 0).ToString(),
- Name = ConvertToString(rd, 1),
- Description = ConvertToString(rd, 2),
- Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
- TargetURL = ConvertToString(rd, 4),
- DBPath = ConvertToString(rd, 5),
- },
- @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" WHERE ID = ?", id)
- .FirstOrDefault();
-
- if (bk != null)
- bk.LoadChildren(this);
-
- return bk;
- }
- }
-
- internal ISchedule GetSchedule(long id)
- {
- lock(m_lock)
- {
- var bk = ReadFromDb(
- (rd) => new Schedule {
- ID = ConvertToInt64(rd, 0),
- Tags = (ConvertToString(rd, 1) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
- Time = ConvertToDateTime(rd, 2),
- Repeat = ConvertToString(rd, 3),
- LastRun = ConvertToDateTime(rd, 4),
- Rule = ConvertToString(rd, 5),
- },
- @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" WHERE ID = ?", id)
- .FirstOrDefault();
-
- return bk;
- }
- }
-
- internal Boolean IsUnencryptedOrPassphraseStored(long id)
- {
- lock (m_lock)
- {
- var usesEncryption = ReadFromDb(
- (rd) => ConvertToBoolean(rd, 0),
- @"SELECT VALUE != """" FROM ""Option"" WHERE BackupID = ? AND NAME='encryption-module'", id)
- .FirstOrDefault();
-
- if (!usesEncryption)
- {
- return true;
- }
-
- return ReadFromDb(
- (rd) => ConvertToBoolean(rd, 0),
- @"SELECT VALUE != """" FROM ""Option"" WHERE BackupID = ? AND NAME='passphrase'", id)
- .FirstOrDefault();
- }
- }
-
- internal long[] GetScheduleIDsFromTags(string[] tags)
- {
- if (tags == null || tags.Length == 0)
- return new long[0];
-
- lock(m_lock)
- using(var cmd = m_connection.CreateCommand())
- {
- var sb = new StringBuilder();
-
- foreach(var t in tags)
- {
- if (sb.Length != 0)
- sb.Append(" OR ");
- sb.Append(@" ("","" || ""Tags"" || "","" LIKE ""%,"" || ? || "",%"") ");
-
- var p = cmd.CreateParameter();
- p.Value = t;
- cmd.Parameters.Add(p);
- }
-
- cmd.CommandText = @"SELECT ""ID"" FROM ""Schedule"" WHERE " + sb;
-
- return Read(cmd, (rd) => ConvertToInt64(rd, 0)).ToArray();
- }
- }
-
- public void AddOrUpdateBackupAndSchedule(IBackup item, ISchedule schedule)
- {
- AddOrUpdateBackup(item, true, schedule);
- }
-
- public string ValidateBackup(IBackup item, ISchedule schedule)
- {
- if (string.IsNullOrWhiteSpace(item.Name))
- return "Missing a name";
-
- if (string.IsNullOrWhiteSpace(item.TargetURL))
- return "Missing a target";
-
- if (item.Sources == null || item.Sources.Any(x => string.IsNullOrWhiteSpace(x)) || item.Sources.Length == 0)
- return "Invalid source list";
-
- var disabled_encryption = false;
- var passphrase = string.Empty;
- var gpgAsymmetricEncryption = false;
- if (item.Settings != null)
- {
- foreach (var s in item.Settings)
-
- if (string.Equals(s.Name, "--no-encryption", StringComparison.OrdinalIgnoreCase))
- disabled_encryption = string.IsNullOrWhiteSpace(s.Value) || Library.Utility.Utility.ParseBool(s.Value, false);
- else if (string.Equals(s.Name, "passphrase", StringComparison.OrdinalIgnoreCase))
- passphrase = s.Value;
- else if (string.Equals(s.Name, "keep-versions", StringComparison.OrdinalIgnoreCase))
- {
- int i;
- if (!int.TryParse(s.Value, out i) || i <= 0)
- return "Retention value must be a positive integer";
- }
- else if (string.Equals(s.Name, "keep-time", StringComparison.OrdinalIgnoreCase))
- {
- try
- {
- var ts = Library.Utility.Timeparser.ParseTimeSpan(s.Value);
- if (ts <= TimeSpan.FromMinutes(5))
- return "Retention value must be more than 5 minutes";
- }
- catch
- {
- return "Retention value must be a valid timespan";
- }
- }
- else if (string.Equals(s.Name, "dblock-size", StringComparison.OrdinalIgnoreCase))
- {
- try
- {
- var ds = Library.Utility.Sizeparser.ParseSize(s.Value);
- if (ds < 1024 * 1024)
- return "DBlock size must be at least 1MB";
- }
- catch
- {
- return "DBlock value must be a valid size string";
- }
- }
- else if (string.Equals(s.Name, "--blocksize", StringComparison.OrdinalIgnoreCase))
- {
- try
- {
- var ds = Library.Utility.Sizeparser.ParseSize(s.Value);
- if (ds < 1024 || ds > int.MaxValue)
- return "The blocksize must be at least 1KB";
- }
- catch
- {
- return "The blocksize value must be a valid size string";
- }
- }
- else if (string.Equals(s.Name, "--prefix", StringComparison.OrdinalIgnoreCase))
- {
- if (!string.IsNullOrWhiteSpace(s.Value) && s.Value.Contains("-"))
- return "The prefix cannot contain hyphens (-)";
- }
- else if (string.Equals(s.Name, "--gpg-encryption-command", StringComparison.OrdinalIgnoreCase)) {
- gpgAsymmetricEncryption = string.Equals(s.Value, "--encrypt", StringComparison.OrdinalIgnoreCase);
- }
- }
-
- if (!disabled_encryption && !gpgAsymmetricEncryption && string.IsNullOrWhiteSpace(passphrase))
- return "Missing passphrase";
-
- if (schedule != null)
- {
- try
- {
- var ts = Library.Utility.Timeparser.ParseTimeSpan(schedule.Repeat);
- if (ts <= TimeSpan.FromMinutes(5))
- return "Schedule repetition time must be more than 5 minutes";
- }
- catch
- {
- return "Schedule repetition value must be a valid timespan";
- }
-
- }
-
- return null;
- }
-
- internal void UpdateBackupDBPath(IBackup item, string path)
- {
- lock (m_lock)
- {
- using (var tr = m_connection.BeginTransaction())
- {
- using (var cmd = m_connection.CreateCommand())
- {
- cmd.Transaction = tr;
- cmd.Parameters.Add(cmd.CreateParameter());
- ((System.Data.IDbDataParameter) cmd.Parameters[0]).Value = path;
- cmd.Parameters.Add(cmd.CreateParameter());
- ((System.Data.IDbDataParameter) cmd.Parameters[1]).Value = item.ID;
-
- cmd.CommandText = @"UPDATE ""Backup"" SET ""DBPath""=? WHERE ""ID""=?";
- cmd.ExecuteNonQuery();
- tr.Commit();
- }
- }
- }
-
- System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID);
- Program.StatusEventNotifyer.SignalNewEvent();
- }
-
- private void AddOrUpdateBackup(IBackup item, bool updateSchedule, ISchedule schedule)
- {
- lock(m_lock)
- {
- bool update = item.ID != null;
- if (!update && item.DBPath == null)
- {
- var folder = Program.DataFolder;
- if (!System.IO.Directory.Exists(folder))
- System.IO.Directory.CreateDirectory(folder);
-
- for(var i = 0; i < 100; i++)
- {
- var guess = System.IO.Path.Combine(folder, System.IO.Path.ChangeExtension(Duplicati.Library.Main.DatabaseLocator.GenerateRandomName(), ".sqlite"));
- if (!System.IO.File.Exists(guess))
- {
- ((Backup)item).DBPath = guess;
- break;
- }
- }
-
- if (item.DBPath == null)
- throw new Exception("Unable to generate a unique database file name");
- }
-
- using(var tr = m_connection.BeginTransaction())
- {
- OverwriteAndUpdateDb(
- tr,
- null,
- new object[] { long.Parse(item.ID ?? "-1") },
- new IBackup[] { item },
- update ?
- @"UPDATE ""Backup"" SET ""Name""=?, ""Description""=?, ""Tags""=?, ""TargetURL""=? WHERE ""ID""=?" :
- @"INSERT INTO ""Backup"" (""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"") VALUES (?,?,?,?,?)",
- (n) => {
-
- if (n.TargetURL.IndexOf(Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER, StringComparison.Ordinal) >= 0)
- throw new Exception("Attempted to save a backup with the password placeholder");
- if (update && long.Parse(n.ID) <= 0)
- throw new Exception("Invalid update, cannot update application settings through update method");
-
- return new object[] {
- n.Name,
- n.Description ?? "" , // Description is optional but the column is set to NOT NULL, an additional check is welcome
- string.Join(",", n.Tags ?? new string[0]),
- n.TargetURL,
- update ? item.ID : n.DBPath
- };
- });
-
- if (!update)
- using(var cmd = m_connection.CreateCommand())
- {
- cmd.Transaction = tr;
- cmd.CommandText = @"SELECT last_insert_rowid();";
- item.ID = ExecuteScalarInt64(cmd).ToString();
- }
-
- var id = long.Parse(item.ID);
-
- if (long.Parse(item.ID) <= 0)
- throw new Exception("Invalid addition, cannot update application settings through update method");
-
- SetSources(item.Sources, id, tr);
- SetSettings(item.Settings, id, tr);
- SetFilters(item.Filters, id, tr);
- SetMetadata(item.Metadata, id, tr);
-
- if (updateSchedule)
- {
- var tags = new string[] { "ID=" + item.ID };
- var existing = GetScheduleIDsFromTags(tags);
- if (schedule == null && existing.Any())
- DeleteFromDb("Schedule", existing.First(), tr);
- else if (schedule != null)
- {
- if (existing.Any())
- {
- var cur = GetSchedule(existing.First());
- cur.AllowedDays = schedule.AllowedDays;
- cur.Repeat = schedule.Repeat;
- cur.Tags = schedule.Tags;
- cur.Time = schedule.Time;
-
- schedule = cur;
- }
- else
- {
- schedule.ID = -1;
- }
-
- schedule.Tags = tags;
- AddOrUpdateSchedule(schedule, tr);
- }
- }
-
- tr.Commit();
- System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID);
- Program.StatusEventNotifyer.SignalNewEvent();
- }
- }
- }
-
- internal void AddOrUpdateSchedule(ISchedule item)
- {
- lock(m_lock)
- using(var tr = m_connection.BeginTransaction())
- {
- AddOrUpdateSchedule(item, tr);
- tr.Commit();
- System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID);
- Program.StatusEventNotifyer.SignalNewEvent();
- }
- }
-
- private void AddOrUpdateSchedule(ISchedule item, System.Data.IDbTransaction tr)
- {
- lock(m_lock)
- {
- bool update = item.ID >= 0;
- OverwriteAndUpdateDb(
- tr,
- null,
- new object[] { item.ID },
- new ISchedule[] { item },
- update ?
- @"UPDATE ""Schedule"" SET ""Tags""=?, ""Time""=?, ""Repeat""=?, ""LastRun""=?, ""Rule""=? WHERE ""ID""=?" :
- @"INSERT INTO ""Schedule"" (""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"") VALUES (?,?,?,?,?)",
- (n) => new object[] {
- string.Join(",", n.Tags),
- Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(n.Time),
- n.Repeat,
- Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(n.LastRun),
- n.Rule ?? "",
- update ? (object)item.ID : null
- });
-
- if (!update)
- using(var cmd = m_connection.CreateCommand())
- {
- cmd.Transaction = tr;
- cmd.CommandText = @"SELECT last_insert_rowid();";
- item.ID = ExecuteScalarInt64(cmd);
- }
- }
- }
-
- public void DeleteBackup(long ID)
- {
- if (ID < 0)
- return;
-
- lock(m_lock)
- {
- using(var tr = m_connection.BeginTransaction())
- {
- var existing = GetScheduleIDsFromTags(new string[] { "ID=" + ID.ToString() });
- if (existing.Any())
- DeleteFromDb("Schedule", existing.First(), tr);
-
- DeleteFromDb("ErrorLog", ID, "BackupID", tr);
- DeleteFromDb("Filter", ID, "BackupID", tr);
- DeleteFromDb("Log", ID, "BackupID", tr);
- DeleteFromDb("Metadata", ID, "BackupID", tr);
- DeleteFromDb("Option", ID, "BackupID", tr);
- DeleteFromDb("Source", ID, "BackupID", tr);
-
- DeleteFromDb("Backup", ID, tr);
-
- tr.Commit();
- }
- }
-
- System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID);
- Program.StatusEventNotifyer.SignalNewEvent();
- }
-
- public void DeleteBackup(IBackup backup)
- {
- if (backup.IsTemporary)
- UnregisterTemporaryBackup(backup);
- else
- DeleteBackup(long.Parse(backup.ID));
- }
-
- public void DeleteSchedule(long ID)
- {
- if (ID < 0)
- return;
-
- lock(m_lock)
- DeleteFromDb("Schedule", ID);
-
- System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID);
- Program.StatusEventNotifyer.SignalNewEvent();
- }
-
- public void DeleteSchedule(ISchedule schedule)
- {
- DeleteSchedule(schedule.ID);
- }
-
- public IBackup[] Backups
- {
- get
- {
- lock(m_lock)
- {
- var lst = ReadFromDb(
- (rd) => (IBackup)new Backup() {
- ID = ConvertToInt64(rd, 0).ToString(),
- Name = ConvertToString(rd, 1),
- Description = ConvertToString(rd, 2),
- Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
- TargetURL = ConvertToString(rd, 4),
- DBPath = ConvertToString(rd, 5),
- },
- @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" ")
- .ToArray();
-
- foreach(var n in lst)
- n.Metadata = GetMetadata(long.Parse(n.ID));
-
-
- return lst;
- }
- }
- }
-
- public ISchedule[] Schedules
- {
- get
- {
- lock(m_lock)
- return ReadFromDb(
- (rd) => (ISchedule)new Schedule() {
- ID = ConvertToInt64(rd, 0),
- Tags = (ConvertToString(rd, 1) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
- Time = ConvertToDateTime(rd, 2),
- Repeat = ConvertToString(rd, 3),
- LastRun = ConvertToDateTime(rd, 4),
- Rule = ConvertToString(rd, 5),
- },
- @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" ")
- .ToArray();
- }
- }
-
-
- public IFilter[] Filters
- {
- get { return GetFilters(ANY_BACKUP_ID); }
- set { SetFilters(value, ANY_BACKUP_ID); }
- }
-
- public ISetting[] Settings
- {
- get { return GetSettings(ANY_BACKUP_ID); }
- set { SetSettings(value, ANY_BACKUP_ID); }
- }
-
- public INotification[] GetNotifications()
- {
- lock(m_lock)
- return ReadFromDb(null).Cast().ToArray();
- }
-
- public bool DismissNotification(long id)
- {
- lock(m_lock)
- {
- var notifications = GetNotifications();
- var cur = notifications.FirstOrDefault(x => x.ID == id);
- if (cur == null)
- return false;
-
- DeleteFromDb(typeof(Notification).Name, id);
- Program.DataConnection.ApplicationSettings.UnackedError = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Error);
- Program.DataConnection.ApplicationSettings.UnackedWarning = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Warning);
- }
-
- System.Threading.Interlocked.Increment(ref Program.LastNotificationUpdateID);
- Program.StatusEventNotifyer.SignalNewEvent();
-
- return true;
- }
-
- public void RegisterNotification(Serialization.NotificationType type, string title, string message, Exception ex, string backupid, string action, string logid, string messageid, string logtag, Func conflicthandler)
- {
- lock(m_lock)
- {
- var notification = new Notification()
- {
- ID = -1,
- Type = type,
- Title = title,
- Message = message,
- Exception = ex == null ? "" : ex.ToString(),
- BackupID = backupid,
- Action = action ?? "",
- Timestamp = DateTime.UtcNow,
- LogEntryID = logid,
- MessageID = messageid,
- MessageLogTag = logtag
- };
-
- var conflictResult = conflicthandler(notification, GetNotifications());
- if (conflictResult == null)
- return;
-
- if (conflictResult != notification)
- DeleteFromDb(typeof(Notification).Name, conflictResult.ID);
-
- OverwriteAndUpdateDb(null, null, null, new Notification[] { notification }, false);
-
- if (type == Duplicati.Server.Serialization.NotificationType.Error)
- Program.DataConnection.ApplicationSettings.UnackedError = true;
- else if (type == Duplicati.Server.Serialization.NotificationType.Warning)
- Program.DataConnection.ApplicationSettings.UnackedWarning = true;
- }
-
- System.Threading.Interlocked.Increment(ref Program.LastNotificationUpdateID);
- Program.StatusEventNotifyer.SignalNewEvent();
- }
-
- //Workaround to clean up the database after invalid settings update
- public void FixInvalidBackupId()
- {
- using(var cmd = m_connection.CreateCommand())
- using (var tr = m_connection.BeginTransaction())
- {
- cmd.Transaction = tr;
- cmd.Parameters.Add(cmd.CreateParameter());
- ((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = -1;
- cmd.CommandText = @"DELETE FROM ""Option"" WHERE ""BackupID"" = ?";
- cmd.ExecuteNonQuery();
- cmd.CommandText = @"DELETE FROM ""Metadata"" WHERE ""BackupID"" = ?";
- cmd.ExecuteNonQuery();
- cmd.CommandText = @"DELETE FROM ""Filter"" WHERE ""BackupID"" = ?";
- cmd.ExecuteNonQuery();
- cmd.CommandText = @"DELETE FROM ""Source"" WHERE ""BackupID"" = ?";
- cmd.ExecuteNonQuery();
-
- cmd.Parameters.Clear();
- cmd.Parameters.Add(cmd.CreateParameter());
-
- ((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = "ID=-1";
- cmd.CommandText = @"DELETE FROM ""Schedule"" WHERE ""Tags"" = ?";
- cmd.ExecuteNonQuery();
- tr.Commit();
- }
-
- ApplicationSettings.FixedInvalidBackupId = true;
- }
-
- public string[] GetUISettingsSchemes()
- {
- lock(m_lock)
- return ReadFromDb(
- (rd) => ConvertToString(rd, 0) ?? "",
- @"SELECT DISTINCT ""Scheme"" FROM ""UIStorage""")
- .ToArray();
- }
-
- public IDictionary GetUISettings(string scheme)
- {
- lock(m_lock)
- return ReadFromDb(
- (rd) => new KeyValuePair(
- ConvertToString(rd, 0) ?? "",
- ConvertToString(rd, 1) ?? ""
- ),
- @"SELECT ""Key"", ""Value"" FROM ""UIStorage"" WHERE ""Scheme"" = ?",
- scheme)
- .GroupBy(x => x.Key)
- .ToDictionary(x => x.Key, x => x.Last().Value);
- }
-
- public void SetUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null)
- {
- lock(m_lock)
- using(var tr = transaction == null ? m_connection.BeginTransaction() : null)
- {
- OverwriteAndUpdateDb(
- tr,
- @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = ?", new object[] { scheme },
- values,
- @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (?, ?, ?)",
- (f) => {
- return new object[] { scheme, f.Key ?? "", f.Value ?? "" };
- }
- );
-
- if (tr != null)
- tr.Commit();
- }
- }
-
- public void UpdateUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null)
- {
- lock (m_lock)
- using (var tr = transaction == null ? m_connection.BeginTransaction() : null)
- {
- OverwriteAndUpdateDb(
- tr,
- @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = ? AND ""Key"" IN (?)", new object[] { scheme, values.Keys },
- values.Where(x => x.Value != null),
- @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (?, ?, ?)",
- (f) =>
- {
- return new object[] { scheme, f.Key ?? "", f.Value ?? "" };
- }
- );
-
- if (tr != null)
- tr.Commit();
- }
- }
-
- public TempFile[] GetTempFiles()
- {
- lock(m_lock)
- return ReadFromDb(null).ToArray();
- }
-
- public void DeleteTempFile(long id)
- {
- lock(m_lock)
- DeleteFromDb(typeof(TempFile).Name, id);
- }
-
- public long RegisterTempFile(string origin, string path, DateTime expires)
- {
- var tempfile = new TempFile() {
- Timestamp = DateTime.Now,
- Origin = origin,
- Path = path,
- Expires = expires
- };
-
- OverwriteAndUpdateDb(null, null, null, new TempFile[] { tempfile }, false);
-
- return tempfile.ID;
- }
-
- public void PurgeLogData(DateTime purgeDate)
- {
- var t = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(purgeDate);
-
- using(var tr = m_connection.BeginTransaction())
- using(var cmd = m_connection.CreateCommand())
- {
- cmd.Transaction = tr;
- cmd.CommandText = @"DELETE FROM ""ErrorLog"" WHERE ""Timestamp"" < ?";
- cmd.Parameters.Add(cmd.CreateParameter());
- ((System.Data.IDataParameter)cmd.Parameters[0]).Value = t;
- cmd.ExecuteNonQuery();
-
- tr.Commit();
- }
- }
-
- private static DateTime ConvertToDateTime(System.Data.IDataReader rd, int index)
- {
- var unixTime = ConvertToInt64(rd, index);
- return unixTime == 0 ? new DateTime(0) : Library.Utility.Utility.EPOCH.AddSeconds(unixTime);
- }
-
- private static bool ConvertToBoolean(System.Data.IDataReader rd, int index)
- {
- return ConvertToInt64(rd, index) == 1;
- }
-
- private static string ConvertToString(System.Data.IDataReader rd, int index)
- {
- var r = rd.GetValue(index);
- return r == null || r == DBNull.Value ? null : r.ToString();
- }
-
- private static long ConvertToInt64(System.Data.IDataReader rd, int index)
- {
- try
- {
- if (!rd.IsDBNull(index))
- return rd.GetInt64(index);
- }
- catch
- {
- }
-
- return -1;
- }
-
- private static long ExecuteScalarInt64(System.Data.IDbCommand cmd, long defaultValue = -1)
- {
- using(var rd = cmd.ExecuteReader())
- return rd.Read() ? ConvertToInt64(rd, 0) : defaultValue;
- }
-
- private static string ExecuteScalarString(System.Data.IDbCommand cmd)
- {
- using(var rd = cmd.ExecuteReader())
- return rd.Read() ? ConvertToString(rd, 0) : null;
-
- }
-
- private object ConvertToEnum(Type enumType, System.Data.IDataReader rd, int index, object @default)
- {
- try
- {
- return Enum.Parse(enumType, ConvertToString(rd, index));
- }
- catch
- {
- }
-
- return @default;
- }
-
- // Overloaded function for legacy functionality
- private bool DeleteFromDb(string tablename, long id, System.Data.IDbTransaction transaction = null)
- {
- return DeleteFromDb(tablename, id, "ID", transaction);
- }
-
- // New function that allows to delete rows from tables with arbitrary identifier values (e.g. ID or BackupID)
- private bool DeleteFromDb(string tablename, long id, string identifier, System.Data.IDbTransaction transaction = null)
- {
- if (transaction == null)
- {
- using(var tr = m_connection.BeginTransaction())
- {
- var r = DeleteFromDb(tablename, id, tr);
- tr.Commit();
- return r;
- }
- }
- else
- {
- using(var cmd = m_connection.CreateCommand())
- {
- cmd.Transaction = transaction;
- cmd.CommandText = string.Format(@"DELETE FROM ""{0}"" WHERE ""{1}""=?", tablename, identifier);
- var p = cmd.CreateParameter();
- p.Value = id;
- cmd.Parameters.Add(p);
-
- var r = cmd.ExecuteNonQuery();
- // Roll back the transaction if more than 1 ID was deleted. Multiple "BackupID" rows being deleted isn't a problem.
- if (identifier == "ID" && r > 1)
- throw new Exception(string.Format("Too many records attempted deleted from table {0} for id {1}: {2}", tablename, id, r));
- return r == 1;
- }
- }
- }
-
- private static IEnumerable Read(System.Data.IDbCommand cmd, Func f)
- {
- using(var rd = cmd.ExecuteReader())
- while(rd.Read())
- yield return f(rd);
- }
-
- private static IEnumerable Read(System.Data.IDataReader rd, Func f)
- {
- while(rd.Read())
- yield return f();
- }
-
- private System.Reflection.PropertyInfo[] GetORMFields()
- {
- var flags =
- System.Reflection.BindingFlags.FlattenHierarchy |
- System.Reflection.BindingFlags.Instance |
- System.Reflection.BindingFlags.Public;
-
- var supportedPropertyTypes = new Type[] {
- typeof(long),
- typeof(string),
- typeof(bool),
- typeof(DateTime)
- };
-
- return
- (from n in typeof(T).GetProperties(flags)
- where supportedPropertyTypes.Contains(n.PropertyType) || n.PropertyType.IsEnum
- select n).ToArray();
- }
-
- private IEnumerable ReadFromDb(string whereclause, params object[] args)
- {
- var properties = GetORMFields();
-
- var sql = string.Format(
- @"SELECT ""{0}"" FROM ""{1}"" {2} {3}",
- string.Join(@""", """, properties.Select(x => x.Name)),
- typeof(T).Name,
- string.IsNullOrWhiteSpace(whereclause) ? "" : " WHERE ",
- whereclause ?? ""
- );
-
- return ReadFromDb((rd) => {
- var item = Activator.CreateInstance();
- for(var i = 0; i < properties.Length; i++)
- {
- var prop = properties[i];
-
- if (prop.PropertyType.IsEnum)
- prop.SetValue(item, ConvertToEnum(prop.PropertyType, rd, i, Enum.GetValues(prop.PropertyType).GetValue(0)), null);
- else if (prop.PropertyType == typeof(string))
- prop.SetValue(item, ConvertToString(rd, i), null);
- else if (prop.PropertyType == typeof(long))
- prop.SetValue(item, ConvertToInt64(rd, i), null);
- else if (prop.PropertyType == typeof(bool))
- prop.SetValue(item, ConvertToBoolean(rd, i), null);
- else if (prop.PropertyType == typeof(DateTime))
- prop.SetValue(item, ConvertToDateTime(rd, i), null);
- }
-
- return item;
- }, sql, args);
- }
-
- private void OverwriteAndUpdateDb(System.Data.IDbTransaction transaction, string deleteSql, object[] deleteArgs, IEnumerable values, bool updateExisting)
- {
- var properties = GetORMFields();
- var idfield = properties.FirstOrDefault(x => x.Name == "ID");
- properties = properties.Where(x => x.Name != "ID").ToArray();
-
- string sql;
-
- if (updateExisting)
- {
- sql = string.Format(
- @"UPDATE ""{0}"" SET {1} WHERE ""ID""=?",
- typeof(T).Name,
- string.Join(@", ", properties.Select(x => string.Format(@"""{0}""=?", x.Name)))
- );
-
- properties = properties.Union(new System.Reflection.PropertyInfo[] { idfield }).ToArray();
- }
- else
- {
-
- sql = string.Format(
- @"INSERT INTO ""{0}"" (""{1}"") VALUES ({2})",
- typeof(T).Name,
- string.Join(@""", """, properties.Select(x => x.Name)),
- string.Join(@", ", properties.Select(x => "?"))
- );
- }
-
- OverwriteAndUpdateDb(transaction, deleteSql, deleteArgs, values, sql, (item) =>
- {
- return properties.Select((x) =>
- {
- var val = x.GetValue(item, null);
-
- if (x.PropertyType.IsEnum)
- val = val.ToString();
- else if (x.PropertyType == typeof(DateTime))
- val = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds((DateTime)val);
-
- return val;
- }).ToArray();
- });
-
- if (!updateExisting && values.Count() == 1 && idfield != null)
- using(var cmd = m_connection.CreateCommand())
- {
- cmd.Transaction = transaction;
- cmd.CommandText = @"SELECT last_insert_rowid();";
- if (idfield.PropertyType == typeof(string))
- idfield.SetValue(values.First(), ExecuteScalarString(cmd), null);
- else
- idfield.SetValue(values.First(), ExecuteScalarInt64(cmd), null);
- }
- }
-
- private IEnumerable ReadFromDb(Func f, string sql, params object[] args)
- {
- using(var cmd = m_connection.CreateCommand())
- {
- cmd.CommandText = sql;
- if (args != null)
- foreach(var a in args)
- {
- var p = cmd.CreateParameter();
- p.Value = a;
- cmd.Parameters.Add(p);
- }
-
- return Read(cmd, f).ToArray();
- }
- }
-
- private void OverwriteAndUpdateDb(System.Data.IDbTransaction transaction, string deleteSql, object[] deleteArgs, IEnumerable values, string insertSql, Func f)
- {
- using(var cmd = m_connection.CreateCommand())
- {
- cmd.Transaction = transaction;
-
- if (!string.IsNullOrEmpty(deleteSql))
- {
- cmd.CommandText = deleteSql;
- if (deleteArgs != null)
- foreach(var a in deleteArgs)
- {
- var p = cmd.CreateParameter();
- p.Value = a;
- cmd.Parameters.Add(p);
- }
-
- cmd.ExecuteNonQuery();
- cmd.Parameters.Clear();
- }
-
- cmd.CommandText = insertSql;
-
- foreach(var n in values)
- {
- var r = f(n);
- if (r == null)
- continue;
-
- while (cmd.Parameters.Count < r.Length)
- cmd.Parameters.Add(cmd.CreateParameter());
-
- for(var i = 0; i < r.Length; i++)
- ((System.Data.IDbDataParameter)cmd.Parameters[i]).Value = r[i];
-
- cmd.ExecuteNonQuery();
- }
- }
- }
-
- #region IDisposable implementation
- public void Dispose()
- {
- if (m_errorcmd != null)
- try { if (m_errorcmd != null) m_errorcmd.Dispose(); }
- catch { }
- finally { m_errorcmd = null; }
-
-
- try
- {
- if (m_connection != null)
- m_connection.Dispose();
- }
- catch
- {
- }
- }
- #endregion
- }
-
-}
-
+// 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.Collections.Generic;
+using System.Linq;
+using Duplicati.Server.Serialization.Interface;
+using System.Text;
+using Duplicati.Library.RestAPI;
+
+namespace Duplicati.Server.Database
+{
+ public class Connection : IDisposable
+ {
+ private readonly System.Data.IDbConnection m_connection;
+ private System.Data.IDbCommand m_errorcmd;
+ public readonly object m_lock = new object();
+ public const int ANY_BACKUP_ID = -1;
+ public const int SERVER_SETTINGS_ID = -2;
+ private readonly Dictionary m_temporaryBackups = new Dictionary();
+
+ public Connection(System.Data.IDbConnection connection)
+ {
+ m_connection = connection;
+ m_errorcmd = m_connection.CreateCommand();
+ m_errorcmd.CommandText = @"INSERT INTO ""ErrorLog"" (""BackupID"", ""Message"", ""Exception"", ""Timestamp"") VALUES (?,?,?,?)";
+ for(var i = 0; i < 4; i++)
+ m_errorcmd.Parameters.Add(m_errorcmd.CreateParameter());
+
+ this.ApplicationSettings = new ServerSettings(this);
+ }
+
+ public void LogError(string backupid, string message, Exception ex)
+ {
+ lock(m_lock)
+ {
+ if (!long.TryParse(backupid, out long id))
+ id = -1;
+ ((System.Data.IDbDataParameter)m_errorcmd.Parameters[0]).Value = id;
+ ((System.Data.IDbDataParameter)m_errorcmd.Parameters[1]).Value = message;
+ ((System.Data.IDbDataParameter)m_errorcmd.Parameters[2]).Value = ex?.ToString();
+ ((System.Data.IDbDataParameter)m_errorcmd.Parameters[3]).Value = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(DateTime.UtcNow);
+ m_errorcmd.ExecuteNonQuery();
+ }
+ }
+
+ internal void ExecuteWithCommand(Action f)
+ {
+ lock(m_lock)
+ using(var cmd = m_connection.CreateCommand())
+ f(cmd);
+ }
+
+ internal Serializable.ImportExportStructure PrepareBackupForExport(IBackup backup)
+ {
+ var scheduleId = GetScheduleIDsFromTags(new string[] { "ID=" + backup.ID });
+ return new Serializable.ImportExportStructure() {
+ CreatedByVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(),
+ Backup = (Database.Backup)backup,
+ Schedule = (Database.Schedule)(scheduleId.Any() ? GetSchedule(scheduleId.First()) : null),
+ DisplayNames = SpecialFolders.GetSourceNames(backup)
+ };
+ }
+
+ public string RegisterTemporaryBackup(IBackup backup)
+ {
+ lock(m_lock)
+ {
+ if (backup == null)
+ throw new ArgumentNullException(nameof(backup));
+ if (backup.ID != null)
+ throw new ArgumentException("Backup is already active, cannot make temporary");
+
+ backup.ID = Guid.NewGuid().ToString("D");
+ m_temporaryBackups.Add(backup.ID, (Backup)backup);
+ return backup.ID;
+ }
+ }
+
+ public void UnregisterTemporaryBackup(IBackup backup)
+ {
+ lock(m_lock)
+ m_temporaryBackups.Remove(backup.ID);
+ }
+
+ public void UpdateTemporaryBackup(IBackup backup)
+ {
+ lock(m_lock)
+ if (m_temporaryBackups.Remove(backup.ID))
+ m_temporaryBackups.Add(backup.ID, (Backup)backup);
+ }
+
+ public IBackup GetTemporaryBackup(string id)
+ {
+ if (string.IsNullOrEmpty(id))
+ return null;
+
+ lock(m_lock)
+ {
+ Backup b;
+ m_temporaryBackups.TryGetValue(id, out b);
+ return b;
+ }
+ }
+
+ public ServerSettings ApplicationSettings { get; private set; }
+
+ internal IDictionary GetMetadata(long id)
+ {
+ lock(m_lock)
+ return ReadFromDb(
+ (rd) => new KeyValuePair(
+ ConvertToString(rd, 0),
+ ConvertToString(rd, 1)
+ ),
+ @"SELECT ""Name"", ""Value"" FROM ""Metadata"" WHERE ""BackupID"" = ? ", id)
+ .ToDictionary((k) => k.Key, (k) => k.Value);
+ }
+
+ internal void SetMetadata(IDictionary values, long id, System.Data.IDbTransaction transaction)
+ {
+ lock(m_lock)
+ using(var tr = transaction == null ? m_connection.BeginTransaction() : null)
+ {
+ OverwriteAndUpdateDb(
+ tr,
+ @"DELETE FROM ""Metadata"" WHERE ""BackupID"" = ?", new object[] { id },
+ values ?? new Dictionary(),
+ @"INSERT INTO ""Metadata"" (""BackupID"", ""Name"", ""Value"") VALUES (?, ?, ?)",
+ (f) => new object[] { id, f.Key, f.Value }
+ );
+
+ if (tr != null)
+ tr.Commit();
+ }
+ }
+
+ internal IFilter[] GetFilters(long id)
+ {
+ lock(m_lock)
+ return ReadFromDb(
+ (rd) => (IFilter)new Filter() {
+ Order = ConvertToInt64(rd, 0),
+ Include = ConvertToBoolean(rd, 1),
+ Expression = ConvertToString(rd, 2) ?? ""
+ },
+ @"SELECT ""Order"", ""Include"", ""Expression"" FROM ""Filter"" WHERE ""BackupID"" = ? ORDER BY ""Order"" ", id)
+ .ToArray();
+ }
+
+ internal void SetFilters(IEnumerable values, long id, System.Data.IDbTransaction transaction = null)
+ {
+ lock(m_lock)
+ using(var tr = transaction == null ? m_connection.BeginTransaction() : null)
+ {
+ OverwriteAndUpdateDb(
+ tr,
+ @"DELETE FROM ""Filter"" WHERE ""BackupID"" = ?", new object[] { id },
+ values,
+ @"INSERT INTO ""Filter"" (""BackupID"", ""Order"", ""Include"", ""Expression"") VALUES (?, ?, ?, ?)",
+ (f) => new object[] { id, f.Order, f.Include, f.Expression }
+ );
+
+ if (tr != null)
+ tr.Commit();
+ }
+ }
+
+ public ISetting[] GetSettings(long id)
+ {
+ lock(m_lock)
+ return ReadFromDb(
+ (rd) => (ISetting)new Setting() {
+ Filter = ConvertToString(rd, 0) ?? "",
+ Name = ConvertToString(rd, 1) ?? "",
+ Value = ConvertToString(rd, 2) ?? ""
+ //TODO: Attach the argument information
+ },
+ @"SELECT ""Filter"", ""Name"", ""Value"" FROM ""Option"" WHERE ""BackupID"" = ?", id)
+ .ToArray();
+ }
+
+ internal void SetSettings(IEnumerable values, long id, System.Data.IDbTransaction transaction = null)
+ {
+ lock(m_lock)
+ using(var tr = transaction == null ? m_connection.BeginTransaction() : null)
+ {
+ OverwriteAndUpdateDb(
+ tr,
+ @"DELETE FROM ""Option"" WHERE ""BackupID"" = ?", new object[] { id },
+ values,
+ @"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (?, ?, ?, ?)",
+ (f) => {
+ if (Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER.Equals(f.Value))
+ throw new Exception("Attempted to save a property with the placeholder password");
+ return new object[] { id, f.Filter ?? "", f.Name, f.Value ?? "" };
+ }
+ );
+
+ if (tr != null)
+ tr.Commit();
+ }
+ }
+
+ internal string[] GetSources(long id)
+ {
+ lock(m_lock)
+ return ReadFromDb(
+ (rd) => ConvertToString(rd, 0),
+ @"SELECT ""Path"" FROM ""Source"" WHERE ""BackupID"" = ?", id)
+ .ToArray();
+ }
+
+ internal void SetSources(IEnumerable values, long id, System.Data.IDbTransaction transaction)
+ {
+ lock(m_lock)
+ using(var tr = transaction == null ? m_connection.BeginTransaction() : null)
+ {
+ OverwriteAndUpdateDb(
+ tr,
+ @"DELETE FROM ""Source"" WHERE ""BackupID"" = ?", new object[] { id },
+ values,
+ @"INSERT INTO ""Source"" (""BackupID"", ""Path"") VALUES (?, ?)",
+ (f) => new object[] { id, f }
+ );
+
+ if (tr != null)
+ tr.Commit();
+ }
+ }
+
+ internal long[] GetBackupIDsForTags(string[] tags)
+ {
+ if (tags == null || tags.Length == 0)
+ return new long[0];
+
+ if (tags.Length == 1 && tags[0].StartsWith("ID=", StringComparison.Ordinal))
+ return new long[] { long.Parse(tags[0].Substring("ID=".Length)) };
+
+ lock(m_lock)
+ using(var cmd = m_connection.CreateCommand())
+ {
+ var sb = new StringBuilder();
+
+ foreach(var t in tags)
+ {
+ if (sb.Length != 0)
+ sb.Append(" OR ");
+ sb.Append(@" (',' || ""Tags"" || ',' LIKE '%,' || ? || ',%') ");
+
+ var p = cmd.CreateParameter();
+ p.Value = t;
+ cmd.Parameters.Add(p);
+ }
+
+ cmd.CommandText = @"SELECT ""ID"" FROM ""Backup"" WHERE " + sb;
+
+ return Read(cmd, (rd) => ConvertToInt64(rd, 0)).ToArray();
+ }
+ }
+
+ internal IBackup GetBackup(string id)
+ {
+ if (string.IsNullOrWhiteSpace(id))
+ throw new ArgumentNullException(nameof(id));
+
+ return long.TryParse(id, out long lid) ? GetBackup(lid) : GetTemporaryBackup(id);
+ }
+
+ internal IBackup GetBackup(long id)
+ {
+ lock(m_lock)
+ {
+ var bk = ReadFromDb(
+ (rd) => new Backup {
+ ID = ConvertToInt64(rd, 0).ToString(),
+ Name = ConvertToString(rd, 1),
+ Description = ConvertToString(rd, 2),
+ Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+ TargetURL = ConvertToString(rd, 4),
+ DBPath = ConvertToString(rd, 5),
+ },
+ @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" WHERE ID = ?", id)
+ .FirstOrDefault();
+
+ if (bk != null)
+ bk.LoadChildren(this);
+
+ return bk;
+ }
+ }
+
+ internal ISchedule GetSchedule(long id)
+ {
+ lock(m_lock)
+ {
+ var bk = ReadFromDb(
+ (rd) => new Schedule {
+ ID = ConvertToInt64(rd, 0),
+ Tags = (ConvertToString(rd, 1) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+ Time = ConvertToDateTime(rd, 2),
+ Repeat = ConvertToString(rd, 3),
+ LastRun = ConvertToDateTime(rd, 4),
+ Rule = ConvertToString(rd, 5),
+ },
+ @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" WHERE ID = ?", id)
+ .FirstOrDefault();
+
+ return bk;
+ }
+ }
+
+ internal Boolean IsUnencryptedOrPassphraseStored(long id)
+ {
+ lock (m_lock)
+ {
+ var usesEncryption = ReadFromDb(
+ (rd) => ConvertToBoolean(rd, 0),
+ @"SELECT VALUE != '' FROM ""Option"" WHERE BackupID = ? AND NAME='encryption-module'", id)
+ .FirstOrDefault();
+
+ if (!usesEncryption)
+ {
+ return true;
+ }
+
+ return ReadFromDb(
+ (rd) => ConvertToBoolean(rd, 0),
+ @"SELECT VALUE != '' FROM ""Option"" WHERE BackupID = ? AND NAME='passphrase'", id)
+ .FirstOrDefault();
+ }
+ }
+
+ internal long[] GetScheduleIDsFromTags(string[] tags)
+ {
+ if (tags == null || tags.Length == 0)
+ return new long[0];
+
+ lock(m_lock)
+ using(var cmd = m_connection.CreateCommand())
+ {
+ var sb = new StringBuilder();
+
+ foreach(var t in tags)
+ {
+ if (sb.Length != 0)
+ sb.Append(" OR ");
+ sb.Append(@" (',' || ""Tags"" || ',' LIKE '%,' || ? || ',%') ");
+
+ var p = cmd.CreateParameter();
+ p.Value = t;
+ cmd.Parameters.Add(p);
+ }
+
+ cmd.CommandText = @"SELECT ""ID"" FROM ""Schedule"" WHERE " + sb;
+
+ return Read(cmd, (rd) => ConvertToInt64(rd, 0)).ToArray();
+ }
+ }
+
+ public void AddOrUpdateBackupAndSchedule(IBackup item, ISchedule schedule)
+ {
+ AddOrUpdateBackup(item, true, schedule);
+ }
+
+ public string ValidateBackup(IBackup item, ISchedule schedule)
+ {
+ if (string.IsNullOrWhiteSpace(item.Name))
+ return "Missing a name";
+
+ if (string.IsNullOrWhiteSpace(item.TargetURL))
+ return "Missing a target";
+
+ if (item.Sources == null || item.Sources.Any(x => string.IsNullOrWhiteSpace(x)) || item.Sources.Length == 0)
+ return "Invalid source list";
+
+ var disabled_encryption = false;
+ var passphrase = string.Empty;
+ var gpgAsymmetricEncryption = false;
+ if (item.Settings != null)
+ {
+ foreach (var s in item.Settings)
+
+ if (string.Equals(s.Name, "--no-encryption", StringComparison.OrdinalIgnoreCase))
+ disabled_encryption = string.IsNullOrWhiteSpace(s.Value) || Library.Utility.Utility.ParseBool(s.Value, false);
+ else if (string.Equals(s.Name, "passphrase", StringComparison.OrdinalIgnoreCase))
+ passphrase = s.Value;
+ else if (string.Equals(s.Name, "keep-versions", StringComparison.OrdinalIgnoreCase))
+ {
+ int i;
+ if (!int.TryParse(s.Value, out i) || i <= 0)
+ return "Retention value must be a positive integer";
+ }
+ else if (string.Equals(s.Name, "keep-time", StringComparison.OrdinalIgnoreCase))
+ {
+ try
+ {
+ var ts = Library.Utility.Timeparser.ParseTimeSpan(s.Value);
+ if (ts <= TimeSpan.FromMinutes(5))
+ return "Retention value must be more than 5 minutes";
+ }
+ catch
+ {
+ return "Retention value must be a valid timespan";
+ }
+ }
+ else if (string.Equals(s.Name, "dblock-size", StringComparison.OrdinalIgnoreCase))
+ {
+ try
+ {
+ var ds = Library.Utility.Sizeparser.ParseSize(s.Value);
+ if (ds < 1024 * 1024)
+ return "DBlock size must be at least 1MB";
+ }
+ catch
+ {
+ return "DBlock value must be a valid size string";
+ }
+ }
+ else if (string.Equals(s.Name, "--blocksize", StringComparison.OrdinalIgnoreCase))
+ {
+ try
+ {
+ var ds = Library.Utility.Sizeparser.ParseSize(s.Value);
+ if (ds < 1024 || ds > int.MaxValue)
+ return "The blocksize must be at least 1KB";
+ }
+ catch
+ {
+ return "The blocksize value must be a valid size string";
+ }
+ }
+ else if (string.Equals(s.Name, "--prefix", StringComparison.OrdinalIgnoreCase))
+ {
+ if (!string.IsNullOrWhiteSpace(s.Value) && s.Value.Contains("-"))
+ return "The prefix cannot contain hyphens (-)";
+ }
+ else if (string.Equals(s.Name, "--gpg-encryption-command", StringComparison.OrdinalIgnoreCase)) {
+ gpgAsymmetricEncryption = string.Equals(s.Value, "--encrypt", StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
+ if (!disabled_encryption && !gpgAsymmetricEncryption && string.IsNullOrWhiteSpace(passphrase))
+ return "Missing passphrase";
+
+ if (schedule != null)
+ {
+ try
+ {
+ var ts = Library.Utility.Timeparser.ParseTimeSpan(schedule.Repeat);
+ if (ts <= TimeSpan.FromMinutes(5))
+ return "Schedule repetition time must be more than 5 minutes";
+ }
+ catch
+ {
+ return "Schedule repetition value must be a valid timespan";
+ }
+
+ }
+
+ return null;
+ }
+
+ internal void UpdateBackupDBPath(IBackup item, string path)
+ {
+ lock (m_lock)
+ {
+ using (var tr = m_connection.BeginTransaction())
+ {
+ using (var cmd = m_connection.CreateCommand())
+ {
+ cmd.Transaction = tr;
+ cmd.Parameters.Add(cmd.CreateParameter());
+ ((System.Data.IDbDataParameter) cmd.Parameters[0]).Value = path;
+ cmd.Parameters.Add(cmd.CreateParameter());
+ ((System.Data.IDbDataParameter) cmd.Parameters[1]).Value = item.ID;
+
+ cmd.CommandText = @"UPDATE ""Backup"" SET ""DBPath""=? WHERE ""ID""=?";
+ cmd.ExecuteNonQuery();
+ tr.Commit();
+ }
+ }
+ }
+
+ FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
+ }
+
+ private void AddOrUpdateBackup(IBackup item, bool updateSchedule, ISchedule schedule)
+ {
+ lock(m_lock)
+ {
+ bool update = item.ID != null;
+ if (!update && item.DBPath == null)
+ {
+ var folder = FIXMEGlobal.DataFolder;
+ if (!System.IO.Directory.Exists(folder))
+ System.IO.Directory.CreateDirectory(folder);
+
+ for(var i = 0; i < 100; i++)
+ {
+ var guess = System.IO.Path.Combine(folder, System.IO.Path.ChangeExtension(Duplicati.Library.Main.DatabaseLocator.GenerateRandomName(), ".sqlite"));
+ if (!System.IO.File.Exists(guess))
+ {
+ ((Backup)item).DBPath = guess;
+ break;
+ }
+ }
+
+ if (item.DBPath == null)
+ throw new Exception("Unable to generate a unique database file name");
+ }
+
+ using(var tr = m_connection.BeginTransaction())
+ {
+ OverwriteAndUpdateDb(
+ tr,
+ null,
+ new object[] { long.Parse(item.ID ?? "-1") },
+ new IBackup[] { item },
+ update ?
+ @"UPDATE ""Backup"" SET ""Name""=?, ""Description""=?, ""Tags""=?, ""TargetURL""=? WHERE ""ID""=?" :
+ @"INSERT INTO ""Backup"" (""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"") VALUES (?,?,?,?,?)",
+ (n) => {
+
+ if (n.TargetURL.IndexOf(Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER, StringComparison.Ordinal) >= 0)
+ throw new Exception("Attempted to save a backup with the password placeholder");
+ if (update && long.Parse(n.ID) <= 0)
+ throw new Exception("Invalid update, cannot update application settings through update method");
+
+ return new object[] {
+ n.Name,
+ n.Description ?? "" , // Description is optional but the column is set to NOT NULL, an additional check is welcome
+ string.Join(",", n.Tags ?? new string[0]),
+ n.TargetURL,
+ update ? item.ID : n.DBPath
+ };
+ });
+
+ if (!update)
+ using(var cmd = m_connection.CreateCommand())
+ {
+ cmd.Transaction = tr;
+ cmd.CommandText = @"SELECT last_insert_rowid();";
+ item.ID = ExecuteScalarInt64(cmd).ToString();
+ }
+
+ var id = long.Parse(item.ID);
+
+ if (long.Parse(item.ID) <= 0)
+ throw new Exception("Invalid addition, cannot update application settings through update method");
+
+ SetSources(item.Sources, id, tr);
+ SetSettings(item.Settings, id, tr);
+ SetFilters(item.Filters, id, tr);
+ SetMetadata(item.Metadata, id, tr);
+
+ if (updateSchedule)
+ {
+ var tags = new string[] { "ID=" + item.ID };
+ var existing = GetScheduleIDsFromTags(tags);
+ if (schedule == null && existing.Any())
+ DeleteFromDb("Schedule", existing.First(), tr);
+ else if (schedule != null)
+ {
+ if (existing.Any())
+ {
+ var cur = GetSchedule(existing.First());
+ cur.AllowedDays = schedule.AllowedDays;
+ cur.Repeat = schedule.Repeat;
+ cur.Tags = schedule.Tags;
+ cur.Time = schedule.Time;
+
+ schedule = cur;
+ }
+ else
+ {
+ schedule.ID = -1;
+ }
+
+ schedule.Tags = tags;
+ AddOrUpdateSchedule(schedule, tr);
+ }
+ }
+
+ tr.Commit();
+ FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
+ }
+ }
+ }
+
+ internal void AddOrUpdateSchedule(ISchedule item)
+ {
+ lock(m_lock)
+ using(var tr = m_connection.BeginTransaction())
+ {
+ AddOrUpdateSchedule(item, tr);
+ tr.Commit();
+ FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
+ }
+ }
+
+ private void AddOrUpdateSchedule(ISchedule item, System.Data.IDbTransaction tr)
+ {
+ lock(m_lock)
+ {
+ bool update = item.ID >= 0;
+ OverwriteAndUpdateDb(
+ tr,
+ null,
+ new object[] { item.ID },
+ new ISchedule[] { item },
+ update ?
+ @"UPDATE ""Schedule"" SET ""Tags""=?, ""Time""=?, ""Repeat""=?, ""LastRun""=?, ""Rule""=? WHERE ""ID""=?" :
+ @"INSERT INTO ""Schedule"" (""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"") VALUES (?,?,?,?,?)",
+ (n) => new object[] {
+ string.Join(",", n.Tags),
+ Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(n.Time),
+ n.Repeat,
+ Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(n.LastRun),
+ n.Rule ?? "",
+ update ? (object)item.ID : null
+ });
+
+ if (!update)
+ using(var cmd = m_connection.CreateCommand())
+ {
+ cmd.Transaction = tr;
+ cmd.CommandText = @"SELECT last_insert_rowid();";
+ item.ID = ExecuteScalarInt64(cmd);
+ }
+ }
+ }
+
+ public void DeleteBackup(long ID)
+ {
+ if (ID < 0)
+ return;
+
+ lock(m_lock)
+ {
+ using(var tr = m_connection.BeginTransaction())
+ {
+ var existing = GetScheduleIDsFromTags(new string[] { "ID=" + ID.ToString() });
+ if (existing.Any())
+ DeleteFromDb("Schedule", existing.First(), tr);
+
+ DeleteFromDb("ErrorLog", ID, "BackupID", tr);
+ DeleteFromDb("Filter", ID, "BackupID", tr);
+ DeleteFromDb("Log", ID, "BackupID", tr);
+ DeleteFromDb("Metadata", ID, "BackupID", tr);
+ DeleteFromDb("Option", ID, "BackupID", tr);
+ DeleteFromDb("Source", ID, "BackupID", tr);
+
+ DeleteFromDb("Backup", ID, tr);
+
+ tr.Commit();
+ }
+ }
+
+ FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
+ }
+
+ public void DeleteBackup(IBackup backup)
+ {
+ if (backup.IsTemporary)
+ UnregisterTemporaryBackup(backup);
+ else
+ DeleteBackup(long.Parse(backup.ID));
+ }
+
+ public void DeleteSchedule(long ID)
+ {
+ if (ID < 0)
+ return;
+
+ lock(m_lock)
+ DeleteFromDb("Schedule", ID);
+
+ FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
+ }
+
+ public void DeleteSchedule(ISchedule schedule)
+ {
+ DeleteSchedule(schedule.ID);
+ }
+
+ public IBackup[] Backups
+ {
+ get
+ {
+ lock(m_lock)
+ {
+ var lst = ReadFromDb(
+ (rd) => (IBackup)new Backup() {
+ ID = ConvertToInt64(rd, 0).ToString(),
+ Name = ConvertToString(rd, 1),
+ Description = ConvertToString(rd, 2),
+ Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+ TargetURL = ConvertToString(rd, 4),
+ DBPath = ConvertToString(rd, 5),
+ },
+ @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" ")
+ .ToArray();
+
+ foreach(var n in lst)
+ n.Metadata = GetMetadata(long.Parse(n.ID));
+
+
+ return lst;
+ }
+ }
+ }
+
+ public ISchedule[] Schedules
+ {
+ get
+ {
+ lock(m_lock)
+ return ReadFromDb(
+ (rd) => (ISchedule)new Schedule() {
+ ID = ConvertToInt64(rd, 0),
+ Tags = (ConvertToString(rd, 1) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
+ Time = ConvertToDateTime(rd, 2),
+ Repeat = ConvertToString(rd, 3),
+ LastRun = ConvertToDateTime(rd, 4),
+ Rule = ConvertToString(rd, 5),
+ },
+ @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" ")
+ .ToArray();
+ }
+ }
+
+
+ public IFilter[] Filters
+ {
+ get { return GetFilters(ANY_BACKUP_ID); }
+ set { SetFilters(value, ANY_BACKUP_ID); }
+ }
+
+ public ISetting[] Settings
+ {
+ get { return GetSettings(ANY_BACKUP_ID); }
+ set { SetSettings(value, ANY_BACKUP_ID); }
+ }
+
+ public INotification[] GetNotifications()
+ {
+ lock(m_lock)
+ return ReadFromDb(null).Cast().ToArray();
+ }
+
+ public bool DismissNotification(long id)
+ {
+ lock(m_lock)
+ {
+ var notifications = GetNotifications();
+ var cur = notifications.FirstOrDefault(x => x.ID == id);
+ if (cur == null)
+ return false;
+
+ DeleteFromDb(typeof(Notification).Name, id);
+ FIXMEGlobal.DataConnection.ApplicationSettings.UnackedError = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Error);
+ FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Warning);
+ }
+
+ FIXMEGlobal.IncrementLastNotificationUpdateID();
+ FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
+
+ return true;
+ }
+
+ public void RegisterNotification(Serialization.NotificationType type, string title, string message, Exception ex, string backupid, string action, string logid, string messageid, string logtag, Func conflicthandler)
+ {
+ lock(m_lock)
+ {
+ var notification = new Notification()
+ {
+ ID = -1,
+ Type = type,
+ Title = title,
+ Message = message,
+ Exception = ex == null ? "" : ex.ToString(),
+ BackupID = backupid,
+ Action = action ?? "",
+ Timestamp = DateTime.UtcNow,
+ LogEntryID = logid,
+ MessageID = messageid,
+ MessageLogTag = logtag
+ };
+
+ var conflictResult = conflicthandler(notification, GetNotifications());
+ if (conflictResult == null)
+ return;
+
+ if (conflictResult != notification)
+ DeleteFromDb(typeof(Notification).Name, conflictResult.ID);
+
+ OverwriteAndUpdateDb(null, null, null, new Notification[] { notification }, false);
+
+ if (type == Duplicati.Server.Serialization.NotificationType.Error)
+ FIXMEGlobal.DataConnection.ApplicationSettings.UnackedError = true;
+ else if (type == Duplicati.Server.Serialization.NotificationType.Warning)
+ FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning = true;
+ }
+
+ FIXMEGlobal.IncrementLastNotificationUpdateID();
+ FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
+ }
+
+ //Workaround to clean up the database after invalid settings update
+ public void FixInvalidBackupId()
+ {
+ using(var cmd = m_connection.CreateCommand())
+ using (var tr = m_connection.BeginTransaction())
+ {
+ cmd.Transaction = tr;
+ cmd.Parameters.Add(cmd.CreateParameter());
+ ((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = -1;
+ cmd.CommandText = @"DELETE FROM ""Option"" WHERE ""BackupID"" = ?";
+ cmd.ExecuteNonQuery();
+ cmd.CommandText = @"DELETE FROM ""Metadata"" WHERE ""BackupID"" = ?";
+ cmd.ExecuteNonQuery();
+ cmd.CommandText = @"DELETE FROM ""Filter"" WHERE ""BackupID"" = ?";
+ cmd.ExecuteNonQuery();
+ cmd.CommandText = @"DELETE FROM ""Source"" WHERE ""BackupID"" = ?";
+ cmd.ExecuteNonQuery();
+
+ cmd.Parameters.Clear();
+ cmd.Parameters.Add(cmd.CreateParameter());
+
+ ((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = "ID=-1";
+ cmd.CommandText = @"DELETE FROM ""Schedule"" WHERE ""Tags"" = ?";
+ cmd.ExecuteNonQuery();
+ tr.Commit();
+ }
+
+ ApplicationSettings.FixedInvalidBackupId = true;
+ }
+
+ public string[] GetUISettingsSchemes()
+ {
+ lock(m_lock)
+ return ReadFromDb(
+ (rd) => ConvertToString(rd, 0) ?? "",
+ @"SELECT DISTINCT ""Scheme"" FROM ""UIStorage""")
+ .ToArray();
+ }
+
+ public IDictionary GetUISettings(string scheme)
+ {
+ lock(m_lock)
+ return ReadFromDb(
+ (rd) => new KeyValuePair(
+ ConvertToString(rd, 0) ?? "",
+ ConvertToString(rd, 1) ?? ""
+ ),
+ @"SELECT ""Key"", ""Value"" FROM ""UIStorage"" WHERE ""Scheme"" = ?",
+ scheme)
+ .GroupBy(x => x.Key)
+ .ToDictionary(x => x.Key, x => x.Last().Value);
+ }
+
+ public void SetUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null)
+ {
+ lock(m_lock)
+ using(var tr = transaction == null ? m_connection.BeginTransaction() : null)
+ {
+ OverwriteAndUpdateDb(
+ tr,
+ @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = ?", new object[] { scheme },
+ values,
+ @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (?, ?, ?)",
+ (f) => {
+ return new object[] { scheme, f.Key ?? "", f.Value ?? "" };
+ }
+ );
+
+ if (tr != null)
+ tr.Commit();
+ }
+ }
+
+ public void UpdateUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null)
+ {
+ lock (m_lock)
+ using (var tr = transaction == null ? m_connection.BeginTransaction() : null)
+ {
+ OverwriteAndUpdateDb(
+ tr,
+ @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = ? AND ""Key"" IN (?)", new object[] { scheme, values.Keys },
+ values.Where(x => x.Value != null),
+ @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (?, ?, ?)",
+ (f) =>
+ {
+ return new object[] { scheme, f.Key ?? "", f.Value ?? "" };
+ }
+ );
+
+ if (tr != null)
+ tr.Commit();
+ }
+ }
+
+ public TempFile[] GetTempFiles()
+ {
+ lock(m_lock)
+ return ReadFromDb(null).ToArray();
+ }
+
+ public void DeleteTempFile(long id)
+ {
+ lock(m_lock)
+ DeleteFromDb(typeof(TempFile).Name, id);
+ }
+
+ public long RegisterTempFile(string origin, string path, DateTime expires)
+ {
+ var tempfile = new TempFile() {
+ Timestamp = DateTime.Now,
+ Origin = origin,
+ Path = path,
+ Expires = expires
+ };
+
+ OverwriteAndUpdateDb(null, null, null, new TempFile[] { tempfile }, false);
+
+ return tempfile.ID;
+ }
+
+ public void PurgeLogData(DateTime purgeDate)
+ {
+ var t = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(purgeDate);
+
+ using(var tr = m_connection.BeginTransaction())
+ using(var cmd = m_connection.CreateCommand())
+ {
+ cmd.Transaction = tr;
+ cmd.CommandText = @"DELETE FROM ""ErrorLog"" WHERE ""Timestamp"" < ?";
+ cmd.Parameters.Add(cmd.CreateParameter());
+ ((System.Data.IDataParameter)cmd.Parameters[0]).Value = t;
+ cmd.ExecuteNonQuery();
+
+ tr.Commit();
+ }
+ }
+
+ private static DateTime ConvertToDateTime(System.Data.IDataReader rd, int index)
+ {
+ var unixTime = ConvertToInt64(rd, index);
+ return unixTime == 0 ? new DateTime(0) : Library.Utility.Utility.EPOCH.AddSeconds(unixTime);
+ }
+
+ private static bool ConvertToBoolean(System.Data.IDataReader rd, int index)
+ {
+ return ConvertToInt64(rd, index) == 1;
+ }
+
+ private static string ConvertToString(System.Data.IDataReader rd, int index)
+ {
+ var r = rd.GetValue(index);
+ return r == null || r == DBNull.Value ? null : r.ToString();
+ }
+
+ private static long ConvertToInt64(System.Data.IDataReader rd, int index)
+ {
+ try
+ {
+ if (!rd.IsDBNull(index))
+ return rd.GetInt64(index);
+ }
+ catch
+ {
+ }
+
+ return -1;
+ }
+
+ private static long ExecuteScalarInt64(System.Data.IDbCommand cmd, long defaultValue = -1)
+ {
+ using(var rd = cmd.ExecuteReader())
+ return rd.Read() ? ConvertToInt64(rd, 0) : defaultValue;
+ }
+
+ private static string ExecuteScalarString(System.Data.IDbCommand cmd)
+ {
+ using(var rd = cmd.ExecuteReader())
+ return rd.Read() ? ConvertToString(rd, 0) : null;
+
+ }
+
+ private object ConvertToEnum(Type enumType, System.Data.IDataReader rd, int index, object @default)
+ {
+ try
+ {
+ return Enum.Parse(enumType, ConvertToString(rd, index));
+ }
+ catch
+ {
+ }
+
+ return @default;
+ }
+
+ // Overloaded function for legacy functionality
+ private bool DeleteFromDb(string tablename, long id, System.Data.IDbTransaction transaction = null)
+ {
+ return DeleteFromDb(tablename, id, "ID", transaction);
+ }
+
+ // New function that allows to delete rows from tables with arbitrary identifier values (e.g. ID or BackupID)
+ private bool DeleteFromDb(string tablename, long id, string identifier, System.Data.IDbTransaction transaction = null)
+ {
+ if (transaction == null)
+ {
+ using(var tr = m_connection.BeginTransaction())
+ {
+ var r = DeleteFromDb(tablename, id, tr);
+ tr.Commit();
+ return r;
+ }
+ }
+ else
+ {
+ using(var cmd = m_connection.CreateCommand())
+ {
+ cmd.Transaction = transaction;
+ cmd.CommandText = string.Format(@"DELETE FROM ""{0}"" WHERE ""{1}""=?", tablename, identifier);
+ var p = cmd.CreateParameter();
+ p.Value = id;
+ cmd.Parameters.Add(p);
+
+ var r = cmd.ExecuteNonQuery();
+ // Roll back the transaction if more than 1 ID was deleted. Multiple "BackupID" rows being deleted isn't a problem.
+ if (identifier == "ID" && r > 1)
+ throw new Exception(string.Format("Too many records attempted deleted from table {0} for id {1}: {2}", tablename, id, r));
+ return r == 1;
+ }
+ }
+ }
+
+ private static IEnumerable Read(System.Data.IDbCommand cmd, Func f)
+ {
+ using(var rd = cmd.ExecuteReader())
+ while(rd.Read())
+ yield return f(rd);
+ }
+
+ private static IEnumerable Read(System.Data.IDataReader rd, Func f)
+ {
+ while(rd.Read())
+ yield return f();
+ }
+
+ private System.Reflection.PropertyInfo[] GetORMFields()
+ {
+ var flags =
+ System.Reflection.BindingFlags.FlattenHierarchy |
+ System.Reflection.BindingFlags.Instance |
+ System.Reflection.BindingFlags.Public;
+
+ var supportedPropertyTypes = new Type[] {
+ typeof(long),
+ typeof(string),
+ typeof(bool),
+ typeof(DateTime)
+ };
+
+ return
+ (from n in typeof(T).GetProperties(flags)
+ where supportedPropertyTypes.Contains(n.PropertyType) || n.PropertyType.IsEnum
+ select n).ToArray();
+ }
+
+ private IEnumerable ReadFromDb(string whereclause, params object[] args)
+ {
+ var properties = GetORMFields();
+
+ var sql = string.Format(
+ @"SELECT ""{0}"" FROM ""{1}"" {2} {3}",
+ string.Join(@""", """, properties.Select(x => x.Name)),
+ typeof(T).Name,
+ string.IsNullOrWhiteSpace(whereclause) ? "" : " WHERE ",
+ whereclause ?? ""
+ );
+
+ return ReadFromDb((rd) => {
+ var item = Activator.CreateInstance();
+ for(var i = 0; i < properties.Length; i++)
+ {
+ var prop = properties[i];
+
+ if (prop.PropertyType.IsEnum)
+ prop.SetValue(item, ConvertToEnum(prop.PropertyType, rd, i, Enum.GetValues(prop.PropertyType).GetValue(0)), null);
+ else if (prop.PropertyType == typeof(string))
+ prop.SetValue(item, ConvertToString(rd, i), null);
+ else if (prop.PropertyType == typeof(long))
+ prop.SetValue(item, ConvertToInt64(rd, i), null);
+ else if (prop.PropertyType == typeof(bool))
+ prop.SetValue(item, ConvertToBoolean(rd, i), null);
+ else if (prop.PropertyType == typeof(DateTime))
+ prop.SetValue(item, ConvertToDateTime(rd, i), null);
+ }
+
+ return item;
+ }, sql, args);
+ }
+
+ private void OverwriteAndUpdateDb(System.Data.IDbTransaction transaction, string deleteSql, object[] deleteArgs, IEnumerable values, bool updateExisting)
+ {
+ var properties = GetORMFields();
+ var idfield = properties.FirstOrDefault(x => x.Name == "ID");
+ properties = properties.Where(x => x.Name != "ID").ToArray();
+
+ string sql;
+
+ if (updateExisting)
+ {
+ sql = string.Format(
+ @"UPDATE ""{0}"" SET {1} WHERE ""ID""=?",
+ typeof(T).Name,
+ string.Join(@", ", properties.Select(x => string.Format(@"""{0}""=?", x.Name)))
+ );
+
+ properties = properties.Union(new System.Reflection.PropertyInfo[] { idfield }).ToArray();
+ }
+ else
+ {
+
+ sql = string.Format(
+ @"INSERT INTO ""{0}"" (""{1}"") VALUES ({2})",
+ typeof(T).Name,
+ string.Join(@""", """, properties.Select(x => x.Name)),
+ string.Join(@", ", properties.Select(x => "?"))
+ );
+ }
+
+ OverwriteAndUpdateDb(transaction, deleteSql, deleteArgs, values, sql, (item) =>
+ {
+ return properties.Select((x) =>
+ {
+ var val = x.GetValue(item, null);
+
+ if (x.PropertyType.IsEnum)
+ val = val.ToString();
+ else if (x.PropertyType == typeof(DateTime))
+ val = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds((DateTime)val);
+
+ return val;
+ }).ToArray();
+ });
+
+ if (!updateExisting && values.Count() == 1 && idfield != null)
+ using(var cmd = m_connection.CreateCommand())
+ {
+ cmd.Transaction = transaction;
+ cmd.CommandText = @"SELECT last_insert_rowid();";
+ if (idfield.PropertyType == typeof(string))
+ idfield.SetValue(values.First(), ExecuteScalarString(cmd), null);
+ else
+ idfield.SetValue(values.First(), ExecuteScalarInt64(cmd), null);
+ }
+ }
+
+ private IEnumerable ReadFromDb(Func f, string sql, params object[] args)
+ {
+ using(var cmd = m_connection.CreateCommand())
+ {
+ cmd.CommandText = sql;
+ if (args != null)
+ foreach(var a in args)
+ {
+ var p = cmd.CreateParameter();
+ p.Value = a;
+ cmd.Parameters.Add(p);
+ }
+
+ return Read(cmd, f).ToArray();
+ }
+ }
+
+ private void OverwriteAndUpdateDb(System.Data.IDbTransaction transaction, string deleteSql, object[] deleteArgs, IEnumerable values, string insertSql, Func f)
+ {
+ using(var cmd = m_connection.CreateCommand())
+ {
+ cmd.Transaction = transaction;
+
+ if (!string.IsNullOrEmpty(deleteSql))
+ {
+ cmd.CommandText = deleteSql;
+ if (deleteArgs != null)
+ foreach(var a in deleteArgs)
+ {
+ var p = cmd.CreateParameter();
+ p.Value = a;
+ cmd.Parameters.Add(p);
+ }
+
+ cmd.ExecuteNonQuery();
+ cmd.Parameters.Clear();
+ }
+
+ cmd.CommandText = insertSql;
+
+ foreach(var n in values)
+ {
+ var r = f(n);
+ if (r == null)
+ continue;
+
+ while (cmd.Parameters.Count < r.Length)
+ cmd.Parameters.Add(cmd.CreateParameter());
+
+ for(var i = 0; i < r.Length; i++)
+ ((System.Data.IDbDataParameter)cmd.Parameters[i]).Value = r[i];
+
+ cmd.ExecuteNonQuery();
+ }
+ }
+ }
+
+ #region IDisposable implementation
+ public void Dispose()
+ {
+ if (m_errorcmd != null)
+ try { if (m_errorcmd != null) m_errorcmd.Dispose(); }
+ catch { }
+ finally { m_errorcmd = null; }
+
+
+ try
+ {
+ if (m_connection != null)
+ m_connection.Dispose();
+ }
+ catch
+ {
+ }
+ }
+ #endregion
+ }
+
+}
+
diff --git a/Duplicati/Server/Database/Database schema/1. Add Notifications.sql b/Duplicati.Library.RestAPI/Database/Database schema/1. Add Notifications.sql
similarity index 95%
rename from Duplicati/Server/Database/Database schema/1. Add Notifications.sql
rename to Duplicati.Library.RestAPI/Database/Database schema/1. Add Notifications.sql
index 578ef8ee8..9dc3c8b75 100644
--- a/Duplicati/Server/Database/Database schema/1. Add Notifications.sql
+++ b/Duplicati.Library.RestAPI/Database/Database schema/1. Add Notifications.sql
@@ -1,14 +1,14 @@
-/*
-Notifications not yet acknowledged by the user
-*/
-CREATE TABLE "Notification" (
- "ID" INTEGER PRIMARY KEY,
- "Type" TEXT NOT NULL,
- "Title" TEXT NOT NULL,
- "Message" TEXT NOT NULL,
- "Exception" TEXT NOT NULL,
- "BackupID" TEXT NULL,
- "Action" TEXT NOT NULL,
- "Timestamp" INTEGER NOT NULL
-);
-
+/*
+Notifications not yet acknowledged by the user
+*/
+CREATE TABLE "Notification" (
+ "ID" INTEGER PRIMARY KEY,
+ "Type" TEXT NOT NULL,
+ "Title" TEXT NOT NULL,
+ "Message" TEXT NOT NULL,
+ "Exception" TEXT NOT NULL,
+ "BackupID" TEXT NULL,
+ "Action" TEXT NOT NULL,
+ "Timestamp" INTEGER NOT NULL
+);
+
diff --git a/Duplicati/Server/Database/Database schema/2. Add UIStorage.sql b/Duplicati.Library.RestAPI/Database/Database schema/2. Add UIStorage.sql
similarity index 94%
rename from Duplicati/Server/Database/Database schema/2. Add UIStorage.sql
rename to Duplicati.Library.RestAPI/Database/Database schema/2. Add UIStorage.sql
index 2c33babc2..b0339749a 100644
--- a/Duplicati/Server/Database/Database schema/2. Add UIStorage.sql
+++ b/Duplicati.Library.RestAPI/Database/Database schema/2. Add UIStorage.sql
@@ -1,9 +1,9 @@
-/*
-Key/value storage for frontends
-*/
-CREATE TABLE "UIStorage" (
- "Scheme" TEXT NOT NULL,
- "Key" TEXT NOT NULL,
- "Value" TEXT NOT NULL
-);
-
+/*
+Key/value storage for frontends
+*/
+CREATE TABLE "UIStorage" (
+ "Scheme" TEXT NOT NULL,
+ "Key" TEXT NOT NULL,
+ "Value" TEXT NOT NULL
+);
+
diff --git a/Duplicati/Server/Database/Database schema/3. Add temp file storage.sql b/Duplicati.Library.RestAPI/Database/Database schema/3. Add temp file storage.sql
similarity index 95%
rename from Duplicati/Server/Database/Database schema/3. Add temp file storage.sql
rename to Duplicati.Library.RestAPI/Database/Database schema/3. Add temp file storage.sql
index 1f77cd181..67d71370a 100644
--- a/Duplicati/Server/Database/Database schema/3. Add temp file storage.sql
+++ b/Duplicati.Library.RestAPI/Database/Database schema/3. Add temp file storage.sql
@@ -1,11 +1,11 @@
-/*
-Long-term temporary file records
-*/
-CREATE TABLE "TempFile" (
- "ID" INTEGER PRIMARY KEY,
- "Origin" TEXT NOT NULL,
- "Path" TEXT NOT NULL,
- "Timestamp" INTEGER NOT NULL,
- "Expires" INTEGER NOT NULL
-);
-
+/*
+Long-term temporary file records
+*/
+CREATE TABLE "TempFile" (
+ "ID" INTEGER PRIMARY KEY,
+ "Origin" TEXT NOT NULL,
+ "Path" TEXT NOT NULL,
+ "Timestamp" INTEGER NOT NULL,
+ "Expires" INTEGER NOT NULL
+);
+
diff --git a/Duplicati/Server/Database/Database schema/4. Add autoincrement to backup id.sql b/Duplicati.Library.RestAPI/Database/Database schema/4. Add autoincrement to backup id.sql
similarity index 100%
rename from Duplicati/Server/Database/Database schema/4. Add autoincrement to backup id.sql
rename to Duplicati.Library.RestAPI/Database/Database schema/4. Add autoincrement to backup id.sql
diff --git a/Duplicati/Server/Database/Database schema/5. Extend notification table.sql b/Duplicati.Library.RestAPI/Database/Database schema/5. Extend notification table.sql
similarity index 100%
rename from Duplicati/Server/Database/Database schema/5. Extend notification table.sql
rename to Duplicati.Library.RestAPI/Database/Database schema/5. Extend notification table.sql
diff --git a/Duplicati/Server/Database/Database schema/6. Add Description to Backup.sql b/Duplicati.Library.RestAPI/Database/Database schema/6. Add Description to Backup.sql
similarity index 100%
rename from Duplicati/Server/Database/Database schema/6. Add Description to Backup.sql
rename to Duplicati.Library.RestAPI/Database/Database schema/6. Add Description to Backup.sql
diff --git a/Duplicati/Server/Database/Database schema/Schema.sql b/Duplicati.Library.RestAPI/Database/Database schema/Schema.sql
similarity index 96%
rename from Duplicati/Server/Database/Database schema/Schema.sql
rename to Duplicati.Library.RestAPI/Database/Database schema/Schema.sql
index 97c237ae2..b05876d22 100644
--- a/Duplicati/Server/Database/Database schema/Schema.sql
+++ b/Duplicati.Library.RestAPI/Database/Database schema/Schema.sql
@@ -1,158 +1,158 @@
-/*
- * The primary table that stores all backups.
- *
- * The name and tag are free form user strings.
- * The tags are comma separated
- * the TargetURL is the url to remote storage,
- * and the DBPath is the path to the local database
- */
-CREATE TABLE "Backup" (
- "ID" INTEGER PRIMARY KEY AUTOINCREMENT,
- "Name" TEXT NOT NULL,
- "Description" TEXT NOT NULL DEFAULT '',
- "Tags" TEXT NOT NULL,
- "TargetURL" TEXT NOT NULL,
- "DBPath" TEXT NOT NULL
-);
-
-/*
- * The table that stores all schedules
- *
- * Tags is a comma separated parsed field that indicates
- * which backups to run when activated.
- * special tags are ID:1 which means backup with ID = 1
- *
- * Time is the scheduled time, and lastRun is the last time the backup was executed
- *
- * Rule is a special parsed field
- */
-CREATE TABLE "Schedule" (
- "ID" INTEGER PRIMARY KEY,
- "Tags" TEXT NOT NULL,
- "Time" INTEGER NOT NULL,
- "Repeat" TEXT NOT NULL,
- "LastRun" INTEGER NOT NULL,
- "Rule" TEXT NOT NULL
-);
-
-/*
- * The source table is a list of source folders and files
- */
-CREATE TABLE "Source" (
- "BackupID" INTEGER NOT NULL,
- "Path" TEXT NOT NULL
-);
-
-/*
- * The filter table contains all filters associated with a backup.
- * The special backupID -1 means "applied to all backups"
- * The expression is the filter, if the filter is a regular
- * expression, it is surrounded by hard brackets [ ]
- */
-CREATE TABLE "Filter" (
- "BackupID" INTEGER NOT NULL,
- "Order" INTEGER NOT NULL,
- "Include" INTEGER NOT NULL,
- "Expression" TEXT NOT NULL
-);
-
-/*
- * All options are stored in this table
- *
- * The special backupID -1 means "applied to all backups".
- *
- * The filter is used to indicate what the option applies to,
- * for instance backend:s3 will only apply to backends of type S3
- *
- * The name and value are the option name and value
- */
-CREATE TABLE "Option" (
- "BackupID" INTEGER NOT NULL,
- "Filter" TEXT NOT NULL,
- "Name" TEXT NOT NULL,
- "Value" TEXT NOT NULL
-);
-
-/*
- * Recorded metadata about a backup
- * This table contains metadata, such as when the backup was last started,
- * how long it took, how many files there were, how big the backup set was,
- * how much data was uploaded, downloaded, how fast, how much space is left,
- * and similar data. Programs can use this information to improve the display,
- * but cannot count on these values being present
- */
-CREATE TABLE "Metadata" (
- "BackupID" INTEGER NOT NULL,
- "Name" TEXT NOT NULL,
- "Value" TEXT NOT NULL
-);
-
-/*
- * The log of operations initiated by the scheduler/user
- */
-CREATE TABLE "Log" (
- "BackupID" INTEGER NOT NULL,
- "Description" TEXT NOT NULL,
- "Start" INTEGER NOT NULL,
- "Finish" INTEGER NOT NULL,
- "Result" TEXT NOT NULL,
- "SuggestedIcon" TEXT NOT NULL
-);
-
-/*
- * The log of errors
- */
-CREATE TABLE "ErrorLog" (
- "BackupID" INTEGER,
- "Message" TEXT NOT NULL,
- "Exception" TEXT,
- "Timestamp" INTEGER NOT NULL
-);
-
-/*
-Internal version tracking
-*/
-CREATE TABLE "Version" (
- "ID" INTEGER PRIMARY KEY,
- "Version" INTEGER NOT NULL
-);
-
-/*
-Notifications not yet acknowledged by the user
-*/
-CREATE TABLE "Notification" (
- "ID" INTEGER PRIMARY KEY,
- "Type" TEXT NOT NULL,
- "Title" TEXT NOT NULL,
- "Message" TEXT NOT NULL,
- "Exception" TEXT NOT NULL,
- "BackupID" TEXT NULL,
- "Action" TEXT NOT NULL,
- "Timestamp" INTEGER NOT NULL,
- "LogEntryID" TEXT NULL,
- "MessageID" TEXT NULL,
- "MessageLogTag" TEXT NULL
-);
-
-/*
-Key/value storage for frontends
-*/
-CREATE TABLE "UIStorage" (
- "Scheme" TEXT NOT NULL,
- "Key" TEXT NOT NULL,
- "Value" TEXT NOT NULL
-);
-
-/*
-Long-term temporary file records
-*/
-CREATE TABLE "TempFile" (
- "ID" INTEGER PRIMARY KEY,
- "Origin" TEXT NOT NULL,
- "Path" TEXT NOT NULL,
- "Timestamp" INTEGER NOT NULL,
- "Expires" INTEGER NOT NULL
-);
-
-INSERT INTO "Version" ("Version") VALUES (6);
-
+/*
+ * The primary table that stores all backups.
+ *
+ * The name and tag are free form user strings.
+ * The tags are comma separated
+ * the TargetURL is the url to remote storage,
+ * and the DBPath is the path to the local database
+ */
+CREATE TABLE "Backup" (
+ "ID" INTEGER PRIMARY KEY AUTOINCREMENT,
+ "Name" TEXT NOT NULL,
+ "Description" TEXT NOT NULL DEFAULT '',
+ "Tags" TEXT NOT NULL,
+ "TargetURL" TEXT NOT NULL,
+ "DBPath" TEXT NOT NULL
+);
+
+/*
+ * The table that stores all schedules
+ *
+ * Tags is a comma separated parsed field that indicates
+ * which backups to run when activated.
+ * special tags are ID:1 which means backup with ID = 1
+ *
+ * Time is the scheduled time, and lastRun is the last time the backup was executed
+ *
+ * Rule is a special parsed field
+ */
+CREATE TABLE "Schedule" (
+ "ID" INTEGER PRIMARY KEY,
+ "Tags" TEXT NOT NULL,
+ "Time" INTEGER NOT NULL,
+ "Repeat" TEXT NOT NULL,
+ "LastRun" INTEGER NOT NULL,
+ "Rule" TEXT NOT NULL
+);
+
+/*
+ * The source table is a list of source folders and files
+ */
+CREATE TABLE "Source" (
+ "BackupID" INTEGER NOT NULL,
+ "Path" TEXT NOT NULL
+);
+
+/*
+ * The filter table contains all filters associated with a backup.
+ * The special backupID -1 means "applied to all backups"
+ * The expression is the filter, if the filter is a regular
+ * expression, it is surrounded by hard brackets [ ]
+ */
+CREATE TABLE "Filter" (
+ "BackupID" INTEGER NOT NULL,
+ "Order" INTEGER NOT NULL,
+ "Include" INTEGER NOT NULL,
+ "Expression" TEXT NOT NULL
+);
+
+/*
+ * All options are stored in this table
+ *
+ * The special backupID -1 means "applied to all backups".
+ *
+ * The filter is used to indicate what the option applies to,
+ * for instance backend:s3 will only apply to backends of type S3
+ *
+ * The name and value are the option name and value
+ */
+CREATE TABLE "Option" (
+ "BackupID" INTEGER NOT NULL,
+ "Filter" TEXT NOT NULL,
+ "Name" TEXT NOT NULL,
+ "Value" TEXT NOT NULL
+);
+
+/*
+ * Recorded metadata about a backup
+ * This table contains metadata, such as when the backup was last started,
+ * how long it took, how many files there were, how big the backup set was,
+ * how much data was uploaded, downloaded, how fast, how much space is left,
+ * and similar data. Programs can use this information to improve the display,
+ * but cannot count on these values being present
+ */
+CREATE TABLE "Metadata" (
+ "BackupID" INTEGER NOT NULL,
+ "Name" TEXT NOT NULL,
+ "Value" TEXT NOT NULL
+);
+
+/*
+ * The log of operations initiated by the scheduler/user
+ */
+CREATE TABLE "Log" (
+ "BackupID" INTEGER NOT NULL,
+ "Description" TEXT NOT NULL,
+ "Start" INTEGER NOT NULL,
+ "Finish" INTEGER NOT NULL,
+ "Result" TEXT NOT NULL,
+ "SuggestedIcon" TEXT NOT NULL
+);
+
+/*
+ * The log of errors
+ */
+CREATE TABLE "ErrorLog" (
+ "BackupID" INTEGER,
+ "Message" TEXT NOT NULL,
+ "Exception" TEXT,
+ "Timestamp" INTEGER NOT NULL
+);
+
+/*
+Internal version tracking
+*/
+CREATE TABLE "Version" (
+ "ID" INTEGER PRIMARY KEY,
+ "Version" INTEGER NOT NULL
+);
+
+/*
+Notifications not yet acknowledged by the user
+*/
+CREATE TABLE "Notification" (
+ "ID" INTEGER PRIMARY KEY,
+ "Type" TEXT NOT NULL,
+ "Title" TEXT NOT NULL,
+ "Message" TEXT NOT NULL,
+ "Exception" TEXT NOT NULL,
+ "BackupID" TEXT NULL,
+ "Action" TEXT NOT NULL,
+ "Timestamp" INTEGER NOT NULL,
+ "LogEntryID" TEXT NULL,
+ "MessageID" TEXT NULL,
+ "MessageLogTag" TEXT NULL
+);
+
+/*
+Key/value storage for frontends
+*/
+CREATE TABLE "UIStorage" (
+ "Scheme" TEXT NOT NULL,
+ "Key" TEXT NOT NULL,
+ "Value" TEXT NOT NULL
+);
+
+/*
+Long-term temporary file records
+*/
+CREATE TABLE "TempFile" (
+ "ID" INTEGER PRIMARY KEY,
+ "Origin" TEXT NOT NULL,
+ "Path" TEXT NOT NULL,
+ "Timestamp" INTEGER NOT NULL,
+ "Expires" INTEGER NOT NULL
+);
+
+INSERT INTO "Version" ("Version") VALUES (6);
+
diff --git a/Duplicati.Library.RestAPI/Database/DatabaseConnectionSchemaMarker.cs b/Duplicati.Library.RestAPI/Database/DatabaseConnectionSchemaMarker.cs
new file mode 100644
index 000000000..110f8477f
--- /dev/null
+++ b/Duplicati.Library.RestAPI/Database/DatabaseConnectionSchemaMarker.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Duplicati.Library.RestAPI.Database
+{
+ public static class DatabaseConnectionSchemaMarker
+ {
+ }
+}
diff --git a/Duplicati/Server/Database/Filter.cs b/Duplicati.Library.RestAPI/Database/Filter.cs
similarity index 100%
rename from Duplicati/Server/Database/Filter.cs
rename to Duplicati.Library.RestAPI/Database/Filter.cs
diff --git a/Duplicati/Server/Database/Notification.cs b/Duplicati.Library.RestAPI/Database/Notification.cs
similarity index 100%
rename from Duplicati/Server/Database/Notification.cs
rename to Duplicati.Library.RestAPI/Database/Notification.cs
diff --git a/Duplicati/Server/Database/Schedule.cs b/Duplicati.Library.RestAPI/Database/Schedule.cs
similarity index 100%
rename from Duplicati/Server/Database/Schedule.cs
rename to Duplicati.Library.RestAPI/Database/Schedule.cs
diff --git a/Duplicati/Server/Database/ServerSettings.cs b/Duplicati.Library.RestAPI/Database/ServerSettings.cs
similarity index 95%
rename from Duplicati/Server/Database/ServerSettings.cs
rename to Duplicati.Library.RestAPI/Database/ServerSettings.cs
index f39379867..92b02bcfa 100644
--- a/Duplicati/Server/Database/ServerSettings.cs
+++ b/Duplicati.Library.RestAPI/Database/ServerSettings.cs
@@ -1,208 +1,209 @@
-// 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.Collections.Generic;
-using System.Security.Cryptography;
-using System.Security.Cryptography.X509Certificates;
-using Duplicati.Library.Common;
+// Copyright (C) 2015, The Duplicati Team
-namespace Duplicati.Server.Database
-{
- public class ServerSettings
- {
- private static class CONST
- {
- public const string STARTUP_DELAY = "startup-delay";
- public const string DOWNLOAD_SPEED_LIMIT = "max-download-speed";
- public const string UPLOAD_SPEED_LIMIT = "max-upload-speed";
- public const string THREAD_PRIORITY = "thread-priority";
- public const string LAST_WEBSERVER_PORT = "last-webserver-port";
- public const string IS_FIRST_RUN = "is-first-run";
- 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";
- public const string UNACKED_ERROR = "unacked-error";
- public const string UNACKED_WARNING = "unacked-warning";
- public const string SERVER_LISTEN_INTERFACE = "server-listen-interface";
- public const string SERVER_SSL_CERTIFICATE = "server-ssl-certificate";
- public const string HAS_FIXED_INVALID_BACKUPID = "has-fixed-invalid-backup-id";
- public const string UPDATE_CHANNEL = "update-channel";
+// 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;
+
+namespace Duplicati.Server.Database
+{
+ public class ServerSettings
+ {
+ private static class CONST
+ {
+ public const string STARTUP_DELAY = "startup-delay";
+ public const string DOWNLOAD_SPEED_LIMIT = "max-download-speed";
+ public const string UPLOAD_SPEED_LIMIT = "max-upload-speed";
+ public const string THREAD_PRIORITY = "thread-priority";
+ public const string LAST_WEBSERVER_PORT = "last-webserver-port";
+ public const string IS_FIRST_RUN = "is-first-run";
+ 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";
+ public const string UNACKED_ERROR = "unacked-error";
+ public const string UNACKED_WARNING = "unacked-warning";
+ public const string SERVER_LISTEN_INTERFACE = "server-listen-interface";
+ public const string SERVER_SSL_CERTIFICATE = "server-ssl-certificate";
+ 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";
- }
-
- private readonly Dictionary settings;
- private readonly Connection databaseConnection;
- private Library.AutoUpdater.UpdateInfo m_latestUpdate;
-
- internal ServerSettings(Connection con)
- {
- settings = new Dictionary();
- databaseConnection = con;
- ReloadSettings();
- }
-
- public void ReloadSettings()
- {
- 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)))
- settings[n] = null;
- foreach(var n in databaseConnection.GetSettings(Connection.SERVER_SETTINGS_ID))
- settings[n.Name] = n.Value;
- }
- }
-
- public void UpdateSettings(Dictionary newsettings, bool clearExisting)
- {
- if (newsettings == null)
- throw new ArgumentNullException();
-
- lock(databaseConnection.m_lock)
- {
- m_latestUpdate = null;
- if (clearExisting)
- settings.Clear();
-
- 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];
-
- }
-
- 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() {
- Filter = "",
- Name = n.Key,
- Value = n.Value
- }, Database.Connection.SERVER_SETTINGS_ID);
-
- System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID);
- Program.StatusEventNotifyer.SignalNewEvent();
-
- // In case the usage reporter is enabled or disabled, refresh now
- Program.StartOrStopUsageReporter();
- // If throttle options were changed, update now
- Program.UpdateThrottleSpeeds();
- }
-
- public string StartupDelayDuration
- {
- get
- {
- return settings[CONST.STARTUP_DELAY];
- }
- set
- {
- lock(databaseConnection.m_lock)
- settings[CONST.STARTUP_DELAY] = value;
- SaveSettings();
- }
- }
-
- public System.Threading.ThreadPriority? ThreadPriorityOverride
- {
- get
- {
- var tp = settings[CONST.THREAD_PRIORITY];
- if (string.IsNullOrEmpty(tp))
- return null;
-
- System.Threading.ThreadPriority r;
- if (Enum.TryParse(tp, true, out r))
- return r;
-
- return null;
- }
- set
- {
- lock(databaseConnection.m_lock)
- settings[CONST.THREAD_PRIORITY] = value.HasValue ? Enum.GetName(typeof(System.Threading.ThreadPriority), value.Value) : null;
- }
- }
-
- public string DownloadSpeedLimit
- {
- get
- {
- return settings[CONST.DOWNLOAD_SPEED_LIMIT];
- }
- set
- {
- lock(databaseConnection.m_lock)
- settings[CONST.DOWNLOAD_SPEED_LIMIT] = value;
- SaveSettings();
- }
- }
-
- public string UploadSpeedLimit
- {
- get
- {
- return settings[CONST.UPLOAD_SPEED_LIMIT];
- }
- set
- {
- lock(databaseConnection.m_lock)
- settings[CONST.UPLOAD_SPEED_LIMIT] = value;
- SaveSettings();
- }
- }
-
- public bool IsFirstRun
- {
- get
- {
- 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 const string DISABLE_TRAY_ICON_LOGIN = "disable-tray-icon-login";
+ public const string SERVER_ALLOWED_HOSTNAMES = "allowed-hostnames";
+ }
+
+ private readonly Dictionary settings;
+ private readonly Connection databaseConnection;
+ private Library.AutoUpdater.UpdateInfo m_latestUpdate;
+
+ internal ServerSettings(Connection con)
+ {
+ settings = new Dictionary();
+ databaseConnection = con;
+ ReloadSettings();
+ }
+
+ public void ReloadSettings()
+ {
+ 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)))
+ settings[n] = null;
+ foreach(var n in databaseConnection.GetSettings(Connection.SERVER_SETTINGS_ID))
+ settings[n.Name] = n.Value;
+ }
+ }
+
+ public void UpdateSettings(Dictionary newsettings, bool clearExisting)
+ {
+ if (newsettings == null)
+ throw new ArgumentNullException();
+
+ lock(databaseConnection.m_lock)
+ {
+ m_latestUpdate = null;
+ if (clearExisting)
+ settings.Clear();
+
+ 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];
+
+ }
+
+ 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() {
+ Filter = "",
+ Name = n.Key,
+ Value = n.Value
+ }, Database.Connection.SERVER_SETTINGS_ID);
+
+ FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
+
+ // In case the usage reporter is enabled or disabled, refresh now
+ FIXMEGlobal.StartOrStopUsageReporter();
+ // If throttle options were changed, update now
+ FIXMEGlobal.UpdateThrottleSpeeds();
+ }
+
+ public string StartupDelayDuration
+ {
+ get
+ {
+ return settings[CONST.STARTUP_DELAY];
+ }
+ set
+ {
+ lock(databaseConnection.m_lock)
+ settings[CONST.STARTUP_DELAY] = value;
+ SaveSettings();
+ }
+ }
+
+ public System.Threading.ThreadPriority? ThreadPriorityOverride
+ {
+ get
+ {
+ var tp = settings[CONST.THREAD_PRIORITY];
+ if (string.IsNullOrEmpty(tp))
+ return null;
+
+ System.Threading.ThreadPriority r;
+ if (Enum.TryParse(tp, true, out r))
+ return r;
+
+ return null;
+ }
+ set
+ {
+ lock(databaseConnection.m_lock)
+ settings[CONST.THREAD_PRIORITY] = value.HasValue ? Enum.GetName(typeof(System.Threading.ThreadPriority), value.Value) : null;
+ }
+ }
+
+ public string DownloadSpeedLimit
+ {
+ get
+ {
+ return settings[CONST.DOWNLOAD_SPEED_LIMIT];
+ }
+ set
+ {
+ lock(databaseConnection.m_lock)
+ settings[CONST.DOWNLOAD_SPEED_LIMIT] = value;
+ SaveSettings();
+ }
+ }
+
+ public string UploadSpeedLimit
+ {
+ get
+ {
+ return settings[CONST.UPLOAD_SPEED_LIMIT];
+ }
+ set
+ {
+ lock(databaseConnection.m_lock)
+ settings[CONST.UPLOAD_SPEED_LIMIT] = value;
+ SaveSettings();
+ }
+ }
+
+ public bool IsFirstRun
+ {
+ get
+ {
+ 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);
+ {
+ return Duplicati.Library.Utility.Utility.ParseBoolOption(settings, CONST.HAS_ASKED_FOR_PASSWORD_PROTECTION);
}
set
{
@@ -212,352 +213,352 @@ namespace Duplicati.Server.Database
}
}
- public bool UnackedError
- {
- get
- {
- return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.UNACKED_ERROR], false);
- }
- set
- {
- lock(databaseConnection.m_lock)
- settings[CONST.UNACKED_ERROR] = value.ToString();
- SaveSettings();
- }
- }
-
- public bool UnackedWarning
- {
- get
- {
- return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.UNACKED_WARNING], false);
- }
- set
- {
- lock(databaseConnection.m_lock)
- settings[CONST.UNACKED_WARNING] = value.ToString();
- SaveSettings();
- }
- }
-
- public bool ServerPortChanged
- {
- get
- {
- return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.SERVER_PORT_CHANGED], false);
- }
- set
- {
- lock(databaseConnection.m_lock)
- settings[CONST.SERVER_PORT_CHANGED] = value.ToString();
- SaveSettings();
- }
- }
-
- public bool DisableTrayIconLogin
- {
- get
- {
- return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.DISABLE_TRAY_ICON_LOGIN], false);
- }
- set
- {
- lock (databaseConnection.m_lock)
- settings[CONST.DISABLE_TRAY_ICON_LOGIN] = value.ToString();
- SaveSettings();
- }
- }
-
- public int LastWebserverPort
- {
- get
- {
- var tp = settings[CONST.LAST_WEBSERVER_PORT];
- int p;
- if (string.IsNullOrEmpty(tp) || !int.TryParse(tp, out p))
- return -1;
-
- return p;
- }
- set
- {
- lock(databaseConnection.m_lock)
- settings[CONST.LAST_WEBSERVER_PORT] = value.ToString();
- SaveSettings();
- }
- }
-
- public string WebserverPassword
- {
- get
- {
- return settings[CONST.SERVER_PASSPHRASE];
- }
- }
-
- public string WebserverPasswordSalt
- {
- get
- {
- return settings[CONST.SERVER_PASSPHRASE_SALT];
- }
- }
-
- public void SetWebserverPassword(string password)
- {
- if (string.IsNullOrWhiteSpace(password))
- {
- 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;
- }
- }
-
- SaveSettings();
- }
-
- public void SetAllowedHostnames(string allowedHostnames)
- {
- lock (databaseConnection.m_lock)
- settings[CONST.SERVER_ALLOWED_HOSTNAMES] = allowedHostnames;
-
- 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()
- {
- var password = "";
- var pwd = "";
-
- if (!string.IsNullOrEmpty(settings[CONST.SERVER_PASSPHRASE]))
- {
- 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.SERVER_PASSPHRASETRAYICON] = password;
- settings[CONST.SERVER_PASSPHRASETRAYICONHASH] = pwd;
- }
-
- SaveSettings();
- }
-
- public DateTime LastUpdateCheck
- {
- get
- {
- long t;
- if (long.TryParse(settings[CONST.UPDATE_CHECK_LAST], out t))
- return new DateTime(t, DateTimeKind.Utc);
- else
- return new DateTime(0, DateTimeKind.Utc);
- }
- set
- {
- lock(databaseConnection.m_lock)
- settings[CONST.UPDATE_CHECK_LAST] = value.ToUniversalTime().Ticks.ToString();
- SaveSettings();
- }
- }
-
- public string UpdateCheckInterval
- {
- get
- {
- var tp = settings[CONST.UPDATE_CHECK_INTERVAL];
- if (string.IsNullOrWhiteSpace(tp))
- tp = "1W";
-
- return tp;
- }
- set
- {
- lock(databaseConnection.m_lock)
- settings[CONST.UPDATE_CHECK_INTERVAL] = value;
- SaveSettings();
- Program.UpdatePoller.Reschedule();
- }
- }
-
- public DateTime NextUpdateCheck
- {
- get
- {
- try
- {
- return Duplicati.Library.Utility.Timeparser.ParseTimeInterval(UpdateCheckInterval, LastUpdateCheck);
- }
- catch
- {
- return LastUpdateCheck.AddDays(7);
- }
- }
- }
-
- public Library.AutoUpdater.UpdateInfo UpdatedVersion
- {
- get
- {
- if (string.IsNullOrWhiteSpace(settings[CONST.UPDATE_CHECK_NEW_VERSION]))
- return null;
-
- try
- {
- if (m_latestUpdate != null)
- return m_latestUpdate;
-
- using(var tr = new System.IO.StringReader(settings[CONST.UPDATE_CHECK_NEW_VERSION]))
- return m_latestUpdate = Server.Serialization.Serializer.Deserialize(tr);
- }
- catch
- {
- }
-
- return null;
- }
- set
- {
- string result = null;
- if (value != null)
- {
- var sb = new System.Text.StringBuilder();
- using(var tw = new System.IO.StringWriter(sb))
- Server.Serialization.Serializer.SerializeJson(tw, value);
-
- result = sb.ToString();
- }
-
- m_latestUpdate = value;
- lock(databaseConnection.m_lock)
- settings[CONST.UPDATE_CHECK_NEW_VERSION] = result;
-
- SaveSettings();
- }
- }
-
- public string ServerListenInterface
- {
- get
- {
- return settings[CONST.SERVER_LISTEN_INTERFACE];
- }
- set
- {
- lock(databaseConnection.m_lock)
- settings[CONST.SERVER_LISTEN_INTERFACE] = value;
- SaveSettings();
- }
- }
-
- public X509Certificate2 ServerSSLCertificate
- {
- get
- {
- if (String.IsNullOrEmpty(settings[CONST.SERVER_SSL_CERTIFICATE]))
- return null;
-
- if (Platform.IsClientWindows)
- return new X509Certificate2(Convert.FromBase64String(settings[CONST.SERVER_SSL_CERTIFICATE]));
- else
- return new X509Certificate2(Convert.FromBase64String(settings[CONST.SERVER_SSL_CERTIFICATE]), "");
- }
- set
- {
- if (value == null)
- {
- lock (databaseConnection.m_lock)
- settings[CONST.SERVER_SSL_CERTIFICATE] = String.Empty;
- }
- else
- {
- if (Platform.IsClientWindows)
- lock (databaseConnection.m_lock)
- settings[CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(value.Export(X509ContentType.Pkcs12));
- else
- lock (databaseConnection.m_lock)
- settings[CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(value.Export(X509ContentType.Pkcs12, ""));
- }
- SaveSettings();
- }
- }
-
- public bool FixedInvalidBackupId
- {
- get
- {
- return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.HAS_FIXED_INVALID_BACKUPID], false);
- }
- set
- {
- lock(databaseConnection.m_lock)
- settings[CONST.HAS_FIXED_INVALID_BACKUPID] = value.ToString();
- SaveSettings();
- }
- }
-
- public string UpdateChannel
- {
- get
- {
- return settings[CONST.UPDATE_CHANNEL];
- }
- set
- {
- lock(databaseConnection.m_lock)
- settings[CONST.UPDATE_CHANNEL] = value;
- SaveSettings();
- }
- }
-
- public string UsageReporterLevel
- {
- get
- {
- return settings[CONST.USAGE_REPORTER_LEVEL];
- }
- set
- {
- lock(databaseConnection.m_lock)
- settings[CONST.USAGE_REPORTER_LEVEL] = value;
- SaveSettings();
- }
- }
- }
-}
-
+ public bool UnackedError
+ {
+ get
+ {
+ return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.UNACKED_ERROR], false);
+ }
+ set
+ {
+ lock(databaseConnection.m_lock)
+ settings[CONST.UNACKED_ERROR] = value.ToString();
+ SaveSettings();
+ }
+ }
+
+ public bool UnackedWarning
+ {
+ get
+ {
+ return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.UNACKED_WARNING], false);
+ }
+ set
+ {
+ lock(databaseConnection.m_lock)
+ settings[CONST.UNACKED_WARNING] = value.ToString();
+ SaveSettings();
+ }
+ }
+
+ public bool ServerPortChanged
+ {
+ get
+ {
+ return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.SERVER_PORT_CHANGED], false);
+ }
+ set
+ {
+ lock(databaseConnection.m_lock)
+ settings[CONST.SERVER_PORT_CHANGED] = value.ToString();
+ SaveSettings();
+ }
+ }
+
+ public bool DisableTrayIconLogin
+ {
+ get
+ {
+ return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.DISABLE_TRAY_ICON_LOGIN], false);
+ }
+ set
+ {
+ lock (databaseConnection.m_lock)
+ settings[CONST.DISABLE_TRAY_ICON_LOGIN] = value.ToString();
+ SaveSettings();
+ }
+ }
+
+ public int LastWebserverPort
+ {
+ get
+ {
+ var tp = settings[CONST.LAST_WEBSERVER_PORT];
+ int p;
+ if (string.IsNullOrEmpty(tp) || !int.TryParse(tp, out p))
+ return -1;
+
+ return p;
+ }
+ set
+ {
+ lock(databaseConnection.m_lock)
+ settings[CONST.LAST_WEBSERVER_PORT] = value.ToString();
+ SaveSettings();
+ }
+ }
+
+ public string WebserverPassword
+ {
+ get
+ {
+ return settings[CONST.SERVER_PASSPHRASE];
+ }
+ }
+
+ public string WebserverPasswordSalt
+ {
+ get
+ {
+ return settings[CONST.SERVER_PASSPHRASE_SALT];
+ }
+ }
+
+ public void SetWebserverPassword(string password)
+ {
+ if (string.IsNullOrWhiteSpace(password))
+ {
+ 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;
+ }
+ }
+
+ SaveSettings();
+ }
+
+ public void SetAllowedHostnames(string allowedHostnames)
+ {
+ lock (databaseConnection.m_lock)
+ settings[CONST.SERVER_ALLOWED_HOSTNAMES] = allowedHostnames;
+
+ 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()
+ {
+ var password = "";
+ var pwd = "";
+
+ if (!string.IsNullOrEmpty(settings[CONST.SERVER_PASSPHRASE]))
+ {
+ 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.SERVER_PASSPHRASETRAYICON] = password;
+ settings[CONST.SERVER_PASSPHRASETRAYICONHASH] = pwd;
+ }
+
+ SaveSettings();
+ }
+
+ public DateTime LastUpdateCheck
+ {
+ get
+ {
+ long t;
+ if (long.TryParse(settings[CONST.UPDATE_CHECK_LAST], out t))
+ return new DateTime(t, DateTimeKind.Utc);
+ else
+ return new DateTime(0, DateTimeKind.Utc);
+ }
+ set
+ {
+ lock(databaseConnection.m_lock)
+ settings[CONST.UPDATE_CHECK_LAST] = value.ToUniversalTime().Ticks.ToString();
+ SaveSettings();
+ }
+ }
+
+ public string UpdateCheckInterval
+ {
+ get
+ {
+ var tp = settings[CONST.UPDATE_CHECK_INTERVAL];
+ if (string.IsNullOrWhiteSpace(tp))
+ tp = "1W";
+
+ return tp;
+ }
+ set
+ {
+ lock(databaseConnection.m_lock)
+ settings[CONST.UPDATE_CHECK_INTERVAL] = value;
+ SaveSettings();
+ FIXMEGlobal.UpdatePoller.Reschedule();
+ }
+ }
+
+ public DateTime NextUpdateCheck
+ {
+ get
+ {
+ try
+ {
+ return Duplicati.Library.Utility.Timeparser.ParseTimeInterval(UpdateCheckInterval, LastUpdateCheck);
+ }
+ catch
+ {
+ return LastUpdateCheck.AddDays(7);
+ }
+ }
+ }
+
+ public Library.AutoUpdater.UpdateInfo UpdatedVersion
+ {
+ get
+ {
+ if (string.IsNullOrWhiteSpace(settings[CONST.UPDATE_CHECK_NEW_VERSION]))
+ return null;
+
+ try
+ {
+ if (m_latestUpdate != null)
+ return m_latestUpdate;
+
+ using(var tr = new System.IO.StringReader(settings[CONST.UPDATE_CHECK_NEW_VERSION]))
+ return m_latestUpdate = Server.Serialization.Serializer.Deserialize(tr);
+ }
+ catch
+ {
+ }
+
+ return null;
+ }
+ set
+ {
+ string result = null;
+ if (value != null)
+ {
+ var sb = new System.Text.StringBuilder();
+ using(var tw = new System.IO.StringWriter(sb))
+ Server.Serialization.Serializer.SerializeJson(tw, value);
+
+ result = sb.ToString();
+ }
+
+ m_latestUpdate = value;
+ lock(databaseConnection.m_lock)
+ settings[CONST.UPDATE_CHECK_NEW_VERSION] = result;
+
+ SaveSettings();
+ }
+ }
+
+ public string ServerListenInterface
+ {
+ get
+ {
+ return settings[CONST.SERVER_LISTEN_INTERFACE];
+ }
+ set
+ {
+ lock(databaseConnection.m_lock)
+ settings[CONST.SERVER_LISTEN_INTERFACE] = value;
+ SaveSettings();
+ }
+ }
+
+ public X509Certificate2 ServerSSLCertificate
+ {
+ get
+ {
+ if (String.IsNullOrEmpty(settings[CONST.SERVER_SSL_CERTIFICATE]))
+ return null;
+
+ if (Platform.IsClientWindows)
+ return new X509Certificate2(Convert.FromBase64String(settings[CONST.SERVER_SSL_CERTIFICATE]));
+ else
+ return new X509Certificate2(Convert.FromBase64String(settings[CONST.SERVER_SSL_CERTIFICATE]), "");
+ }
+ set
+ {
+ if (value == null)
+ {
+ lock (databaseConnection.m_lock)
+ settings[CONST.SERVER_SSL_CERTIFICATE] = String.Empty;
+ }
+ else
+ {
+ if (Platform.IsClientWindows)
+ lock (databaseConnection.m_lock)
+ settings[CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(value.Export(X509ContentType.Pkcs12));
+ else
+ lock (databaseConnection.m_lock)
+ settings[CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(value.Export(X509ContentType.Pkcs12, ""));
+ }
+ SaveSettings();
+ }
+ }
+
+ public bool FixedInvalidBackupId
+ {
+ get
+ {
+ return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.HAS_FIXED_INVALID_BACKUPID], false);
+ }
+ set
+ {
+ lock(databaseConnection.m_lock)
+ settings[CONST.HAS_FIXED_INVALID_BACKUPID] = value.ToString();
+ SaveSettings();
+ }
+ }
+
+ public string UpdateChannel
+ {
+ get
+ {
+ return settings[CONST.UPDATE_CHANNEL];
+ }
+ set
+ {
+ lock(databaseConnection.m_lock)
+ settings[CONST.UPDATE_CHANNEL] = value;
+ SaveSettings();
+ }
+ }
+
+ public string UsageReporterLevel
+ {
+ get
+ {
+ return settings[CONST.USAGE_REPORTER_LEVEL];
+ }
+ set
+ {
+ lock(databaseConnection.m_lock)
+ settings[CONST.USAGE_REPORTER_LEVEL] = value;
+ SaveSettings();
+ }
+ }
+ }
+}
+
diff --git a/Duplicati/Server/Database/Setting.cs b/Duplicati.Library.RestAPI/Database/Setting.cs
similarity index 100%
rename from Duplicati/Server/Database/Setting.cs
rename to Duplicati.Library.RestAPI/Database/Setting.cs
diff --git a/Duplicati/Server/Database/TempFile.cs b/Duplicati.Library.RestAPI/Database/TempFile.cs
similarity index 100%
rename from Duplicati/Server/Database/TempFile.cs
rename to Duplicati.Library.RestAPI/Database/TempFile.cs
diff --git a/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj b/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj
new file mode 100644
index 000000000..e10690aa5
--- /dev/null
+++ b/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net8.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\thirdparty\HttpServer\HttpServer.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Duplicati/Server/EventPollNotify.cs b/Duplicati.Library.RestAPI/EventPollNotify.cs
similarity index 97%
rename from Duplicati/Server/EventPollNotify.cs
rename to Duplicati.Library.RestAPI/EventPollNotify.cs
index 015e05af5..777dc4a1e 100644
--- a/Duplicati/Server/EventPollNotify.cs
+++ b/Duplicati.Library.RestAPI/EventPollNotify.cs
@@ -1,77 +1,77 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
-namespace Duplicati.Server
-{
- ///
- /// This class handles synchronized waiting for events
- ///
- public class EventPollNotify
- {
- ///
- /// The lock that grants exclusive access to control structures
- ///
- private readonly object m_lock = new object();
- ///
- /// The current eventID
- ///
- private long m_eventNo = 0;
- ///
- /// The list of subscribed waiting threads
- ///
- private readonly Queue m_waitQueue = new Queue();
-
- ///
- /// An eventhandler for subscribing to event updates without blocking
- ///
- public event EventHandler NewEvent;
-
- ///
- /// Gets the current event ID
- ///
- public long EventNo { get { return m_eventNo; } }
-
- ///
- /// Call to wait for an event that is newer than the current known event
- ///
- /// The last known event id
- /// The number of milliseconds to block
- /// The current event id
- public long Wait(long eventId, int milliseconds)
- {
- System.Threading.ManualResetEvent mre;
- lock (m_lock)
- {
- //If a newer event has already occured, return immediately
- if (eventId != m_eventNo)
- return m_eventNo;
-
- //Otherwise register this thread as waiting
- mre = new System.Threading.ManualResetEvent(false);
- m_waitQueue.Enqueue(mre);
- }
-
- //Wait until we are signalled or the time has elapsed
- mre.WaitOne(milliseconds, false);
- return m_eventNo;
- }
-
- ///
- /// Signals that an event has occurred and notifies all waiting threads
- ///
- public void SignalNewEvent()
- {
- lock (m_lock)
- {
- m_eventNo++;
- while (m_waitQueue.Count > 0)
- m_waitQueue.Dequeue().Set();
- }
-
- if (NewEvent != null)
- NewEvent(this, null);
- }
- }
-}
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Duplicati.Server
+{
+ ///
+ /// This class handles synchronized waiting for events
+ ///
+ public class EventPollNotify
+ {
+ ///
+ /// The lock that grants exclusive access to control structures
+ ///
+ private readonly object m_lock = new object();
+ ///
+ /// The current eventID
+ ///
+ private long m_eventNo = 0;
+ ///
+ /// The list of subscribed waiting threads
+ ///
+ private readonly Queue m_waitQueue = new Queue();
+
+ ///
+ /// An eventhandler for subscribing to event updates without blocking
+ ///
+ public event EventHandler NewEvent;
+
+ ///
+ /// Gets the current event ID
+ ///
+ public long EventNo { get { return m_eventNo; } }
+
+ ///
+ /// Call to wait for an event that is newer than the current known event
+ ///
+ /// The last known event id
+ /// The number of milliseconds to block
+ /// The current event id
+ public long Wait(long eventId, int milliseconds)
+ {
+ System.Threading.ManualResetEvent mre;
+ lock (m_lock)
+ {
+ //If a newer event has already occured, return immediately
+ if (eventId != m_eventNo)
+ return m_eventNo;
+
+ //Otherwise register this thread as waiting
+ mre = new System.Threading.ManualResetEvent(false);
+ m_waitQueue.Enqueue(mre);
+ }
+
+ //Wait until we are signalled or the time has elapsed
+ mre.WaitOne(milliseconds, false);
+ return m_eventNo;
+ }
+
+ ///
+ /// Signals that an event has occurred and notifies all waiting threads
+ ///
+ public void SignalNewEvent()
+ {
+ lock (m_lock)
+ {
+ m_eventNo++;
+ while (m_waitQueue.Count > 0)
+ m_waitQueue.Dequeue().Set();
+ }
+
+ if (NewEvent != null)
+ NewEvent(this, null);
+ }
+ }
+}
diff --git a/Duplicati.Library.RestAPI/FIXMEGlobal.cs b/Duplicati.Library.RestAPI/FIXMEGlobal.cs
new file mode 100644
index 000000000..2a8fc3148
--- /dev/null
+++ b/Duplicati.Library.RestAPI/FIXMEGlobal.cs
@@ -0,0 +1,97 @@
+
+using Duplicati.Server;
+using System;
+using System.Collections.Generic;
+
+namespace Duplicati.Library.RestAPI
+{
+ /**
+ * In the absense of dependancy injection, there is a significant amount of variables exposed through Program as globals.
+ * This causes a problem decoupling classes and leads to circular dependancies.
+ */
+ public static class FIXMEGlobal
+ {
+
+ ///
+ /// This is the only access to the database
+ ///
+ public static Server.Database.Connection DataConnection;
+
+ ///
+ /// The controller interface for pause/resume and throttle options
+ ///
+ public static LiveControls LiveControl;
+
+ ///
+ /// A delegate method for creating a copy of the current progress state
+ ///
+ public static Func GenerateProgressState;
+
+ ///
+ /// The status event signaler, used to control long polling of status updates
+ ///
+ public static readonly EventPollNotify StatusEventNotifyer = new EventPollNotify();
+
+ ///
+ /// This is the working thread
+ ///
+ public static Duplicati.Library.Utility.WorkerThread WorkThread;
+
+ public static Func PeekLastDataUpdateID;
+ public static Func PeekLastNotificationUpdateID;
+
+ public static Action IncrementLastDataUpdateID;
+
+ public static Action IncrementLastNotificationUpdateID;
+
+ public static Action StartOrStopUsageReporter;
+
+ public static Action UpdateThrottleSpeeds;
+
+ ///
+ /// Gets the folder where Duplicati data is stored
+ ///
+ public static string DataFolder;
+
+ ///
+ /// This is the scheduling thread
+ ///
+ public static Scheduler Scheduler;
+
+ ///
+ /// The log redirect handler
+ ///
+ public static readonly LogWriteHandler LogHandler = new LogWriteHandler();
+
+ public static Func, Server.Database.Connection> GetDatabaseConnection;
+
+ ///
+ /// The update poll thread.
+ ///
+ public static UpdatePollThread UpdatePoller;
+
+
+ ///
+ /// Used to check the origin of the web server (e.g. Tray icon or a stand alone Server)
+ ///
+ public static string Origin = "Server";
+
+
+ ///
+ /// The application exit event
+ ///
+ public static System.Threading.ManualResetEvent ApplicationExitEvent;
+
+
+ ///
+ /// List of completed task results
+ ///
+ public static readonly List> TaskResultCache = new List>();
+
+
+ ///
+ /// This is the lock to be used before manipulating the shared resources
+ ///
+ public static readonly object MainLock = new object();
+ }
+}
diff --git a/Duplicati/Server/LiveControls.cs b/Duplicati.Library.RestAPI/LiveControls.cs
similarity index 96%
rename from Duplicati/Server/LiveControls.cs
rename to Duplicati.Library.RestAPI/LiveControls.cs
index 5c9252e15..1a51636a3 100644
--- a/Duplicati/Server/LiveControls.cs
+++ b/Duplicati.Library.RestAPI/LiveControls.cs
@@ -1,403 +1,404 @@
-#region Disclaimer / License
-// 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-//
-#endregion
-using System;
-using System.Collections.Generic;
-using System.Text;
+#region Disclaimer / License
+// 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+//
+#endregion
+using System;
+using System.Collections.Generic;
+using System.Text;
using Duplicati.Library.Common;
+using Duplicati.Library.RestAPI;
-namespace Duplicati.Server
-{
- ///
- /// This class keeps track of the users modifications regarding
- /// throttling and pause/resume
- ///
- public class LiveControls
- {
- ///
- /// The tag used for logging
- ///
- private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType();
-
- ///
- /// An event that is activated when the pause state changes
- ///
- public event EventHandler StateChanged;
-
- ///
- /// An event that is activated when the thread priority changes
- ///
- public event EventHandler ThreadPriorityChanged;
-
- ///
- /// An event that is activated when the throttle speed changes
- ///
- public event EventHandler ThrottleSpeedChanged;
-
- ///
- /// The possible states for the live control
- ///
- public enum LiveControlState
- {
- ///
- /// Indicates that the backups are running
- ///
- Running,
- ///
- /// Indicates that the backups are currently suspended
- ///
- Paused
- }
-
- ///
- /// The current control state
- ///
- private LiveControlState m_state;
-
- ///
- /// A value that indicates if the current pause state is caused by being suspended
- ///
- private bool m_pausedForSuspend = false;
-
- ///
- /// The time to pause for, used to ensure that a user set pause can override the suspend pause
- ///
- private DateTime m_suspendMinimumPause = new DateTime(0);
-
- ///
- /// Gets the current state for the control
- ///
- public LiveControlState State { get { return m_state; } }
-
- ///
- /// The internal variable that tracks the the priority
- ///
- private System.Threading.ThreadPriority? m_priority;
-
- ///
- /// The internal variable that tracks the upload limit
- ///
- private long? m_uploadLimit;
-
- ///
- /// The internal variable that tracks the download limit
- ///
- private long? m_downloadLimit;
-
- ///
- /// The object that ensures concurrent operations
- ///
- private readonly object m_lock = new object();
-
- ///
- /// Gets the current overridden thread priority
- ///
- public System.Threading.ThreadPriority? ThreadPriority
- {
- get { return m_priority; }
- set
- {
- if (m_priority != value)
- {
- m_priority = value;
- if (ThreadPriorityChanged != null)
- ThreadPriorityChanged(this, null);
- }
- }
- }
-
- ///
- /// Gets the current upload limit in bps
- ///
- public long? UploadLimit
- {
- get { return m_uploadLimit; }
- set
- {
- if (m_uploadLimit != value)
- {
- m_uploadLimit = value;
- if (ThrottleSpeedChanged != null)
- ThrottleSpeedChanged(this, null);
- }
- }
- }
-
- ///
- /// Gets the download limit in bps
- ///
- public long? DownloadLimit
- {
- get { return m_downloadLimit; }
- set
- {
- if (m_downloadLimit != value)
- {
- m_downloadLimit = value;
- if (ThrottleSpeedChanged != null)
- ThrottleSpeedChanged(this, null);
- }
- }
- }
-
- ///
- /// The timer that is activated after a pause period.
- ///
- private readonly System.Threading.Timer m_waitTimer;
-
- ///
- /// The time that the current pause is expected to expire
- ///
- private DateTime m_waitTimeExpiration = new DateTime(0);
-
- ///
- /// Constructs a new instance of the LiveControl
- ///
- public LiveControls(Database.ServerSettings settings)
- {
- m_state = LiveControlState.Running;
- m_waitTimer = new System.Threading.Timer(m_waitTimer_Tick, this, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
-
- if (!string.IsNullOrEmpty(settings.StartupDelayDuration) && settings.StartupDelayDuration != "0")
- {
- long milliseconds = 0;
- try { milliseconds = (long)Duplicati.Library.Utility.Timeparser.ParseTimeSpan(settings.StartupDelayDuration).TotalMilliseconds; }
- catch {}
-
- if (milliseconds > 0)
- {
- m_waitTimeExpiration = DateTime.Now.AddMilliseconds(milliseconds);
- m_waitTimer.Change(milliseconds, System.Threading.Timeout.Infinite);
- m_state = LiveControlState.Paused;
- }
- }
-
- m_priority = settings.ThreadPriorityOverride;
- if (!string.IsNullOrEmpty(settings.DownloadSpeedLimit))
- try
- {
- m_downloadLimit = Library.Utility.Sizeparser.ParseSize(settings.DownloadSpeedLimit, "kb");
- }
- catch (Exception ex)
- {
- Library.Logging.Log.WriteErrorMessage(LOGTAG, "ParseDownloadLimitError", ex, "Failed to parse download limit: {0}", settings.DownloadSpeedLimit);
- }
-
- if (!string.IsNullOrEmpty(settings.UploadSpeedLimit))
- try
- {
- m_uploadLimit = Library.Utility.Sizeparser.ParseSize(settings.UploadSpeedLimit, "kb");
- }
- catch (Exception ex)
- {
- Library.Logging.Log.WriteErrorMessage(LOGTAG, "ParseUploadLimitError", ex, "Failed to parse upload limit: {0}", settings.UploadSpeedLimit);
- }
-
- try
- {
- if (!Platform.IsClientPosix)
- RegisterHibernateMonitor();
- }
- catch { }
- }
-
- ///
- /// Event that occurs when the timeout duration is exceeded
- ///
- /// The sender of the event
- private void m_waitTimer_Tick(object sender)
- {
- lock (m_lock)
- Resume();
- }
-
- ///
- /// Internal helper to reset the timeout timer
- ///
- /// The time to wait
- private void ResetTimer(string timeout)
- {
- lock (m_lock)
- if (!string.IsNullOrEmpty(timeout))
- {
- long milliseconds = (long)Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout).TotalMilliseconds;
- m_waitTimeExpiration = DateTime.Now.AddMilliseconds(milliseconds);
- m_waitTimer.Change(milliseconds, System.Threading.Timeout.Infinite);
- }
- else
- {
- m_waitTimeExpiration = new DateTime(0);
- m_waitTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
- }
- }
-
- ///
- /// Internal helper to set the pause mode
- ///
- private void SetPauseMode()
- {
- lock (m_lock)
- {
- if (m_state == LiveControlState.Running)
- {
- m_state = LiveControlState.Paused;
- if (StateChanged != null)
- StateChanged(this, null);
- }
- }
- }
-
- ///
- /// Pauses the backups until resumed
- ///
- public void Pause()
- {
- lock(m_lock)
- {
- var fireEvent = m_waitTimeExpiration.Ticks != 0 && m_state == LiveControlState.Paused && StateChanged != null;
-
- ResetTimer(null);
-
- if (fireEvent)
- StateChanged(this, null);
- else
- SetPauseMode();
- }
- }
-
- ///
- /// Resumes a backups to the running state
- ///
- public void Resume()
- {
- lock (m_lock)
- {
- if (m_state == LiveControlState.Paused)
- {
- //Make sure that the timer is cleared
- ResetTimer(null);
-
- m_state = LiveControlState.Running;
- if (StateChanged != null)
- StateChanged(this, null);
- }
- }
- }
-
- ///
- /// Suspends the backups for a given period
- ///
- /// The duration to wait
- public void Pause(string timeout)
- {
- Pause(Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout));
- }
-
- ///
- /// Suspends the backups for a given period
- ///
- /// The duration to wait
- public void Pause(TimeSpan timeout)
- {
- lock (m_lock)
- {
- m_waitTimeExpiration = DateTime.Now.AddMilliseconds((long)timeout.TotalMilliseconds);
- m_waitTimer.Change((long)timeout.TotalMilliseconds, System.Threading.Timeout.Infinite);
-
- //We change the time, so we issue a new event
- if (m_state == LiveControlState.Paused && StateChanged != null)
- StateChanged(this, null);
- else
- SetPauseMode();
- }
- }
-
- ///
- /// Gets the time the current pause is expected to end
- ///
- public DateTime EstimatedPauseEnd { get { return m_waitTimeExpiration; } }
-
- ///
- /// Method for calling a Win32 API
- ///
- private void RegisterHibernateMonitor()
- {
- Microsoft.Win32.SystemEvents.PowerModeChanged += new Microsoft.Win32.PowerModeChangedEventHandler(SystemEvents_PowerModeChanged);
- }
-
- ///
- /// A monitor for detecting when the system hibernates or resumes
- ///
- /// Unused sender parameter
- /// The event information
- private void SystemEvents_PowerModeChanged(object sender, object _e)
- {
- Microsoft.Win32.PowerModeChangedEventArgs e = _e as Microsoft.Win32.PowerModeChangedEventArgs;
- if (e == null)
- return;
-
- if (e.Mode == Microsoft.Win32.PowerModes.Suspend)
- {
- //If we are running, register as being paused due to suspending
- if (this.m_state == LiveControlState.Running)
- {
- this.SetPauseMode();
- m_pausedForSuspend = true;
- m_suspendMinimumPause = new DateTime(0);
- }
- else
- {
- if (m_waitTimeExpiration.Ticks != 0)
- {
- m_pausedForSuspend = true;
- m_suspendMinimumPause = this.EstimatedPauseEnd;
- ResetTimer(null);
- }
-
- }
- }
- else if (e.Mode == Microsoft.Win32.PowerModes.Resume)
- {
- //If we have been been paused due to suspending, we un-pause now
- if (m_pausedForSuspend)
- {
- long delayTicks = (m_suspendMinimumPause - DateTime.Now).Ticks;
-
- var appset = Program.DataConnection.ApplicationSettings;
- if (!string.IsNullOrEmpty(appset.StartupDelayDuration) && appset.StartupDelayDuration != "0")
- try { delayTicks = Math.Max(delayTicks, Library.Utility.Timeparser.ParseTimeSpan(appset.StartupDelayDuration).Ticks); }
- catch { }
-
- if (delayTicks > 0)
- {
- this.Pause(TimeSpan.FromTicks(delayTicks));
- }
- else
- {
- this.Resume();
- }
- }
-
- m_pausedForSuspend = false;
- m_suspendMinimumPause = new DateTime(0);
- }
- }
-
- }
-}
+namespace Duplicati.Server
+{
+ ///
+ /// This class keeps track of the users modifications regarding
+ /// throttling and pause/resume
+ ///
+ public class LiveControls
+ {
+ ///
+ /// The tag used for logging
+ ///
+ private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType();
+
+ ///
+ /// An event that is activated when the pause state changes
+ ///
+ public event EventHandler StateChanged;
+
+ ///
+ /// An event that is activated when the thread priority changes
+ ///
+ public event EventHandler ThreadPriorityChanged;
+
+ ///
+ /// An event that is activated when the throttle speed changes
+ ///
+ public event EventHandler ThrottleSpeedChanged;
+
+ ///
+ /// The possible states for the live control
+ ///
+ public enum LiveControlState
+ {
+ ///
+ /// Indicates that the backups are running
+ ///
+ Running,
+ ///
+ /// Indicates that the backups are currently suspended
+ ///
+ Paused
+ }
+
+ ///
+ /// The current control state
+ ///
+ private LiveControlState m_state;
+
+ ///
+ /// A value that indicates if the current pause state is caused by being suspended
+ ///
+ private bool m_pausedForSuspend = false;
+
+ ///
+ /// The time to pause for, used to ensure that a user set pause can override the suspend pause
+ ///
+ private DateTime m_suspendMinimumPause = new DateTime(0);
+
+ ///
+ /// Gets the current state for the control
+ ///
+ public LiveControlState State { get { return m_state; } }
+
+ ///
+ /// The internal variable that tracks the the priority
+ ///
+ private System.Threading.ThreadPriority? m_priority;
+
+ ///
+ /// The internal variable that tracks the upload limit
+ ///
+ private long? m_uploadLimit;
+
+ ///
+ /// The internal variable that tracks the download limit
+ ///
+ private long? m_downloadLimit;
+
+ ///
+ /// The object that ensures concurrent operations
+ ///
+ private readonly object m_lock = new object();
+
+ ///
+ /// Gets the current overridden thread priority
+ ///
+ public System.Threading.ThreadPriority? ThreadPriority
+ {
+ get { return m_priority; }
+ set
+ {
+ if (m_priority != value)
+ {
+ m_priority = value;
+ if (ThreadPriorityChanged != null)
+ ThreadPriorityChanged(this, null);
+ }
+ }
+ }
+
+ ///
+ /// Gets the current upload limit in bps
+ ///
+ public long? UploadLimit
+ {
+ get { return m_uploadLimit; }
+ set
+ {
+ if (m_uploadLimit != value)
+ {
+ m_uploadLimit = value;
+ if (ThrottleSpeedChanged != null)
+ ThrottleSpeedChanged(this, null);
+ }
+ }
+ }
+
+ ///
+ /// Gets the download limit in bps
+ ///
+ public long? DownloadLimit
+ {
+ get { return m_downloadLimit; }
+ set
+ {
+ if (m_downloadLimit != value)
+ {
+ m_downloadLimit = value;
+ if (ThrottleSpeedChanged != null)
+ ThrottleSpeedChanged(this, null);
+ }
+ }
+ }
+
+ ///
+ /// The timer that is activated after a pause period.
+ ///
+ private readonly System.Threading.Timer m_waitTimer;
+
+ ///
+ /// The time that the current pause is expected to expire
+ ///
+ private DateTime m_waitTimeExpiration = new DateTime(0);
+
+ ///
+ /// Constructs a new instance of the LiveControl
+ ///
+ public LiveControls(Database.ServerSettings settings)
+ {
+ m_state = LiveControlState.Running;
+ m_waitTimer = new System.Threading.Timer(m_waitTimer_Tick, this, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
+
+ if (!string.IsNullOrEmpty(settings.StartupDelayDuration) && settings.StartupDelayDuration != "0")
+ {
+ long milliseconds = 0;
+ try { milliseconds = (long)Duplicati.Library.Utility.Timeparser.ParseTimeSpan(settings.StartupDelayDuration).TotalMilliseconds; }
+ catch {}
+
+ if (milliseconds > 0)
+ {
+ m_waitTimeExpiration = DateTime.Now.AddMilliseconds(milliseconds);
+ m_waitTimer.Change(milliseconds, System.Threading.Timeout.Infinite);
+ m_state = LiveControlState.Paused;
+ }
+ }
+
+ m_priority = settings.ThreadPriorityOverride;
+ if (!string.IsNullOrEmpty(settings.DownloadSpeedLimit))
+ try
+ {
+ m_downloadLimit = Library.Utility.Sizeparser.ParseSize(settings.DownloadSpeedLimit, "kb");
+ }
+ catch (Exception ex)
+ {
+ Library.Logging.Log.WriteErrorMessage(LOGTAG, "ParseDownloadLimitError", ex, "Failed to parse download limit: {0}", settings.DownloadSpeedLimit);
+ }
+
+ if (!string.IsNullOrEmpty(settings.UploadSpeedLimit))
+ try
+ {
+ m_uploadLimit = Library.Utility.Sizeparser.ParseSize(settings.UploadSpeedLimit, "kb");
+ }
+ catch (Exception ex)
+ {
+ Library.Logging.Log.WriteErrorMessage(LOGTAG, "ParseUploadLimitError", ex, "Failed to parse upload limit: {0}", settings.UploadSpeedLimit);
+ }
+
+ try
+ {
+ if (!Platform.IsClientPosix)
+ RegisterHibernateMonitor();
+ }
+ catch { }
+ }
+
+ ///
+ /// Event that occurs when the timeout duration is exceeded
+ ///
+ /// The sender of the event
+ private void m_waitTimer_Tick(object sender)
+ {
+ lock (m_lock)
+ Resume();
+ }
+
+ ///
+ /// Internal helper to reset the timeout timer
+ ///
+ /// The time to wait
+ private void ResetTimer(string timeout)
+ {
+ lock (m_lock)
+ if (!string.IsNullOrEmpty(timeout))
+ {
+ long milliseconds = (long)Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout).TotalMilliseconds;
+ m_waitTimeExpiration = DateTime.Now.AddMilliseconds(milliseconds);
+ m_waitTimer.Change(milliseconds, System.Threading.Timeout.Infinite);
+ }
+ else
+ {
+ m_waitTimeExpiration = new DateTime(0);
+ m_waitTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
+ }
+ }
+
+ ///
+ /// Internal helper to set the pause mode
+ ///
+ private void SetPauseMode()
+ {
+ lock (m_lock)
+ {
+ if (m_state == LiveControlState.Running)
+ {
+ m_state = LiveControlState.Paused;
+ if (StateChanged != null)
+ StateChanged(this, null);
+ }
+ }
+ }
+
+ ///
+ /// Pauses the backups until resumed
+ ///
+ public void Pause()
+ {
+ lock(m_lock)
+ {
+ var fireEvent = m_waitTimeExpiration.Ticks != 0 && m_state == LiveControlState.Paused && StateChanged != null;
+
+ ResetTimer(null);
+
+ if (fireEvent)
+ StateChanged(this, null);
+ else
+ SetPauseMode();
+ }
+ }
+
+ ///
+ /// Resumes a backups to the running state
+ ///
+ public void Resume()
+ {
+ lock (m_lock)
+ {
+ if (m_state == LiveControlState.Paused)
+ {
+ //Make sure that the timer is cleared
+ ResetTimer(null);
+
+ m_state = LiveControlState.Running;
+ if (StateChanged != null)
+ StateChanged(this, null);
+ }
+ }
+ }
+
+ ///
+ /// Suspends the backups for a given period
+ ///
+ /// The duration to wait
+ public void Pause(string timeout)
+ {
+ Pause(Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout));
+ }
+
+ ///
+ /// Suspends the backups for a given period
+ ///
+ /// The duration to wait
+ public void Pause(TimeSpan timeout)
+ {
+ lock (m_lock)
+ {
+ m_waitTimeExpiration = DateTime.Now.AddMilliseconds((long)timeout.TotalMilliseconds);
+ m_waitTimer.Change((long)timeout.TotalMilliseconds, System.Threading.Timeout.Infinite);
+
+ //We change the time, so we issue a new event
+ if (m_state == LiveControlState.Paused && StateChanged != null)
+ StateChanged(this, null);
+ else
+ SetPauseMode();
+ }
+ }
+
+ ///
+ /// Gets the time the current pause is expected to end
+ ///
+ public DateTime EstimatedPauseEnd { get { return m_waitTimeExpiration; } }
+
+ ///
+ /// Method for calling a Win32 API
+ ///
+ private void RegisterHibernateMonitor()
+ {
+ Microsoft.Win32.SystemEvents.PowerModeChanged += new Microsoft.Win32.PowerModeChangedEventHandler(SystemEvents_PowerModeChanged);
+ }
+
+ ///
+ /// A monitor for detecting when the system hibernates or resumes
+ ///
+ /// Unused sender parameter
+ /// The event information
+ private void SystemEvents_PowerModeChanged(object sender, object _e)
+ {
+ Microsoft.Win32.PowerModeChangedEventArgs e = _e as Microsoft.Win32.PowerModeChangedEventArgs;
+ if (e == null)
+ return;
+
+ if (e.Mode == Microsoft.Win32.PowerModes.Suspend)
+ {
+ //If we are running, register as being paused due to suspending
+ if (this.m_state == LiveControlState.Running)
+ {
+ this.SetPauseMode();
+ m_pausedForSuspend = true;
+ m_suspendMinimumPause = new DateTime(0);
+ }
+ else
+ {
+ if (m_waitTimeExpiration.Ticks != 0)
+ {
+ m_pausedForSuspend = true;
+ m_suspendMinimumPause = this.EstimatedPauseEnd;
+ ResetTimer(null);
+ }
+
+ }
+ }
+ else if (e.Mode == Microsoft.Win32.PowerModes.Resume)
+ {
+ //If we have been been paused due to suspending, we un-pause now
+ if (m_pausedForSuspend)
+ {
+ long delayTicks = (m_suspendMinimumPause - DateTime.Now).Ticks;
+
+ var appset = FIXMEGlobal.DataConnection.ApplicationSettings;
+ if (!string.IsNullOrEmpty(appset.StartupDelayDuration) && appset.StartupDelayDuration != "0")
+ try { delayTicks = Math.Max(delayTicks, Library.Utility.Timeparser.ParseTimeSpan(appset.StartupDelayDuration).Ticks); }
+ catch { }
+
+ if (delayTicks > 0)
+ {
+ this.Pause(TimeSpan.FromTicks(delayTicks));
+ }
+ else
+ {
+ this.Resume();
+ }
+ }
+
+ m_pausedForSuspend = false;
+ m_suspendMinimumPause = new DateTime(0);
+ }
+ }
+
+ }
+}
diff --git a/Duplicati/Server/LogWriteHandler.cs b/Duplicati.Library.RestAPI/LogWriteHandler.cs
similarity index 100%
rename from Duplicati/Server/LogWriteHandler.cs
rename to Duplicati.Library.RestAPI/LogWriteHandler.cs
diff --git a/Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs b/Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs
new file mode 100644
index 000000000..f9b558c5b
--- /dev/null
+++ b/Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs
@@ -0,0 +1,57 @@
+// 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> Types
+ {
+ get
+ {
+ return new KeyValuePair[] {
+ new KeyValuePair(HttpServer.Method.Get, typeof(GetResponse)),
+ };
+ }
+ }
+ }
+}
+
diff --git a/Duplicati/Server/WebServer/RESTMethods/Backup.cs b/Duplicati.Library.RestAPI/RESTMethods/Backup.cs
similarity index 84%
rename from Duplicati/Server/WebServer/RESTMethods/Backup.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Backup.cs
index 926e7ba80..e18730fea 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Backup.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Backup.cs
@@ -1,732 +1,742 @@
-// 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.Collections.Generic;
-using Duplicati.Server.Serialization;
-using System.IO;
-using System.Linq;
-using Duplicati.Server.Serialization.Interface;
-
-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 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();
-
- 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
- {
- ["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 = Program.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()))
- {
- 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);
-
- Program.WorkThread.AddTask(task);
-
- info.OutputOK(new { TaskID = task.TaskID });
- }
-
- private void CreateReport(IBackup backup, RequestInfo info)
- {
- var task = Runner.CreateTask(DuplicatiOperation.CreateReport, backup);
- Program.WorkThread.AddTask(task);
- Program.StatusEventNotifyer.SignalNewEvent();
-
- info.OutputOK(new { Status = "OK", ID = task.TaskID });
- }
-
- private void ReportRemoteSize(IBackup backup, RequestInfo info)
- {
- var task = Runner.CreateTask(DuplicatiOperation.ListRemote, backup);
- Program.WorkThread.AddTask(task);
- Program.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);
- Program.WorkThread.AddTask(task);
- Program.StatusEventNotifyer.SignalNewEvent();
-
- info.OutputOK(new { Status = "OK", ID = task.TaskID });
- }
-
- private void Verify(IBackup backup, RequestInfo info)
- {
- var task = Runner.CreateTask(DuplicatiOperation.Verify, backup);
- Program.WorkThread.AddTask(task);
- Program.StatusEventNotifyer.SignalNewEvent();
-
- info.OutputOK(new {Status = "OK", ID = task.TaskID});
- }
-
- private void Compact(IBackup backup, RequestInfo info)
- {
- var task = Runner.CreateTask(DuplicatiOperation.Compact, backup);
- Program.WorkThread.AddTask(task);
- Program.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(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();
- 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);
- Program.WorkThread.AddTask(task);
- Program.StatusEventNotifyer.SignalNewEvent();
-
- info.OutputOK(new {Status = "OK", ID = task.TaskID});
- }
-
- private void RunBackup(IBackup backup, RequestInfo info)
- {
- var t = Program.WorkThread.CurrentTask;
- var bt = t == null ? null : t.Backup;
- if (bt != null && backup.ID == bt.ID)
- {
- // Already running
- }
- else if (Program.WorkThread.CurrentTasks.Any(x => {
- var bn = x?.Backup;
- return bn == null || bn.ID == backup.ID;
- }))
- {
- // Already in queue
- }
- else
- {
- Program.WorkThread.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup), true);
- Program.StatusEventNotifyer.SignalNewEvent();
- }
-
- info.OutputOK();
- }
-
- private void IsActive(IBackup backup, RequestInfo info)
- {
- var t = Program.WorkThread.CurrentTask;
- var bt = t?.Backup;
- if (bt != null && backup.ID == bt.ID)
- {
- info.OutputOK(new { Status = "OK", Active = true });
- return;
- }
- else if (Program.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);
-
- Program.DataConnection.UpdateBackupDBPath(backup, np);
- }
-
- }
-
- }
-
- public void GET(string key, RequestInfo info)
- {
- var parts = (key ?? "").Split(new char[] { '/' }, 2);
- var bk = Program.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 = Program.DataConnection.GetScheduleIDsFromTags(new string[] { "ID=" + bk.ID });
- var schedule = scheduleId.Any() ? Program.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 = Program.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(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 = Program.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 = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8).ReadToEnd();
-
- if (string.IsNullOrWhiteSpace(str))
- {
- info.ReportClientError("Missing backup object", System.Net.HttpStatusCode.BadRequest);
- return;
- }
-
- Backups.AddOrUpdateBackupData data = null;
- try
- {
- data = Serializer.Deserialize(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 = Program.DataConnection.GetBackup(data.Backup.ID);
- if (backup.IsTemporary)
- throw new InvalidDataException("External is temporary but internal is not?");
-
- Program.DataConnection.UpdateTemporaryBackup(backup);
- info.OutputOK();
- }
- else
- {
- lock(Program.DataConnection.m_lock)
- {
- var backup = Program.DataConnection.GetBackup(data.Backup.ID);
- if (backup == null)
- {
- info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound);
- return;
- }
-
- if (Program.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 = Program.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
- Program.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 = Program.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 (Program.WorkThread.Active)
- {
- try
- {
- //TODO: It's not safe to access the values like this,
- //because the runner thread might interfere
- var nt = Program.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 = Program.LiveControl.State == LiveControls.LiveControlState.Paused;
- Program.LiveControl.Pause();
-
- for(int i = 0; i < 10; i++)
- if (Program.WorkThread.Active)
- {
- var t = Program.WorkThread.CurrentTask;
- if (backup.Equals(t == null ? null : t.Backup))
- System.Threading.Thread.Sleep(1000);
- else
- break;
- }
- else
- break;
-
- if (Program.WorkThread.Active)
- {
- var t = Program.WorkThread.CurrentTask;
- if (backup.Equals(t == null ? null : t.Backup))
- {
- if (hasPaused)
- Program.LiveControl.Resume();
- info.OutputError(new { status = "failed", reason = "backup-unstoppable" });
- return;
- }
- }
-
- if (hasPaused)
- Program.LiveControl.Resume();
- }
- }
- catch (Exception ex)
- {
- info.OutputError(new { status = "error", message = ex.Message });
- return;
- }
- }
-
- var extra = new Dictionary();
- 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);
- Program.WorkThread.AddTask(task);
- Program.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> Types
- {
- get
- {
- return new KeyValuePair[] {
- new KeyValuePair(HttpServer.Method.Get, typeof(GetResponse)),
- new KeyValuePair(HttpServer.Method.Put, typeof(Backups.AddOrUpdateBackupData)),
- new KeyValuePair(HttpServer.Method.Delete, typeof(long))
- };
- }
- }
- }
-}
-
+// 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 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();
+
+ 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
+ {
+ ["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()))
+ {
+ 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(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();
+ 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(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(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();
+ 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> Types
+ {
+ get
+ {
+ return new KeyValuePair[] {
+ new KeyValuePair(HttpServer.Method.Get, typeof(GetResponse)),
+ new KeyValuePair(HttpServer.Method.Put, typeof(Backups.AddOrUpdateBackupData)),
+ new KeyValuePair(HttpServer.Method.Delete, typeof(long))
+ };
+ }
+ }
+ }
+}
+
diff --git a/Duplicati/Server/WebServer/RESTMethods/BackupDefaults.cs b/Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs
similarity index 71%
rename from Duplicati/Server/WebServer/RESTMethods/BackupDefaults.cs
rename to Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs
index fc6725cca..4d6675520 100644
--- a/Duplicati/Server/WebServer/RESTMethods/BackupDefaults.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs
@@ -1,131 +1,141 @@
-// 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;
+// 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
- {
- 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 Program.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(System.Reflection.Assembly.GetExecutingAssembly().GetName().Name + ".newbackup.json")))
- n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(s.ReadToEnd());
-
- MergeJsonObjects(o, n);
- }
- catch
- {
- }
-
- try
- {
- // Add install defaults/overrides, if present
- var path = SystemIO.IO_OS.PathCombine(Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "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
- {
- }
-
- 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;
- }
- }
- }
- }
- }
-}
-
+namespace Duplicati.Server.WebServer.RESTMethods
+{
+ public class BackupDefaults : IRESTMethodGET
+ {
+
+ private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType();
+
+ 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;
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/Duplicati/Server/WebServer/RESTMethods/Backups.cs b/Duplicati.Library.RestAPI/RESTMethods/Backups.cs
similarity index 78%
rename from Duplicati/Server/WebServer/RESTMethods/Backups.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Backups.cs
index 91c028013..bae68fa57 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Backups.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Backups.cs
@@ -1,299 +1,310 @@
-// 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.Collections.Generic;
-using Duplicati.Server.Serialization;
-using System.IO;
-
-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 = Program.DataConnection.Schedules;
- var backups = Program.DataConnection.Backups;
-
- var all = from n in backups
- select new AddOrUpdateBackupData {
- IsUnencryptedOrPassphraseStored = Program.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 = "";
- //output_template = "";
- 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 (Program.DataConnection.m_lock)
- {
- var basename = ipx.Backup.Name;
- var c = 0;
- while (c++ < 100 && Program.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase)))
- ipx.Backup.Name = basename + " (" + c.ToString() + ")";
-
- if (Program.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 = Program.DataConnection.ValidateBackup(ipx.Backup, ipx.Schedule);
- if (!string.IsNullOrWhiteSpace(err))
- {
- info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest);
- return;
- }
-
- Program.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)
- {
- Program.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 getPassword, Dictionary 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 = Program.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 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()))
- {
- using (var m2 = m.Decrypt(fs))
- {
- using (var sr = new System.IO.StreamReader(m2))
- {
- ipx = Serializer.Deserialize(sr);
- }
- }
- }
- }
- else
- {
- using (var sr = new System.IO.StreamReader(fs))
- {
- ipx = Serializer.Deserialize(sr);
- }
- }
- }
-
- if (ipx.Backup == null)
- {
- throw new Exception("No backup found in document");
- }
-
- if (ipx.Backup.Metadata == null)
- {
- ipx.Backup.Metadata = new Dictionary();
- }
-
- 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 = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8).ReadToEnd();
-
- data = Serializer.Deserialize(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];
-
- Program.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(Program.DataConnection.m_lock)
- {
- if (Program.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 = Program.DataConnection.ValidateBackup(data.Backup, data.Schedule);
- if (!string.IsNullOrWhiteSpace(err))
- {
- info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest);
- return;
- }
-
- Program.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> Types
- {
- get
- {
- return new KeyValuePair[] {
- new KeyValuePair(HttpServer.Method.Get, typeof(AddOrUpdateBackupData[])),
- new KeyValuePair(HttpServer.Method.Post, typeof(AddOrUpdateBackupData))
- };
- }
- }
- }
-}
-
+// 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 = "";
+ //output_template = "";
+ 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 getPassword, Dictionary 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 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()))
+ {
+ using (var m2 = m.Decrypt(fs))
+ {
+ using (var sr = new System.IO.StreamReader(m2))
+ {
+ ipx = Serializer.Deserialize(sr);
+ }
+ }
+ }
+ }
+ else
+ {
+ using (var sr = new System.IO.StreamReader(fs))
+ {
+ ipx = Serializer.Deserialize(sr);
+ }
+ }
+ }
+
+ if (ipx.Backup == null)
+ {
+ throw new Exception("No backup found in document");
+ }
+
+ if (ipx.Backup.Metadata == null)
+ {
+ ipx.Backup.Metadata = new Dictionary();
+ }
+
+ 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(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> Types
+ {
+ get
+ {
+ return new KeyValuePair[] {
+ new KeyValuePair(HttpServer.Method.Get, typeof(AddOrUpdateBackupData[])),
+ new KeyValuePair(HttpServer.Method.Post, typeof(AddOrUpdateBackupData))
+ };
+ }
+ }
+ }
+}
+
diff --git a/Duplicati.Library.RestAPI/RESTMethods/BugReport.cs b/Duplicati.Library.RestAPI/RESTMethods/BugReport.cs
new file mode 100644
index 000000000..388a32aad
--- /dev/null
+++ b/Duplicati.Library.RestAPI/RESTMethods/BugReport.cs
@@ -0,0 +1,62 @@
+// 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();
+ }
+ }
+ }
+}
+
diff --git a/Duplicati/Server/WebServer/RESTMethods/Captcha.cs b/Duplicati.Library.RestAPI/RESTMethods/Captcha.cs
similarity index 78%
rename from Duplicati/Server/WebServer/RESTMethods/Captcha.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Captcha.cs
index de64085c0..93345c8f8 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Captcha.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Captcha.cs
@@ -1,166 +1,172 @@
-// Copyright (C) 2016, 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.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 m_captchas = new Dictionary();
-
- 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;
- token = Library.Utility.Utility.Base64PlainToBase64Url(Library.Utility.Utility.CalculateHash(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);
- }
- }
- }
-}
+// 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 m_captchas = new Dictionary();
+
+ 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);
+ }
+ }
+ }
+}
diff --git a/Duplicati/Server/WebServer/RESTMethods/Changelog.cs b/Duplicati.Library.RestAPI/RESTMethods/Changelog.cs
similarity index 58%
rename from Duplicati/Server/WebServer/RESTMethods/Changelog.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Changelog.cs
index 6adb73f0c..7a9712562 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Changelog.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Changelog.cs
@@ -1,19 +1,24 @@
-// 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
+// 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;
@@ -42,7 +47,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
}
else
{
- var updateInfo = Program.DataConnection.ApplicationSettings.UpdatedVersion;
+ var updateInfo = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion;
if (updateInfo == null)
{
info.ReportClientError("No update found", System.Net.HttpStatusCode.NotFound);
diff --git a/Duplicati/Server/WebServer/RESTMethods/CommandLine.cs b/Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs
similarity index 79%
rename from Duplicati/Server/WebServer/RESTMethods/CommandLine.cs
rename to Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs
index 624821b49..e4ff87db9 100644
--- a/Duplicati/Server/WebServer/RESTMethods/CommandLine.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs
@@ -1,283 +1,293 @@
-// Copyright (C) 2017, 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.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
-
-namespace Duplicati.Server.WebServer.RESTMethods
-{
- public class CommandLine : IRESTMethodGET, IRESTMethodPOST
- {
- private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType();
-
- 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)
- {
- Program.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
- Program.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 m_activeItems = new Dictionary();
- private System.Threading.Tasks.Task m_cleanupTask;
-
- public void POST(string key, RequestInfo info)
- {
- if (string.IsNullOrWhiteSpace(key))
- {
- string[] args;
- using (var sr = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
- args = Newtonsoft.Json.JsonConvert.DeserializeObject(sr.ReadToEnd());
-
- 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;
- }
- });
-
- Program.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.Abort();
-
- 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();
- 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
- });
- }
- }
- }
-}
+// 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();
+
+ 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 m_activeItems = new Dictionary();
+ 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(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();
+ 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
+ });
+ }
+ }
+ }
+}
diff --git a/Duplicati/Server/WebServer/RESTMethods/Filesystem.cs b/Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs
similarity index 87%
rename from Duplicati/Server/WebServer/RESTMethods/Filesystem.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs
index 3c2b25aed..cc77df60e 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Filesystem.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs
@@ -1,27 +1,31 @@
-// 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
+// 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;
-
+using Duplicati.Library.Common.IO;
+using Duplicati.Library.Common;
+
namespace Duplicati.Server.WebServer.RESTMethods
{
public class Filesystem : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented
diff --git a/Duplicati/Server/WebServer/RESTMethods/Help.cs b/Duplicati.Library.RestAPI/RESTMethods/Help.cs
similarity index 67%
rename from Duplicati/Server/WebServer/RESTMethods/Help.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Help.cs
index 87433390c..b409b7f77 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Help.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Help.cs
@@ -1,19 +1,23 @@
-// 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
+// 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;
diff --git a/Duplicati/Server/WebServer/RESTMethods/HyperV.cs b/Duplicati.Library.RestAPI/RESTMethods/HyperV.cs
similarity index 68%
rename from Duplicati/Server/WebServer/RESTMethods/HyperV.cs
rename to Duplicati.Library.RestAPI/RESTMethods/HyperV.cs
index b5298eab9..b957cfd48 100644
--- a/Duplicati/Server/WebServer/RESTMethods/HyperV.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/HyperV.cs
@@ -1,27 +1,31 @@
-// 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
+// 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;
-
+using Duplicati.Library.Common;
+
namespace Duplicati.Server.WebServer.RESTMethods
{
public class HyperV : IRESTMethodGET, IRESTMethodDocumented
diff --git a/Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs b/Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs
new file mode 100644
index 000000000..488f4ce1d
--- /dev/null
+++ b/Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs
@@ -0,0 +1,60 @@
+// 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> 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);
+ }
+}
+
diff --git a/Duplicati.Library.RestAPI/RESTMethods/Licenses.cs b/Duplicati.Library.RestAPI/RESTMethods/Licenses.cs
new file mode 100644
index 000000000..39bd5d754
--- /dev/null
+++ b/Duplicati.Library.RestAPI/RESTMethods/Licenses.cs
@@ -0,0 +1,44 @@
+// 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));
+ }
+ }
+}
+
diff --git a/Duplicati/Server/WebServer/RESTMethods/LogData.cs b/Duplicati.Library.RestAPI/RESTMethods/LogData.cs
similarity index 69%
rename from Duplicati/Server/WebServer/RESTMethods/LogData.cs
rename to Duplicati.Library.RestAPI/RESTMethods/LogData.cs
index f87bbe2cb..1b78b4898 100644
--- a/Duplicati/Server/WebServer/RESTMethods/LogData.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/LogData.cs
@@ -1,21 +1,26 @@
-// 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
+// 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
{
@@ -41,13 +46,13 @@ namespace Duplicati.Server.WebServer.RESTMethods
long.TryParse(id_str, out id);
Enum.TryParse(level_str, true, out level);
- info.OutputOK(Program.LogHandler.AfterID(id, level, pagesize));
+ info.OutputOK(FIXMEGlobal.LogHandler.AfterID(id, level, pagesize));
}
else
{
List> res = null;
- Program.DataConnection.ExecuteWithCommand(x =>
+ FIXMEGlobal.DataConnection.ExecuteWithCommand(x =>
{
res = DumpTable(x, "ErrorLog", "Timestamp", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value);
});
diff --git a/Duplicati/Server/WebServer/RESTMethods/MSSQL.cs b/Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs
similarity index 67%
rename from Duplicati/Server/WebServer/RESTMethods/MSSQL.cs
rename to Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs
index 2015fb959..be873efdb 100644
--- a/Duplicati/Server/WebServer/RESTMethods/MSSQL.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs
@@ -1,27 +1,31 @@
-// 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
+// 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;
-
+using Duplicati.Library.Common;
+
namespace Duplicati.Server.WebServer.RESTMethods
{
public class MSSQL : IRESTMethodGET, IRESTMethodDocumented
diff --git a/Duplicati.Library.RestAPI/RESTMethods/Notification.cs b/Duplicati.Library.RestAPI/RESTMethods/Notification.cs
new file mode 100644
index 000000000..24199cf0b
--- /dev/null
+++ b/Duplicati.Library.RestAPI/RESTMethods/Notification.cs
@@ -0,0 +1,65 @@
+// 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();
+ }
+ }
+ }
+}
+
diff --git a/Duplicati.Library.RestAPI/RESTMethods/Notifications.cs b/Duplicati.Library.RestAPI/RESTMethods/Notifications.cs
new file mode 100644
index 000000000..3e4bb7b8b
--- /dev/null
+++ b/Duplicati.Library.RestAPI/RESTMethods/Notifications.cs
@@ -0,0 +1,34 @@
+// 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;
+
+namespace Duplicati.Server.WebServer.RESTMethods
+{
+ public class Notifications : IRESTMethodGET
+ {
+ public void GET(string key, RequestInfo info)
+ {
+ info.OutputOK(FIXMEGlobal.DataConnection.GetNotifications());
+ }
+ }
+}
+
diff --git a/Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs b/Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs
new file mode 100644
index 000000000..e85974dd4
--- /dev/null
+++ b/Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs
@@ -0,0 +1,50 @@
+// 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> Types
+ {
+ get
+ {
+ return new KeyValuePair[] {
+ new KeyValuePair(HttpServer.Method.Get, typeof(Serialization.Interface.IProgressEventData))
+ };
+ }
+ }
+ }
+}
+
diff --git a/Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs b/Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs
similarity index 51%
rename from Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs
rename to Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs
index 6996683f6..f9a8ded5e 100644
--- a/Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs
@@ -1,23 +1,29 @@
-// 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
+// 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
{
@@ -32,6 +38,62 @@ namespace Duplicati.Server.WebServer.RESTMethods
});
}
+ private Dictionary ParseUrlOptions(Library.Utility.Uri uri)
+ {
+ var qp = uri.QueryParameters;
+
+ var opts = Runner.GetCommonOptions();
+ foreach (var k in qp.Keys.Cast())
+ opts[k] = qp[k];
+
+ return opts;
+ }
+
+ private IEnumerable ConfigureModules(IDictionary 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 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());
+ 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()))
@@ -46,12 +108,12 @@ namespace Duplicati.Server.WebServer.RESTMethods
var remotename = info.Request.QueryString["filename"].Value;
using(var ms = new System.IO.MemoryStream())
- using(var b = Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary()))
+ using(var b = GetBackend(uri))
{
using(var tf = new Library.Utility.TempFile())
{
System.IO.File.WriteAllText(tf, data);
- b.PutAsync(remotename, tf, CancellationToken.None).Wait();
+ b.Backend.PutAsync(remotename, tf, CancellationToken.None).Wait();
}
}
@@ -60,37 +122,43 @@ namespace Duplicati.Server.WebServer.RESTMethods
private void ListFolder(string uri, RequestInfo info)
{
- using(var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary()))
- info.OutputOK(b.List());
+ using(var b = GetBackend(uri))
+ info.OutputOK(b.Backend.List());
}
private void TestConnection(string url, RequestInfo info)
{
-
- var modules = (from n in Library.DynamicLoader.GenericLoader.Modules
- where n is Library.Interface.IConnectionModule
- select n).ToArray();
+ bool autoCreate = info.Request.Param.Contains("autocreate")
+ ? Library.Utility.Utility.ParseBool(info.Request.Param["autocreate"].Value, false)
+ : false;
+
+ TupleDisposeWrapper wrapper = null;
try
{
- var uri = new Library.Utility.Uri(url);
- var qp = uri.QueryParameters;
+ wrapper = GetBackend(url);
- var opts = new Dictionary();
- foreach (var k in qp.Keys.Cast())
- opts[k] = qp[k];
+ using (var b = wrapper.Backend)
+ {
+ try { b.Test(); }
+ catch (FolderMissingException)
+ {
+ if (!autoCreate)
+ throw;
- foreach (var n in modules)
- n.Configure(opts);
-
- using (var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(url, new Dictionary()))
- b.Test();
-
- info.OutputOK();
+ b.CreateFolder();
+ b.Test();
+ }
+ info.OutputOK();
+ }
}
catch (Duplicati.Library.Interface.FolderMissingException)
{
- info.ReportServerError("missing-folder");
+ if (!autoCreate) {
+ info.ReportServerError("missing-folder");
+ } else {
+ info.ReportServerError("error-creating-folder");
+ }
}
catch (Duplicati.Library.Utility.SslCertificateValidator.InvalidCertificateException icex)
{
@@ -114,9 +182,8 @@ namespace Duplicati.Server.WebServer.RESTMethods
}
finally
{
- foreach (var n in modules)
- if (n is IDisposable disposable)
- disposable.Dispose();
+ if (wrapper != null)
+ wrapper.DisposeModules();
}
}
diff --git a/Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs b/Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs
similarity index 71%
rename from Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs
rename to Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs
index c80b7edd8..2069b6cf9 100644
--- a/Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs
@@ -1,19 +1,23 @@
-// 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
+// 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
@@ -92,6 +96,10 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
Response.Status = code;
Response.Reason = reason ?? "Error";
+ if(item == null && reason != null)
+ {
+ item = new { Error = reason };
+ }
BodyWriter.WriteJsonObject(item);
}
diff --git a/Duplicati/Server/WebServer/RESTMethods/ServerSetting.cs b/Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs
similarity index 62%
rename from Duplicati/Server/WebServer/RESTMethods/ServerSetting.cs
rename to Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs
index 9454c15fa..418589837 100644
--- a/Duplicati/Server/WebServer/RESTMethods/ServerSetting.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs
@@ -1,19 +1,24 @@
-// 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
+// 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;
@@ -32,13 +37,13 @@ namespace Duplicati.Server.WebServer.RESTMethods
if (key.Equals("server-ssl-certificate", StringComparison.OrdinalIgnoreCase) || key.Equals("ServerSSLCertificate", StringComparison.OrdinalIgnoreCase))
{
- info.OutputOK(Program.DataConnection.ApplicationSettings.ServerSSLCertificate == null ? "False" : "True");
+ info.OutputOK(FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate == null ? "False" : "True");
return;
}
if (key.StartsWith("--", StringComparison.Ordinal))
{
- var prop = Program.DataConnection.Settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase));
+ var prop = FIXMEGlobal.DataConnection.Settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase));
info.OutputOK(prop == null ? null : prop.Value);
}
else
@@ -47,7 +52,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
if (prop == null)
info.OutputError(null, System.Net.HttpStatusCode.NotFound, "Not found");
else
- info.OutputOK(prop.GetValue(Program.DataConnection.ApplicationSettings));
+ info.OutputOK(prop.GetValue(FIXMEGlobal.DataConnection.ApplicationSettings));
}
}
@@ -67,7 +72,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
if (key.StartsWith("--", StringComparison.Ordinal))
{
- var settings = Program.DataConnection.Settings.ToList();
+ var settings = FIXMEGlobal.DataConnection.Settings.ToList();
var prop = settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase));
if (prop == null)
@@ -75,7 +80,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
else
prop.Value = info.Request.Form["data"].Value;
- Program.DataConnection.Settings = settings.ToArray();
+ FIXMEGlobal.DataConnection.Settings = settings.ToArray();
info.OutputOK(prop == null ? null : prop.Value);
}
@@ -88,7 +93,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
var dict = new Dictionary();
dict[key] = info.Request.Form["data"].Value;
- Program.DataConnection.ApplicationSettings.UpdateSettings(dict, false);
+ FIXMEGlobal.DataConnection.ApplicationSettings.UpdateSettings(dict, false);
info.OutputOK();
}
}
diff --git a/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs b/Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs
similarity index 66%
rename from Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs
rename to Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs
index cb09037d8..77258f307 100644
--- a/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs
@@ -1,126 +1,136 @@
-// 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.Collections.Generic;
-using Duplicati.Server.Database;
-using System.IO;
-using Duplicati.Server.Serialization;
-
-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 =
- Program.DataConnection.GetSettings(Database.Connection.SERVER_SETTINGS_ID)
- .Where(x => !string.IsNullOrWhiteSpace(x.Name))
- .Union(
- Program.DataConnection.Settings
- .Where(x => !string.IsNullOrWhiteSpace(x.Name) && x.Name.StartsWith("--", StringComparison.Ordinal))
- );
-
- var dict = new Dictionary();
- 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 = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8).ReadToEnd();
-
- if (string.IsNullOrWhiteSpace(str))
- {
- info.ReportClientError("Missing data object", System.Net.HttpStatusCode.BadRequest);
- return;
- }
-
- Dictionary data = null;
- try
- {
- data = Serializer.Deserialize>(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())
- Program.DataConnection.ApplicationSettings.UpdateSettings(serversettings, false);
-
- if (globalsettings.Any())
- {
- // Update based on inputs
- var existing = Program.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 };
- }
-
- Program.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> Types
- {
- get
- {
- return new KeyValuePair[] {
- new KeyValuePair(HttpServer.Method.Get, typeof(Database.ServerSettings))
- };
- }
- }
- }
-}
-
+// 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();
+ 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 data = null;
+ try
+ {
+ data = Serializer.Deserialize>(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> Types
+ {
+ get
+ {
+ return new KeyValuePair[] {
+ new KeyValuePair(HttpServer.Method.Get, typeof(Database.ServerSettings))
+ };
+ }
+ }
+ }
+}
+
diff --git a/Duplicati/Server/WebServer/RESTMethods/ServerState.cs b/Duplicati.Library.RestAPI/RESTMethods/ServerState.cs
similarity index 61%
rename from Duplicati/Server/WebServer/RESTMethods/ServerState.cs
rename to Duplicati.Library.RestAPI/RESTMethods/ServerState.cs
index 89c065a72..bd1fa914a 100644
--- a/Duplicati/Server/WebServer/RESTMethods/ServerState.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/ServerState.cs
@@ -1,19 +1,24 @@
-// 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
+// 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;
@@ -27,7 +32,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
long id = 0;
long.TryParse(key, out id);
- if (info.LongPollCheck(Program.StatusEventNotifyer, ref id, out isError))
+ 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();
@@ -59,20 +64,20 @@ namespace Duplicati.Server.WebServer.RESTMethods
return;
}
if (ts.TotalMilliseconds > 0)
- Program.LiveControl.Pause(ts);
+ FIXMEGlobal.LiveControl.Pause(ts);
else
- Program.LiveControl.Pause();
+ FIXMEGlobal.LiveControl.Pause();
}
else
{
- Program.LiveControl.Pause();
+ FIXMEGlobal.LiveControl.Pause();
}
info.OutputOK();
return;
case "resume":
- Program.LiveControl.Resume();
+ FIXMEGlobal.LiveControl.Resume();
info.OutputOK();
return;
diff --git a/Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs b/Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs
similarity index 67%
rename from Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs
rename to Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs
index 3932e895a..3c84ad5c6 100644
--- a/Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs
@@ -1,28 +1,33 @@
-// 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
+// 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.Common;
+using Duplicati.Library.RestAPI;
+
namespace Duplicati.Server.WebServer.RESTMethods
{
- public class SystemInfo : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented
+ public class SystemInfo : IRESTMethodGET, IRESTMethodDocumented
{
public string Description { get { return "Gets various system properties"; } }
public IEnumerable> Types
@@ -40,26 +45,6 @@ namespace Duplicati.Server.WebServer.RESTMethods
info.BodyWriter.OutputOK(SystemData(info));
}
- public void POST(string key, RequestInfo info)
- {
- switch ((key ?? "").ToLowerInvariant())
- {
- case "suppressdonationmessages":
- Library.Main.Utility.SuppressDonationMessages = true;
- info.OutputOK();
- return;
-
- case "showdonationmessages":
- Library.Main.Utility.SuppressDonationMessages = false;
- info.OutputOK();
- return;
-
- default:
- info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound);
- return;
- }
- }
-
private static object SystemData(RequestInfo info)
{
var browserlanguage = RESTHandler.ParseDefaultRequestCulture(info) ?? System.Globalization.CultureInfo.InvariantCulture;
@@ -71,8 +56,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
ServerVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(),
ServerVersionName = Duplicati.License.VersionNumbers.Version,
ServerVersionType = Duplicati.Library.AutoUpdater.UpdaterManager.SelfVersion.ReleaseType,
- StartedBy = Duplicati.Server.Program.Origin,
- BaseVersionName = Duplicati.Library.AutoUpdater.UpdaterManager.BaseVersion.Displayname,
+ StartedBy = FIXMEGlobal.Origin,
DefaultUpdateChannel = Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel,
DefaultUsageReportLevel = Duplicati.Library.UsageReporter.Reporter.DefaultReportLevel,
ServerTime = DateTime.Now,
@@ -80,8 +64,8 @@ namespace Duplicati.Server.WebServer.RESTMethods
DirectorySeparator = System.IO.Path.DirectorySeparatorChar,
PathSeparator = System.IO.Path.PathSeparator,
CaseSensitiveFilesystem = Duplicati.Library.Utility.Utility.IsFSCaseSensitive,
- MonoVersion = Duplicati.Library.Utility.Utility.IsMono ? Duplicati.Library.Utility.Utility.MonoVersion.ToString() : null,
MachineName = System.Environment.MachineName,
+ PackageTypeId = Duplicati.Library.AutoUpdater.UpdaterManager.PackageTypeId,
UserName = System.Environment.UserName,
NewLine = System.Environment.NewLine,
CLRVersion = System.Environment.Version.ToString(),
@@ -102,7 +86,6 @@ namespace Duplicati.Server.WebServer.RESTMethods
ServerModules = Serializable.ServerSettings.ServerModules,
UsingAlternateUpdateURLs = Duplicati.Library.AutoUpdater.AutoUpdateSettings.UsesAlternateURLs,
LogLevels = Enum.GetNames(typeof(Duplicati.Library.Logging.LogMessageType)),
- SuppressDonationMessages = Duplicati.Library.Main.Utility.SuppressDonationMessages,
SpecialFolders = from n in SpecialFolders.Nodes select new { ID = n.id, Path = n.resolvedpath },
BrowserLocale = new
{
diff --git a/Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs b/Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs
new file mode 100644
index 000000000..09eebe706
--- /dev/null
+++ b/Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs
@@ -0,0 +1,48 @@
+// 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()).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> Types
+ {
+ get
+ {
+ return new KeyValuePair[] {
+ new KeyValuePair(HttpServer.Method.Get, typeof(ICommandLineArgument[]))
+ };
+ }
+ }
+
+ }
+}
+
diff --git a/Duplicati.Library.RestAPI/RESTMethods/Tags.cs b/Duplicati.Library.RestAPI/RESTMethods/Tags.cs
new file mode 100644
index 000000000..90333250a
--- /dev/null
+++ b/Duplicati.Library.RestAPI/RESTMethods/Tags.cs
@@ -0,0 +1,59 @@
+// 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> Types
+ {
+ get
+ {
+ return new KeyValuePair[] {
+ new KeyValuePair(HttpServer.Method.Get, typeof(string[])),
+ };
+ }
+ }
+ }
+}
+
diff --git a/Duplicati/Server/WebServer/RESTMethods/Task.cs b/Duplicati.Library.RestAPI/RESTMethods/Task.cs
similarity index 64%
rename from Duplicati/Server/WebServer/RESTMethods/Task.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Task.cs
index 756dd2e3e..1af624e61 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Task.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Task.cs
@@ -1,111 +1,113 @@
-#region Disclaimer / License
-// Copyright (C) 2019, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-//
-#endregion
-using System;
-using System.Linq;
-using System.Collections.Generic;
-
-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 = Program.WorkThread.CurrentTask;
- var tasks = Program.WorkThread.CurrentTasks;
-
- if (task != null && task.TaskID == taskid)
- {
- info.OutputOK(new { Status = "Running" });
- return;
- }
-
- if (tasks.FirstOrDefault(x => x.TaskID == taskid) == null)
- {
- KeyValuePair[] matches;
- lock(Program.MainLock)
- matches = Program.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 = Program.WorkThread.CurrentTask;
- var tasks = Program.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);
- }
- }
-}
-
+// 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[] 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);
+ }
+ }
+}
+
diff --git a/Duplicati.Library.RestAPI/RESTMethods/Tasks.cs b/Duplicati.Library.RestAPI/RESTMethods/Tasks.cs
new file mode 100644
index 000000000..cd900c522
--- /dev/null
+++ b/Duplicati.Library.RestAPI/RESTMethods/Tasks.cs
@@ -0,0 +1,41 @@
+// 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 Tasks : IRESTMethodGET
+ {
+ public void GET(string key, RequestInfo info)
+ {
+ var cur = FIXMEGlobal.WorkThread.CurrentTask;
+ var n = FIXMEGlobal.WorkThread.CurrentTasks;
+
+ if (cur != null)
+ n.Insert(0, cur);
+
+ info.OutputOK(n);
+ }
+ }
+}
+
diff --git a/Duplicati.Library.RestAPI/RESTMethods/UISettings.cs b/Duplicati.Library.RestAPI/RESTMethods/UISettings.cs
new file mode 100644
index 000000000..37330516d
--- /dev/null
+++ b/Duplicati.Library.RestAPI/RESTMethods/UISettings.cs
@@ -0,0 +1,82 @@
+// 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 data;
+ try
+ {
+ data = Serializer.Deserialize>(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();
+ }
+
+ }
+}
+
diff --git a/Duplicati.Library.RestAPI/RESTMethods/Updates.cs b/Duplicati.Library.RestAPI/RESTMethods/Updates.cs
new file mode 100644
index 000000000..94abc4491
--- /dev/null
+++ b/Duplicati.Library.RestAPI/RESTMethods/Updates.cs
@@ -0,0 +1,44 @@
+// 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;
+
+namespace Duplicati.Server.WebServer.RESTMethods
+{
+ public class Updates : IRESTMethodPOST
+ {
+ public void POST(string key, RequestInfo info)
+ {
+ switch ((key ?? "").ToLowerInvariant())
+ {
+ case "check":
+ FIXMEGlobal.UpdatePoller.CheckNow();
+ info.OutputOK();
+ return;
+
+ default:
+ info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound);
+ return;
+ }
+ }
+ }
+}
+
diff --git a/Duplicati.Library.RestAPI/RESTMethods/WebModule.cs b/Duplicati.Library.RestAPI/RESTMethods/WebModule.cs
new file mode 100644
index 000000000..676b3c963
--- /dev/null
+++ b/Duplicati.Library.RestAPI/RESTMethods/WebModule.cs
@@ -0,0 +1,45 @@
+// 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))
+ });
+ }
+ }
+}
+
diff --git a/Duplicati.Library.RestAPI/RESTMethods/WebModules.cs b/Duplicati.Library.RestAPI/RESTMethods/WebModules.cs
new file mode 100644
index 000000000..9f90d91db
--- /dev/null
+++ b/Duplicati.Library.RestAPI/RESTMethods/WebModules.cs
@@ -0,0 +1,34 @@
+// 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 WebModules : IRESTMethodGET
+ {
+ public void GET(string key, RequestInfo info)
+ {
+ info.OutputOK(Duplicati.Library.DynamicLoader.WebLoader.Modules);
+ }
+ }
+}
+
diff --git a/Duplicati/Server/Runner.cs b/Duplicati.Library.RestAPI/Runner.cs
similarity index 81%
rename from Duplicati/Server/Runner.cs
rename to Duplicati.Library.RestAPI/Runner.cs
index 33a230381..544bb3f92 100644
--- a/Duplicati/Server/Runner.cs
+++ b/Duplicati.Library.RestAPI/Runner.cs
@@ -1,965 +1,968 @@
-#region Disclaimer / License
-// Copyright (C) 2019, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-//
-#endregion
-using System;
-using System.Linq;
-using System.Collections.Generic;
-using Duplicati.Library.Interface;
-using Duplicati.Server.Serialization;
-
-namespace Duplicati.Server
-{
- public static class Runner
- {
- public interface IRunnerData : Duplicati.Server.Serialization.Interface.IQueuedTask
- {
- Duplicati.Server.Serialization.Interface.IBackup Backup { get; }
- IDictionary ExtraOptions { get; }
- string[] FilterStrings { get; }
- void Stop(bool allowCurrentFileToFinish);
- void Abort();
- void Pause();
- void Resume();
- void UpdateThrottleSpeed();
- void SetController(Duplicati.Library.Main.Controller controller);
- }
-
- private class RunnerData : IRunnerData
- {
- private static long RunnerTaskID = 1;
-
- public Duplicati.Server.Serialization.DuplicatiOperation Operation { get; internal set; }
- public Duplicati.Server.Serialization.Interface.IBackup Backup { get; internal set; }
- public IDictionary ExtraOptions { get; internal set; }
- public string[] FilterStrings { get; internal set; }
-
- public string BackupID { get { return Backup.ID; } }
- public long TaskID { get { return m_taskID; } }
-
- internal Duplicati.Library.Main.Controller Controller { get; set; }
-
- public void SetController(Duplicati.Library.Main.Controller controller)
- {
- Controller = controller;
- }
-
- public void Stop(bool allowCurrentFileToFinish)
- {
- var c = Controller;
- if (c != null)
- c.Stop(allowCurrentFileToFinish);
- }
-
- public void Abort()
- {
- var c = Controller;
- if (c != null)
- c.Abort();
- }
-
- public void Pause()
- {
- var c = Controller;
- if (c != null)
- c.Pause();
- }
-
- public void Resume()
- {
- var c = Controller;
- if (c != null)
- c.Resume();
- }
-
- public long OriginalUploadSpeed { get; set; }
- public long OriginalDownloadSpeed { get; set; }
-
- public void UpdateThrottleSpeed()
- {
- var controller = this.Controller;
- if (controller == null)
- return;
-
- var job_upload_throttle = this.OriginalUploadSpeed <= 0 ? long.MaxValue : this.OriginalUploadSpeed;
- var job_download_throttle = this.OriginalDownloadSpeed <= 0 ? long.MaxValue : this.OriginalDownloadSpeed;
-
- var server_upload_throttle = long.MaxValue;
- var server_download_throttle = long.MaxValue;
-
- try
- {
- if (!string.IsNullOrWhiteSpace(Program.DataConnection.ApplicationSettings.UploadSpeedLimit))
- server_upload_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(Program.DataConnection.ApplicationSettings.UploadSpeedLimit, "kb");
- }
- catch { }
-
- try
- {
- if (!string.IsNullOrWhiteSpace(Program.DataConnection.ApplicationSettings.DownloadSpeedLimit))
- server_download_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(Program.DataConnection.ApplicationSettings.DownloadSpeedLimit, "kb");
- }
- catch { }
-
- var upload_throttle = Math.Min(job_upload_throttle, server_upload_throttle);
- var download_throttle = Math.Min(job_download_throttle, server_download_throttle);
-
- if (upload_throttle <= 0 || upload_throttle == long.MaxValue)
- upload_throttle = 0;
-
- if (download_throttle <= 0 || download_throttle == long.MaxValue)
- download_throttle = 0;
-
- controller.MaxUploadSpeed = upload_throttle;
- controller.MaxDownloadSpeed = download_throttle;
- }
-
- private readonly long m_taskID;
-
- public RunnerData()
- {
- m_taskID = System.Threading.Interlocked.Increment(ref RunnerTaskID);
- }
- }
-
- private class CustomRunnerTask : RunnerData
- {
- public readonly Action Run;
-
- public CustomRunnerTask(Action runner)
- : base()
- {
- if (runner == null)
- throw new ArgumentNullException(nameof(runner));
- Run = runner;
- Operation = DuplicatiOperation.CustomRunner;
- Backup = new Database.Backup();
- }
- }
-
- public static IRunnerData CreateCustomTask(Action runner)
- {
- return new CustomRunnerTask(runner);
- }
-
- public static IRunnerData CreateTask(Duplicati.Server.Serialization.DuplicatiOperation operation, Duplicati.Server.Serialization.Interface.IBackup backup, IDictionary extraOptions = null, string[] filterStrings = null)
- {
- return new RunnerData() {
- Operation = operation,
- Backup = backup,
- ExtraOptions = extraOptions,
- FilterStrings = filterStrings
- };
- }
-
- public static IRunnerData CreateListTask(Duplicati.Server.Serialization.Interface.IBackup backup, string[] filters, bool onlyPrefix, bool allVersions, bool folderContents, DateTime time)
- {
- var dict = new Dictionary();
- if (onlyPrefix)
- dict["list-prefix-only"] = "true";
- if (allVersions)
- dict["all-versions"] = "true";
- if (time.Ticks > 0)
- dict["time"] = Duplicati.Library.Utility.Utility.SerializeDateTime(time.ToUniversalTime());
- if (folderContents)
- dict["list-folder-contents"] = "true";
-
- return CreateTask(
- DuplicatiOperation.List,
- backup,
- dict,
- filters);
- }
-
- public static IRunnerData CreateRestoreTask(Duplicati.Server.Serialization.Interface.IBackup backup, string[] filters,
- DateTime time, string restoreTarget, bool overwrite, bool restore_permissions,
- bool skip_metadata, string passphrase)
- {
- var dict = new Dictionary
- {
- ["time"] = Library.Utility.Utility.SerializeDateTime(time.ToUniversalTime()),
- ["overwrite"] = overwrite? Boolean.TrueString : Boolean.FalseString,
- ["restore-permissions"] = restore_permissions ? Boolean.TrueString : Boolean.FalseString,
- ["skip-metadata"] = skip_metadata ? Boolean.TrueString : Boolean.FalseString,
- ["allow-passphrase-change"] = Boolean.TrueString
- };
- if (!string.IsNullOrWhiteSpace(restoreTarget))
- dict["restore-path"] = SpecialFolders.ExpandEnvironmentVariables(restoreTarget);
- if (!(passphrase is null))
- dict["passphrase"] = passphrase;
-
- return CreateTask(
- DuplicatiOperation.Restore,
- backup,
- dict,
- filters);
- }
- private class MessageSink : Duplicati.Library.Main.IMessageSink
- {
- private class ProgressState : Server.Serialization.Interface.IProgressEventData
- {
- private readonly string m_backupID;
- private readonly long m_taskID;
-
- internal Duplicati.Library.Main.BackendActionType m_backendAction;
- internal string m_backendPath;
- internal long m_backendFileSize;
- internal long m_backendFileProgress;
- internal long m_backendSpeed;
- internal bool m_backendIsBlocking;
-
- internal string m_currentFilename;
- internal long m_currentFilesize;
- internal long m_currentFileoffset;
- internal bool m_currentFilecomplete;
-
- internal Duplicati.Library.Main.OperationPhase m_phase;
- internal float m_overallProgress;
- internal long m_processedFileCount;
- internal long m_processedFileSize;
- internal long m_totalFileCount;
- internal long m_totalFileSize;
- internal bool m_stillCounting;
-
- public ProgressState(long taskId, string backupId)
- {
- m_backupID = backupId;
- m_taskID = taskId;
- }
-
- internal ProgressState Clone()
- {
- return (ProgressState)this.MemberwiseClone();
- }
-
- #region IProgressEventData implementation
- public string BackupID { get { return m_backupID; } }
- public long TaskID { get { return m_taskID; } }
- public string BackendAction { get { return m_backendAction.ToString(); } }
- public string BackendPath { get { return m_backendPath; } }
- public long BackendFileSize { get { return m_backendFileSize; } }
- public long BackendFileProgress { get { return m_backendFileProgress; } }
- public long BackendSpeed { get { return m_backendSpeed; } }
- public bool BackendIsBlocking { get { return m_backendIsBlocking; } }
- public string CurrentFilename { get { return m_currentFilename; } }
- public long CurrentFilesize { get { return m_currentFilesize; } }
- public long CurrentFileoffset { get { return m_currentFileoffset; } }
- public bool CurrentFilecomplete { get { return m_currentFilecomplete; } }
- public string Phase { get { return m_phase.ToString(); } }
- public float OverallProgress { get { return m_overallProgress; } }
- public long ProcessedFileCount { get { return m_processedFileCount; } }
- public long ProcessedFileSize { get { return m_processedFileSize; } }
- public long TotalFileCount { get { return m_totalFileCount; } }
- public long TotalFileSize { get { return m_totalFileSize; } }
- public bool StillCounting { get { return m_stillCounting; } }
- #endregion
- }
-
- private readonly ProgressState m_state;
- private Duplicati.Library.Main.IBackendProgress m_backendProgress;
- private Duplicati.Library.Main.IOperationProgress m_operationProgress;
- private readonly object m_lock = new object();
-
- public MessageSink(long taskId, string backupId)
- {
- m_state = new ProgressState(taskId, backupId);
- }
-
- public Server.Serialization.Interface.IProgressEventData Copy()
- {
- lock(m_lock)
- {
- if (m_backendProgress != null)
- m_backendProgress.Update(out m_state.m_backendAction, out m_state.m_backendPath, out m_state.m_backendFileSize, out m_state.m_backendFileProgress, out m_state.m_backendSpeed, out m_state.m_backendIsBlocking);
- if (m_operationProgress != null)
- {
- m_operationProgress.UpdateFile(out m_state.m_currentFilename, out m_state.m_currentFilesize, out m_state.m_currentFileoffset, out m_state.m_currentFilecomplete);
- m_operationProgress.UpdateOverall(out m_state.m_phase, out m_state.m_overallProgress, out m_state.m_processedFileCount, out m_state.m_processedFileSize, out m_state.m_totalFileCount, out m_state.m_totalFileSize, out m_state.m_stillCounting);
- }
-
- return m_state.Clone();
- }
- }
-
- #region IMessageSink implementation
- public void BackendEvent(Duplicati.Library.Main.BackendActionType action, Duplicati.Library.Main.BackendEventType type, string path, long size)
- {
- lock(m_lock)
- {
- m_state.m_backendAction = action;
- m_state.m_backendPath = path;
- if (type == Duplicati.Library.Main.BackendEventType.Started)
- m_state.m_backendFileSize = size;
- else if (type == Duplicati.Library.Main.BackendEventType.Progress)
- m_state.m_backendFileProgress = size;
- else
- {
- m_state.m_backendFileSize = 0;
- m_state.m_backendFileProgress = 0;
- m_state.m_backendSpeed = 0;
- }
- }
- }
-
- public void SetBackendProgress(Library.Main.IBackendProgress progress)
- {
- lock (m_lock)
- m_backendProgress = progress;
- }
-
- public void SetOperationProgress(Library.Main.IOperationProgress progress)
- {
- lock (m_lock)
- m_operationProgress = progress;
- }
-
- public void WriteMessage(Library.Logging.LogEntry entry)
- {
- // Do nothing. Implementation needed for ILogDestination interface.
- }
- #endregion
- }
-
- public static string GetCommandLine(IRunnerData data)
- {
- var backup = data.Backup;
-
- var options = ApplyOptions(backup, GetCommonOptions());
- if (data.ExtraOptions != null)
- foreach(var k in data.ExtraOptions)
- options[k.Key] = k.Value;
-
- var cf = Program.DataConnection.Filters;
- var bf = backup.Filters;
-
- var sources =
- (from n in backup.Sources
- let p = SpecialFolders.ExpandEnvironmentVariables(n)
- where !string.IsNullOrWhiteSpace(p)
- select p).ToArray();
-
- var exe =
- System.IO.Path.Combine(
- Library.AutoUpdater.UpdaterManager.InstalledBaseDir,
- System.IO.Path.GetFileName(
- typeof(Duplicati.CommandLine.Commands).Assembly.Location
- )
- );
-
- var cmd = new System.Text.StringBuilder();
- if (Library.Utility.Utility.IsMono)
- cmd.Append("mono ");
-
- cmd.Append(Library.Utility.Utility.WrapAsCommandLine(new string[] { exe, "backup", backup.TargetURL }, false));
-
- cmd.Append(" ");
- cmd.Append(Library.Utility.Utility.WrapAsCommandLine(sources, true));
-
- // TODO: We should check each option to see if it is a path, and allow expansion on that
- foreach(var opt in options)
- cmd.AppendFormat(" --{0}={1}", opt.Key, Library.Utility.Utility.WrapCommandLineElement(opt.Value, false));
-
- if (cf != null)
- foreach(var f in cf)
- cmd.AppendFormat(" --{0}={1}", f.Include ? "include" : "exclude", Library.Utility.Utility.WrapCommandLineElement(f.Expression, true));
-
- if (bf != null)
- foreach(var f in bf)
- cmd.AppendFormat(" --{0}={1}", f.Include ? "include" : "exclude", Library.Utility.Utility.WrapCommandLineElement(f.Expression, true));
-
- return cmd.ToString();
- }
-
- public static string[] GetCommandLineParts(IRunnerData data)
- {
- var backup = data.Backup;
-
- var options = ApplyOptions(backup, GetCommonOptions());
- if (data.ExtraOptions != null)
- foreach (var k in data.ExtraOptions)
- options[k.Key] = k.Value;
-
- var cf = Program.DataConnection.Filters;
- var bf = backup.Filters;
-
- var sources =
- (from n in backup.Sources
- let p = SpecialFolders.ExpandEnvironmentVariables(n)
- where !string.IsNullOrWhiteSpace(p)
- select p).ToArray();
-
- var parts = new List
- {
- backup.TargetURL
- };
- parts.AddRange(sources);
-
- foreach (var opt in options)
- parts.Add(string.Format("--{0}={1}", opt.Key, opt.Value));
-
- if (cf != null)
- foreach (var f in cf)
- parts.Add(string.Format("--{0}={1}", f.Include ? "include" : "exclude", f.Expression));
-
- if (bf != null)
- foreach (var f in bf)
- parts.Add(string.Format("--{0}={1}", f.Include ? "include" : "exclude", f.Expression));
-
- return parts.ToArray();
- }
-
- public static Duplicati.Library.Interface.IBasicResults Run(IRunnerData data, bool fromQueue)
- {
- if (data is CustomRunnerTask task)
- {
- try
- {
- var sink = new MessageSink(task.TaskID, null);
- Program.GenerateProgressState = sink.Copy;
- Program.StatusEventNotifyer.SignalNewEvent();
-
- task.Run(sink);
- }
- catch(Exception ex)
- {
- Program.DataConnection.LogError(string.Empty, "Failed while executing custom task", ex);
- }
-
- return null;
- }
-
- var backup = data.Backup;
- if (backup.Metadata == null)
- {
- backup.Metadata = new Dictionary();
- }
-
- Duplicati.Library.Utility.TempFolder tempfolder = null;
-
- try
- {
- var sink = new MessageSink(data.TaskID, backup.ID);
- if (fromQueue)
- {
- Program.GenerateProgressState = () => sink.Copy();
- Program.StatusEventNotifyer.SignalNewEvent();
- }
-
- var options = ApplyOptions(backup, GetCommonOptions());
- if (data.ExtraOptions != null)
- foreach(var k in data.ExtraOptions)
- options[k.Key] = k.Value;
-
- // Pack in the system or task config for easy restore
- if (data.Operation == DuplicatiOperation.Backup && options.ContainsKey("store-task-config"))
- {
- tempfolder = StoreTaskConfigAndGetTempFolder(data, options);
- }
-
- // Attach a log scope that tags all messages to relay the TaskID and BackupID
- using (Library.Logging.Log.StartScope(log => {
- log[LogWriteHandler.LOG_EXTRA_TASKID] = data.TaskID.ToString();
- log[LogWriteHandler.LOG_EXTRA_BACKUPID] = data.BackupID;
- }))
-
- using(tempfolder)
- using(var controller = new Duplicati.Library.Main.Controller(backup.TargetURL, options, sink))
- {
- try
- {
- if (options.ContainsKey("throttle-upload"))
- ((RunnerData)data).OriginalUploadSpeed = Duplicati.Library.Utility.Sizeparser.ParseSize(options["throttle-upload"], "kb");
- }
- catch { }
-
- try
- {
- if (options.ContainsKey("throttle-download"))
- ((RunnerData)data).OriginalDownloadSpeed = Duplicati.Library.Utility.Sizeparser.ParseSize(options["throttle-download"], "kb");
- }
- catch { }
-
- ((RunnerData)data).Controller = controller;
- data.UpdateThrottleSpeed();
-
- if (backup.Metadata.ContainsKey("LastCompactFinished"))
- controller.LastCompact = Library.Utility.Utility.DeserializeDateTime(backup.Metadata["LastCompactFinished"]);
-
- if (backup.Metadata.ContainsKey("LastVacuumFinished"))
- controller.LastVacuum = Library.Utility.Utility.DeserializeDateTime(backup.Metadata["LastVacuumFinished"]);
-
- switch (data.Operation)
- {
- case DuplicatiOperation.Backup:
- {
- var filter = ApplyFilter(backup, GetCommonFilter());
- var sources =
- (from n in backup.Sources
- let p = SpecialFolders.ExpandEnvironmentVariables(n)
- where !string.IsNullOrWhiteSpace(p)
- select p).ToArray();
-
- var r = controller.Backup(sources, filter);
- UpdateMetadata(backup, r);
- return r;
- }
- case DuplicatiOperation.List:
- {
- var r = controller.List(data.FilterStrings, null);
- UpdateMetadata(backup, r);
- return r;
- }
- case DuplicatiOperation.Repair:
- {
- var r = controller.Repair(data.FilterStrings == null ? null : new Library.Utility.FilterExpression(data.FilterStrings));
- UpdateMetadata(backup, r);
- return r;
- }
- case DuplicatiOperation.RepairUpdate:
- {
- var r = controller.UpdateDatabaseWithVersions();
- UpdateMetadata(backup, r);
- return r;
- }
- case DuplicatiOperation.Remove:
- {
- var r = controller.Delete();
- UpdateMetadata(backup, r);
- return r;
- }
- case DuplicatiOperation.Restore:
- {
- var r = controller.Restore(data.FilterStrings);
- UpdateMetadata(backup, r);
- return r;
- }
- case DuplicatiOperation.Verify:
- {
- var r = controller.Test();
- UpdateMetadata(backup, r);
- return r;
- }
- case DuplicatiOperation.Compact:
- {
- var r = controller.Compact();
- UpdateMetadata(backup, r);
- return r;
- }
- case DuplicatiOperation.CreateReport:
- {
- using(var tf = new Duplicati.Library.Utility.TempFile())
- {
- var r = controller.CreateLogDatabase(tf);
- var tempid = Program.DataConnection.RegisterTempFile("create-bug-report", r.TargetPath, DateTime.Now.AddDays(3));
-
- if (string.Equals(tf, r.TargetPath, Library.Utility.Utility.ClientFilenameStringComparison))
- tf.Protected = true;
-
- Program.DataConnection.RegisterNotification(
- NotificationType.Information,
- "Bugreport ready",
- "Bugreport is ready for download",
- null,
- null,
- "bug-report:created:" + tempid,
- null,
- "BugreportCreatedReady",
- "",
- (n, a) => n
- );
-
- return r;
- }
- }
-
- case DuplicatiOperation.ListRemote:
- {
- var r = controller.ListRemote();
- UpdateMetadata(backup, r);
- return r;
- }
-
- case DuplicatiOperation.Delete:
- {
- if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions, "delete-remote-files"))
- controller.DeleteAllRemoteFiles();
-
- if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions, "delete-local-db"))
- {
- string dbpath;
- options.TryGetValue("dbpath", out dbpath);
-
- if (!string.IsNullOrWhiteSpace(dbpath) && System.IO.File.Exists(dbpath))
- System.IO.File.Delete(dbpath);
- }
- Program.DataConnection.DeleteBackup(backup);
- Program.Scheduler.Reschedule();
- return null;
- }
- case DuplicatiOperation.Vacuum:
- {
- var r = controller.Vacuum();
- UpdateMetadata(backup, r);
- return r;
- }
- default:
- //TODO: Log this
- return null;
- }
- }
- }
- catch (Exception ex)
- {
- Program.DataConnection.LogError(data.Backup.ID, string.Format("Failed while executing \"{0}\" with id: {1}", data.Operation, data.Backup.ID), ex);
- UpdateMetadataError(data.Backup, ex);
- Library.UsageReporter.Reporter.Report(ex);
-
- if (!fromQueue)
- throw;
-
- return null;
- }
- finally
- {
- ((RunnerData)data).Controller = null;
- }
- }
-
- private static Duplicati.Library.Utility.TempFolder StoreTaskConfigAndGetTempFolder(IRunnerData data, Dictionary options)
- {
- var all_tasks = string.Equals(options["store-task-config"], "all", StringComparison.OrdinalIgnoreCase) || string.Equals(options["store-task-config"], "*", StringComparison.OrdinalIgnoreCase);
- var this_task = Duplicati.Library.Utility.Utility.ParseBool(options["store-task-config"], false);
-
- options.Remove("store-task-config");
-
- Duplicati.Library.Utility.TempFolder tempfolder = null;
- if (all_tasks || this_task)
- {
- tempfolder = new Duplicati.Library.Utility.TempFolder();
- var temppath = System.IO.Path.Combine(tempfolder, "task-setup.json");
- using (var tempfile = Duplicati.Library.Utility.TempFile.WrapExistingFile(temppath))
- {
- object taskdata = null;
- if (all_tasks)
- taskdata = Program.DataConnection.Backups.Where(x => !x.IsTemporary).Select(x => Program.DataConnection.PrepareBackupForExport(Program.DataConnection.GetBackup(x.ID)));
- else
- taskdata = new[] { Program.DataConnection.PrepareBackupForExport(data.Backup) };
-
- using (var fs = System.IO.File.OpenWrite(tempfile))
- using (var sw = new System.IO.StreamWriter(fs, System.Text.Encoding.UTF8))
- Serializer.SerializeJson(sw, taskdata, true);
-
- tempfile.Protected = true;
-
- options.TryGetValue("control-files", out string controlfiles);
-
- if (string.IsNullOrWhiteSpace(controlfiles))
- controlfiles = tempfile;
- else
- controlfiles += System.IO.Path.PathSeparator + tempfile;
-
- options["control-files"] = controlfiles;
- }
- }
- return tempfolder;
- }
-
- private static void UpdateMetadataError(Duplicati.Server.Serialization.Interface.IBackup backup, Exception ex)
- {
- backup.Metadata["LastErrorDate"] = Library.Utility.Utility.SerializeDateTime(DateTime.UtcNow);
- backup.Metadata["LastErrorMessage"] = ex.Message;
-
- if (!backup.IsTemporary)
- Program.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null);
-
- string messageid = null;
- if (ex is UserInformationException exception)
- messageid = exception.HelpID;
-
- System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID);
- Program.DataConnection.RegisterNotification(
- NotificationType.Error,
- backup.IsTemporary ?
- "Error" : string.Format("Error while running {0}", backup.Name),
- ex.Message,
- ex,
- backup.ID,
- "backup:show-log",
- null,
- messageid,
- null,
- (n, a) => {
- return a.FirstOrDefault(x => x.BackupID == backup.ID) ?? n;
- }
- );
- }
-
- private static void UpdateMetadataLastCompact(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.ICompactResults r)
- {
- if (r != null)
- {
- backup.Metadata["LastCompactDuration"] = r.Duration.ToString();
- backup.Metadata["LastCompactStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime());
- backup.Metadata["LastCompactFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime());
- }
- }
-
- private static void UpdateMetadataLastVacuum(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IVacuumResults r)
- {
- if (r != null)
- {
- backup.Metadata["LastVacuumDuration"] = r.Duration.ToString();
- backup.Metadata["LastVacuumStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime());
- backup.Metadata["LastVacuumFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime());
- }
- }
-
- private static void UpdateMetadata(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IParsedBackendStatistics r)
- {
- if (r != null)
- {
- backup.Metadata["LastBackupDate"] = Library.Utility.Utility.SerializeDateTime(r.LastBackupDate.ToUniversalTime());
- backup.Metadata["BackupListCount"] = r.BackupListCount.ToString();
- backup.Metadata["TotalQuotaSpace"] = r.TotalQuotaSpace.ToString();
- backup.Metadata["FreeQuotaSpace"] = r.FreeQuotaSpace.ToString();
- backup.Metadata["AssignedQuotaSpace"] = r.AssignedQuotaSpace.ToString();
-
- backup.Metadata["TargetFilesSize"] = r.KnownFileSize.ToString();
- backup.Metadata["TargetFilesCount"] = r.KnownFileCount.ToString();
- backup.Metadata["TargetSizeString"] = Duplicati.Library.Utility.Utility.FormatSizeString(r.KnownFileSize);
- }
- }
-
- private static void UpdateMetadata(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IBasicResults result)
- {
- if (result is IRestoreResults r1)
- {
- backup.Metadata["LastRestoreDuration"] = r1.Duration.ToString();
- backup.Metadata["LastRestoreStarted"] = Library.Utility.Utility.SerializeDateTime(result.BeginTime.ToUniversalTime());
- backup.Metadata["LastRestoreFinished"] = Library.Utility.Utility.SerializeDateTime(result.EndTime.ToUniversalTime());
- }
-
- if (result is IParsedBackendStatistics r2)
- {
- UpdateMetadata(backup, r2);
- }
-
- if (result is IBackendStatsticsReporter r3)
- {
- if (r3.BackendStatistics is IParsedBackendStatistics statistics)
- UpdateMetadata(backup, statistics);
- }
-
- if (result is ICompactResults r4)
- {
- UpdateMetadataLastCompact(backup, r4);
-
- if (r4.VacuumResults != null)
- UpdateMetadataLastVacuum(backup, r4.VacuumResults);
- }
-
- if (result is IVacuumResults r5)
- {
- UpdateMetadataLastVacuum(backup, r5);
- }
-
- if (result is IBackupResults r)
- {
- backup.Metadata["SourceFilesSize"] = r.SizeOfExaminedFiles.ToString();
- backup.Metadata["SourceFilesCount"] = r.ExaminedFiles.ToString();
- backup.Metadata["SourceSizeString"] = Duplicati.Library.Utility.Utility.FormatSizeString(r.SizeOfExaminedFiles);
- backup.Metadata["LastBackupStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime());
- backup.Metadata["LastBackupFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime());
- backup.Metadata["LastBackupDuration"] = r.Duration.ToString();
-
- if (r.CompactResults != null)
- UpdateMetadataLastCompact(backup, r.CompactResults);
-
- if (r.VacuumResults != null)
- UpdateMetadataLastVacuum(backup, r.VacuumResults);
-
- if (r.FilesWithError > 0 || r.Warnings.Any() || r.Errors.Any())
- {
- string message;
- string titleType;
- if (r.FilesWithError > 0)
- {
- message = $"Errors affected {r.FilesWithError} file(s).";
- titleType = "Error";
- }
- else if (r.Errors.Any())
- {
- message = r.Errors.Count() == 1 ? r.Errors.Single() : $"Encountered {r.Errors.Count()} errors.";
- titleType = "Error";
- }
- else
- {
- message = r.Warnings.Count() == 1 ? r.Warnings.Single() : $"Encountered {r.Warnings.Count()} warnings.";
- titleType = "Warning";
- }
-
- Program.DataConnection.RegisterNotification(
- r.FilesWithError == 0 && !r.Errors.Any() ? NotificationType.Warning : NotificationType.Error,
- backup.IsTemporary ? "Warning" : $"{titleType} while running {backup.Name}",
- message,
- null,
- backup.ID,
- "backup:show-log",
- null,
- null,
- null,
- (n, a) =>
- {
- var existing = a.FirstOrDefault(x => x.BackupID == backup.ID);
- if (existing == null)
- return n;
-
- if (existing.Type == NotificationType.Error)
- return existing;
-
- return n;
- }
- );
- }
- }
- else if (result.ParsedResult != Library.Interface.ParsedResultType.Success)
- {
- var type = result.ParsedResult == Library.Interface.ParsedResultType.Warning
- ? NotificationType.Warning
- : NotificationType.Error;
-
- var title = result.ParsedResult == Library.Interface.ParsedResultType.Warning
- ? (backup.IsTemporary ?
- "Warning" : string.Format("Warning while running {0}", backup.Name))
- : (backup.IsTemporary ?
- "Error" : string.Format("Error while running {0}", backup.Name));
-
- var message = result.ParsedResult == Library.Interface.ParsedResultType.Warning
- ? string.Format("Got {0} warning(s)", result.Warnings.Count())
- : string.Format("Got {0} error(s)", result.Errors.Count());
-
- Program.DataConnection.RegisterNotification(
- type,
- title,
- message,
- null,
- backup.ID,
- "backup:show-log",
- null,
- null,
- "backup:show-log",
- (n, a) => n
- );
- }
-
- if (!backup.IsTemporary)
- Program.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null);
-
- System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID);
- Program.StatusEventNotifyer.SignalNewEvent();
- }
-
- private static bool TestIfOptionApplies()
- {
- //TODO: Implement to avoid warnings
- return true;
- }
-
- private static void DisableModule(string module, Dictionary options)
- {
- string disabledModules;
- string enabledModules;
-
- if (options.TryGetValue("enable-module", out enabledModules))
- {
- var emods = (enabledModules ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- options["enable-module"] = string.Join(",", emods.Where(x => module.Equals(x, StringComparison.OrdinalIgnoreCase)));
- }
-
- options.TryGetValue("disable-module", out disabledModules);
- var mods = (disabledModules ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
- options["disable-module"] = string.Join(",", mods.Union(new string[] { module }).Distinct(StringComparer.OrdinalIgnoreCase));
- }
-
- internal static Dictionary ApplyOptions(Duplicati.Server.Serialization.Interface.IBackup backup, Dictionary options)
- {
- options["backup-name"] = backup.Name;
- options["dbpath"] = backup.DBPath;
-
- // Apply normal options
- foreach(var o in backup.Settings)
- if (!o.Name.StartsWith("--", StringComparison.Ordinal) && TestIfOptionApplies())
- options[o.Name] = o.Value;
-
- // Apply override options
- foreach(var o in backup.Settings)
- if (o.Name.StartsWith("--", StringComparison.Ordinal) && TestIfOptionApplies())
- options[o.Name.Substring(2)] = o.Value;
-
-
- // The server hangs if the module is enabled as there is no console attached
- DisableModule("console-password-input", options);
-
- return options;
- }
-
- private static Library.Utility.IFilter ApplyFilter(Serialization.Interface.IBackup backup, Library.Utility.IFilter filter)
- {
- var f2 = backup.Filters;
- if (f2 != null && f2.Length > 0)
- {
- var nf =
- (from n in f2
- let exp =
- n.Expression.StartsWith("[", StringComparison.Ordinal) && n.Expression.EndsWith("]", StringComparison.Ordinal)
- ? SpecialFolders.ExpandEnvironmentVariablesRegexp(n.Expression)
- : SpecialFolders.ExpandEnvironmentVariables(n.Expression)
- orderby n.Order
- select (Library.Utility.IFilter)(new Library.Utility.FilterExpression(exp, n.Include)))
- .Aggregate((a, b) => Library.Utility.FilterExpression.Combine(a, b));
-
- filter = Library.Utility.FilterExpression.Combine(filter, nf);
- }
-
- return filter;
- }
-
- internal static Dictionary GetCommonOptions()
- {
- return
- (from n in Program.DataConnection.Settings
- where TestIfOptionApplies()
- select n).ToDictionary(k => k.Name.StartsWith("--", StringComparison.Ordinal) ? k.Name.Substring(2) : k.Name, k => k.Value);
- }
-
- private static Duplicati.Library.Utility.IFilter GetCommonFilter()
- {
- var filters = Program.DataConnection.Filters;
- if (filters == null || filters.Length == 0)
- return null;
-
- return
- (from n in filters
- orderby n.Order
- let exp = Environment.ExpandEnvironmentVariables(n.Expression)
- select (Duplicati.Library.Utility.IFilter)(new Duplicati.Library.Utility.FilterExpression(exp, n.Include)))
- .Aggregate((a, b) => Duplicati.Library.Utility.FilterExpression.Combine(a, b));
- }
- }
-}
-
+// 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.Server.Serialization;
+using Duplicati.Library.RestAPI;
+
+namespace Duplicati.Server
+{
+ public static class Runner
+ {
+ public interface IRunnerData : Duplicati.Server.Serialization.Interface.IQueuedTask
+ {
+ Duplicati.Server.Serialization.Interface.IBackup Backup { get; }
+ IDictionary ExtraOptions { get; }
+ string[] FilterStrings { get; }
+ void Stop(bool allowCurrentFileToFinish);
+ void Abort();
+ void Pause();
+ void Resume();
+ void UpdateThrottleSpeed();
+ void SetController(Duplicati.Library.Main.Controller controller);
+ }
+
+ private class RunnerData : IRunnerData
+ {
+ private static long RunnerTaskID = 1;
+
+ public Duplicati.Server.Serialization.DuplicatiOperation Operation { get; internal set; }
+ public Duplicati.Server.Serialization.Interface.IBackup Backup { get; internal set; }
+ public IDictionary ExtraOptions { get; internal set; }
+ public string[] FilterStrings { get; internal set; }
+
+ public string BackupID { get { return Backup.ID; } }
+ public long TaskID { get { return m_taskID; } }
+
+ internal Duplicati.Library.Main.Controller Controller { get; set; }
+
+ public void SetController(Duplicati.Library.Main.Controller controller)
+ {
+ Controller = controller;
+ }
+
+ public void Stop(bool allowCurrentFileToFinish)
+ {
+ var c = Controller;
+ if (c != null)
+ c.Stop(allowCurrentFileToFinish);
+ }
+
+ public void Abort()
+ {
+ var c = Controller;
+ if (c != null)
+ c.Abort();
+ }
+
+ public void Pause()
+ {
+ var c = Controller;
+ if (c != null)
+ c.Pause();
+ }
+
+ public void Resume()
+ {
+ var c = Controller;
+ if (c != null)
+ c.Resume();
+ }
+
+ public long OriginalUploadSpeed { get; set; }
+ public long OriginalDownloadSpeed { get; set; }
+
+ public void UpdateThrottleSpeed()
+ {
+ var controller = this.Controller;
+ if (controller == null)
+ return;
+
+ var job_upload_throttle = this.OriginalUploadSpeed <= 0 ? long.MaxValue : this.OriginalUploadSpeed;
+ var job_download_throttle = this.OriginalDownloadSpeed <= 0 ? long.MaxValue : this.OriginalDownloadSpeed;
+
+ var server_upload_throttle = long.MaxValue;
+ var server_download_throttle = long.MaxValue;
+
+ try
+ {
+ if (!string.IsNullOrWhiteSpace(FIXMEGlobal.DataConnection.ApplicationSettings.UploadSpeedLimit))
+ server_upload_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(FIXMEGlobal.DataConnection.ApplicationSettings.UploadSpeedLimit, "kb");
+ }
+ catch { }
+
+ try
+ {
+ if (!string.IsNullOrWhiteSpace(FIXMEGlobal.DataConnection.ApplicationSettings.DownloadSpeedLimit))
+ server_download_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(FIXMEGlobal.DataConnection.ApplicationSettings.DownloadSpeedLimit, "kb");
+ }
+ catch { }
+
+ var upload_throttle = Math.Min(job_upload_throttle, server_upload_throttle);
+ var download_throttle = Math.Min(job_download_throttle, server_download_throttle);
+
+ if (upload_throttle <= 0 || upload_throttle == long.MaxValue)
+ upload_throttle = 0;
+
+ if (download_throttle <= 0 || download_throttle == long.MaxValue)
+ download_throttle = 0;
+
+ controller.MaxUploadSpeed = upload_throttle;
+ controller.MaxDownloadSpeed = download_throttle;
+ }
+
+ private readonly long m_taskID;
+
+ public RunnerData()
+ {
+ m_taskID = System.Threading.Interlocked.Increment(ref RunnerTaskID);
+ }
+ }
+
+ private class CustomRunnerTask : RunnerData
+ {
+ public readonly Action Run;
+
+ public CustomRunnerTask(Action runner)
+ : base()
+ {
+ if (runner == null)
+ throw new ArgumentNullException(nameof(runner));
+ Run = runner;
+ Operation = DuplicatiOperation.CustomRunner;
+ Backup = new Database.Backup();
+ }
+ }
+
+ public static IRunnerData CreateCustomTask(Action