diff --git a/.appveyor.yml b/.appveyor.yml
deleted file mode 100644
index e1f6a2ba4..000000000
--- a/.appveyor.yml
+++ /dev/null
@@ -1,66 +0,0 @@
-version: 1.0.{build}
-image: Visual Studio 2019
-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..33fcf131b
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+*.csproj text
+*.sh text eol=lf
+#The #! in rules won't work if CRLF
+/Installer/debian/docker/debian/* text eol=lf
+/Installer/fedora/*.sh text eol=lf
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index be047521c..3d627b1dc 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -1,6 +1,6 @@
-
+
diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml
new file mode 100644
index 000000000..cdfce5be6
--- /dev/null
+++ b/.github/workflows/artifacts.yml
@@ -0,0 +1,105 @@
+name: Artifacts
+
+on:
+ push:
+ branches: "**net5**"
+ pull_request:
+ branches: "**net5**"
+
+jobs:
+ zip:
+ runs-on: ${{ matrix.cfg.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ cfg:
+ - { os: ubuntu-latest, runtime: linux-x64 }
+ - { os: windows-latest, runtime: win-x64 }
+ - { os: macos-latest, runtime: osx-x64 }
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: 6.0.x
+ - name: Restore dependencies
+ run: dotnet restore --runtime=${{ matrix.cfg.runtime }} Duplicati.sln
+ - name: Package
+ #TODO: investigate #if __WindowsGTK__ || ENABLE_GTK via -p:DefineConstants=ENABLE_GTK Maybe needed for GUI?
+ run: dotnet publish -c Release --runtime=${{ matrix.cfg.runtime }} -o publish Duplicati.sln
+ - name: Save Artifacts
+ uses: actions/upload-artifact@v2
+ with:
+ name: duplicati-${{ matrix.cfg.runtime }}
+ path: publish
+ deb:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Package
+ run: Installer/debian/build.sh
+ - name: Save Artifacts
+ uses: actions/upload-artifact@v2
+ with:
+ name: deb package
+ path: duplicati*.deb
+ rpm:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Package
+ run: Installer/fedora/build.sh
+ - name: Save Artifacts
+ uses: actions/upload-artifact@v2
+ with:
+ name: rpm package
+ path: duplicati*.rpm
+ msi:
+ runs-on: windows-latest
+ needs: zip
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Download zip
+ uses: actions/download-artifact@v2
+ with:
+ name: duplicati-win-x64
+ path: bin
+ - name: Install go-msi
+ run: choco install -y "go-msi"
+ - name: Prepare PATH
+ shell: bash
+ run: |
+ echo "$WIX\\bin" >> $GITHUB_PATH
+ echo "C:\\Program Files\\go-msi" >> $GITHUB_PATH
+ - name: Build MSI
+ id: buildmsi
+ shell: bash
+ run: |
+ Installer/Windows/MSI/build.sh
+ - name: Save Artifacts
+ uses: actions/upload-artifact@v2
+ with:
+ name: duplicati-win-x64-msi
+ path: duplicati-win-x64.msi
+ dmg:
+ runs-on: macos-latest
+ needs: zip
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Download zip
+ uses: actions/download-artifact@v2
+ with:
+ name: duplicati-osx-x64
+ path: bin
+ - name: Build DMG
+ shell: bash
+ run: |
+ Installer/OSX/dmg/build.sh
+ - name: Save Artifacts
+ uses: actions/upload-artifact@v2
+ with:
+ name: Duplicati.dmg
+ path: Duplicati.dmg
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/tests.yml b/.github/workflows/tests.yml
index 9e5d0c0b1..f140717aa 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@v1.8.0
+ with:
+ dotnet-version: 8.0.x
- - name: Install NuGet
- uses: nuget/setup-nuget@v1
+ - name: Checkout source
+ uses: actions/checkout@v2.3.4
- - 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@v2
+ - name: Selenium
+ run: pipeline/selenium/test.sh
diff --git a/.gitignore b/.gitignore
index 7ba91052e..20f24ea49 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,5 +23,10 @@ changelog-news.txt
binfiles.wxs
.idea/
.vs/
+/publish/
Installer/debian/*.buildinfo
+/*.deb
+/*.rpm
+
+ReleaseBuilder/build-temp/
\ 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..aa27c67b8
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,48 @@
+{
+ // 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": "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 5768330ff..000000000
--- a/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.csproj
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
- net8.0
- Exe
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.sln b/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.sln
deleted file mode 100644
index 3750b01e7..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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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 6d170e20e..000000000
--- a/BuildTools/AutoUpdateBuilder/Program.cs
+++ /dev/null
@@ -1,184 +0,0 @@
-// Copyright (C) 2024, The Duplicati Team
-// https://duplicati.com, hello@duplicati.com
-//
-// Permission is hereby granted, free of charge, to any person obtaining a
-// copy of this software and associated documentation files (the "Software"),
-// to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense,
-// and/or sell copies of the Software, and to permit persons to whom the
-// Software is furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-// DEALINGS IN THE SOFTWARE.
-
-using System;
-using System.Collections.Generic;
-using System.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/DependencyFinder/DependencyFinder.csproj b/BuildTools/DependencyFinder/DependencyFinder.csproj
index efad950d8..ef79e3a56 100644
--- a/BuildTools/DependencyFinder/DependencyFinder.csproj
+++ b/BuildTools/DependencyFinder/DependencyFinder.csproj
@@ -1,9 +1,19 @@
+
net8.0
Exe
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
\ No newline at end of file
diff --git a/BuildTools/DependencyFinder/DependencyFinder.sln b/BuildTools/DependencyFinder/DependencyFinder.sln
index cc68d58a9..85448817a 100644
--- a/BuildTools/DependencyFinder/DependencyFinder.sln
+++ b/BuildTools/DependencyFinder/DependencyFinder.sln
@@ -1,17 +1,17 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 2012
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyFinder", "DependencyFinder.csproj", "{3E891818-579A-4C06-8066-4C702BFA5921}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|x86 = Debug|x86
- Release|x86 = Release|x86
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {3E891818-579A-4C06-8066-4C702BFA5921}.Debug|x86.ActiveCfg = Debug|x86
- {3E891818-579A-4C06-8066-4C702BFA5921}.Debug|x86.Build.0 = Debug|x86
- {3E891818-579A-4C06-8066-4C702BFA5921}.Release|x86.ActiveCfg = Release|x86
- {3E891818-579A-4C06-8066-4C702BFA5921}.Release|x86.Build.0 = Release|x86
- EndGlobalSection
-EndGlobal
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2012
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyFinder", "DependencyFinder.csproj", "{3E891818-579A-4C06-8066-4C702BFA5921}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|x86 = Debug|x86
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {3E891818-579A-4C06-8066-4C702BFA5921}.Debug|x86.ActiveCfg = Debug|x86
+ {3E891818-579A-4C06-8066-4C702BFA5921}.Debug|x86.Build.0 = Debug|x86
+ {3E891818-579A-4C06-8066-4C702BFA5921}.Release|x86.ActiveCfg = Release|x86
+ {3E891818-579A-4C06-8066-4C702BFA5921}.Release|x86.Build.0 = Release|x86
+ EndGlobalSection
+EndGlobal
diff --git a/BuildTools/DependencyFinder/Program.cs b/BuildTools/DependencyFinder/Program.cs
index a007f71db..a11daf0bd 100644
--- a/BuildTools/DependencyFinder/Program.cs
+++ b/BuildTools/DependencyFinder/Program.cs
@@ -60,7 +60,7 @@ namespace DependencyFinder
//Console.WriteLine("Probing for {0}", dllname);
try { return AssemblyDefinition.ReadAssembly(dllname); }
catch { }
- try { return resolver.Resolve(asm.FullName); }
+ try { return resolver.Resolve(asm); }
catch { }
return null;
diff --git a/BuildTools/GnupgSigningTool/GnupgSigningTool.csproj b/BuildTools/GnupgSigningTool/GnupgSigningTool.csproj
index 6a24a8c18..0e7751830 100644
--- a/BuildTools/GnupgSigningTool/GnupgSigningTool.csproj
+++ b/BuildTools/GnupgSigningTool/GnupgSigningTool.csproj
@@ -1,11 +1,21 @@
+
net8.0
Exe
+
+
-
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
\ No newline at end of file
diff --git a/BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj b/BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj
index b70477ad5..e7bd3a791 100644
--- a/BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj
+++ b/BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj
@@ -1,10 +1,19 @@
+
net8.0
Exe
+
-
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
\ No newline at end of file
diff --git a/Duplicati/Server/Database/Backup.cs b/Duplicati.Library.RestAPI/Database/Backup.cs
similarity index 77%
rename from Duplicati/Server/Database/Backup.cs
rename to Duplicati.Library.RestAPI/Database/Backup.cs
index 33b2671f3..117253942 100644
--- a/Duplicati/Server/Database/Backup.cs
+++ b/Duplicati.Library.RestAPI/Database/Backup.cs
@@ -1,24 +1,20 @@
-// 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.
+// 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 Duplicati.Server.Serialization.Interface;
using System.Collections.Generic;
diff --git a/Duplicati/Server/Database/Connection.cs b/Duplicati.Library.RestAPI/Database/Connection.cs
similarity index 91%
rename from Duplicati/Server/Database/Connection.cs
rename to Duplicati.Library.RestAPI/Database/Connection.cs
index ff9971ee7..fc47340ef 100644
--- a/Duplicati/Server/Database/Connection.cs
+++ b/Duplicati.Library.RestAPI/Database/Connection.cs
@@ -1,1265 +1,1262 @@
-// 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 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.Library.RestAPI/Database/Filter.cs b/Duplicati.Library.RestAPI/Database/Filter.cs
new file mode 100644
index 000000000..7f14e2bda
--- /dev/null
+++ b/Duplicati.Library.RestAPI/Database/Filter.cs
@@ -0,0 +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
+using System;
+
+namespace Duplicati.Server.Database
+{
+ public class Filter : Duplicati.Server.Serialization.Interface.IFilter
+ {
+ public long Order { get; set; }
+
+ public bool Include { get; set; }
+
+ public string Expression { get; set; }
+ }
+}
+
diff --git a/Duplicati.Library.RestAPI/Database/Notification.cs b/Duplicati.Library.RestAPI/Database/Notification.cs
new file mode 100644
index 000000000..22b507701
--- /dev/null
+++ b/Duplicati.Library.RestAPI/Database/Notification.cs
@@ -0,0 +1,39 @@
+// 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;
+
+namespace Duplicati.Server.Database
+{
+ public class Notification : Server.Serialization.Interface.INotification
+ {
+ #region INotification implementation
+ public long ID { get; set; }
+ public Duplicati.Server.Serialization.NotificationType Type { get; set; }
+ public string Title { get; set; }
+ public string Message { get; set; }
+ public string Exception { get; set; }
+ public string BackupID { get; set; }
+ public string Action { get; set; }
+ public DateTime Timestamp { get; set; }
+ public string LogEntryID { get; set; }
+ public string MessageID { get; set; }
+ public string MessageLogTag { get; set; }
+ #endregion
+ }
+}
+
diff --git a/Duplicati/Server/Database/Schedule.cs b/Duplicati.Library.RestAPI/Database/Schedule.cs
similarity index 65%
rename from Duplicati/Server/Database/Schedule.cs
rename to Duplicati.Library.RestAPI/Database/Schedule.cs
index c0ebb77d8..7df817d40 100644
--- a/Duplicati/Server/Database/Schedule.cs
+++ b/Duplicati.Library.RestAPI/Database/Schedule.cs
@@ -1,24 +1,20 @@
-// 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.
+// 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 Duplicati.Server.Serialization.Interface;
diff --git a/Duplicati/Server/Database/ServerSettings.cs b/Duplicati.Library.RestAPI/Database/ServerSettings.cs
similarity index 89%
rename from Duplicati/Server/Database/ServerSettings.cs
rename to Duplicati.Library.RestAPI/Database/ServerSettings.cs
index f21f0ea3d..92b02bcfa 100644
--- a/Duplicati/Server/Database/ServerSettings.cs
+++ b/Duplicati.Library.RestAPI/Database/ServerSettings.cs
@@ -1,567 +1,564 @@
-// 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 System.Security.Cryptography;
-using System.Security.Cryptography.X509Certificates;
-using Duplicati.Library.Common;
-
-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 bool HasAskedForPasswordProtection
- {
- get
- {
- return Duplicati.Library.Utility.Utility.ParseBoolOption(settings, CONST.HAS_ASKED_FOR_PASSWORD_PROTECTION);
- }
- set
- {
- lock (databaseConnection.m_lock)
- settings[CONST.HAS_ASKED_FOR_PASSWORD_PROTECTION] = value.ToString();
- 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();
- 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();
- }
- }
- }
-}
-
+// 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;
+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);
+
+ 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);
+ }
+ set
+ {
+ lock (databaseConnection.m_lock)
+ settings[CONST.HAS_ASKED_FOR_PASSWORD_PROTECTION] = value.ToString();
+ 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.Library.RestAPI/Database/Setting.cs b/Duplicati.Library.RestAPI/Database/Setting.cs
new file mode 100644
index 000000000..84150bf1a
--- /dev/null
+++ b/Duplicati.Library.RestAPI/Database/Setting.cs
@@ -0,0 +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
+using System;
+
+namespace Duplicati.Server.Database
+{
+ public class Setting : Duplicati.Server.Serialization.Implementations.Setting
+ {
+ }
+}
+
diff --git a/Duplicati.Library.RestAPI/Database/TempFile.cs b/Duplicati.Library.RestAPI/Database/TempFile.cs
new file mode 100644
index 000000000..b652a2920
--- /dev/null
+++ b/Duplicati.Library.RestAPI/Database/TempFile.cs
@@ -0,0 +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
+using System;
+
+namespace Duplicati.Server.Database
+{
+ public class TempFile
+ {
+ public long ID { get; set; }
+ public string Origin { get; set; }
+ public string Path { get; set; }
+ public DateTime Timestamp { get; set; }
+ public DateTime Expires { get; set; }
+ }
+}
+
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 66%
rename from Duplicati/Server/EventPollNotify.cs
rename to Duplicati.Library.RestAPI/EventPollNotify.cs
index fcaa5209e..777dc4a1e 100644
--- a/Duplicati/Server/EventPollNotify.cs
+++ b/Duplicati.Library.RestAPI/EventPollNotify.cs
@@ -1,97 +1,77 @@
-// 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.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 88%
rename from Duplicati/Server/LiveControls.cs
rename to Duplicati.Library.RestAPI/LiveControls.cs
index 9ff6dba3a..1a51636a3 100644
--- a/Duplicati/Server/LiveControls.cs
+++ b/Duplicati.Library.RestAPI/LiveControls.cs
@@ -1,405 +1,404 @@
-// Copyright (C) 2024, The Duplicati Team
-// https://duplicati.com, hello@duplicati.com
+#region Disclaimer / License
+// Copyright (C) 2015, The Duplicati Team
+// http://www.duplicati.com, info@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:
+// 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.
//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-// DEALINGS IN THE SOFTWARE.
-
-using System;
-using System.Collections.Generic;
-using System.Text;
+// 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 87%
rename from Duplicati/Server/LogWriteHandler.cs
rename to Duplicati.Library.RestAPI/LogWriteHandler.cs
index c6b605abb..640886f66 100644
--- a/Duplicati/Server/LogWriteHandler.cs
+++ b/Duplicati.Library.RestAPI/LogWriteHandler.cs
@@ -1,24 +1,20 @@
-// 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.
+// 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 Duplicati.Library.Logging;
diff --git a/Duplicati/Server/WebServer/RESTMethods/Acknowledgements.cs b/Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs
similarity index 97%
rename from Duplicati/Server/WebServer/RESTMethods/Acknowledgements.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs
index 12b45e612..f9b558c5b 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Acknowledgements.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs
@@ -18,7 +18,6 @@
// 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;
diff --git a/Duplicati/Server/WebServer/RESTMethods/Backup.cs b/Duplicati.Library.RestAPI/RESTMethods/Backup.cs
similarity index 87%
rename from Duplicati/Server/WebServer/RESTMethods/Backup.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Backup.cs
index dd06d9a45..e18730fea 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Backup.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Backup.cs
@@ -1,737 +1,742 @@
-// 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;
-
-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 90%
rename from Duplicati/Server/WebServer/RESTMethods/BackupDefaults.cs
rename to Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs
index 6ab403cff..4d6675520 100644
--- a/Duplicati/Server/WebServer/RESTMethods/BackupDefaults.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs
@@ -1,136 +1,141 @@
-// 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.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;
- }
- }
- }
- }
- }
-}
-
+// Copyright (C) 2024, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+using System;
+using System.Linq;
+using Duplicati.Library.Common.IO;
+using Duplicati.Library.RestAPI;
+using Duplicati.Library.Utility;
+
+namespace Duplicati.Server.WebServer.RESTMethods
+{
+ public class BackupDefaults : IRESTMethodGET
+ {
+
+ private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType();
+
+ 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 86%
rename from Duplicati/Server/WebServer/RESTMethods/Backups.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Backups.cs
index 6476fba79..bae68fa57 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Backups.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Backups.cs
@@ -1,304 +1,310 @@
-// 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;
-
-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/Server/WebServer/RESTMethods/BugReport.cs b/Duplicati.Library.RestAPI/RESTMethods/BugReport.cs
similarity index 92%
rename from Duplicati/Server/WebServer/RESTMethods/BugReport.cs
rename to Duplicati.Library.RestAPI/RESTMethods/BugReport.cs
index cf5e8ca66..388a32aad 100644
--- a/Duplicati/Server/WebServer/RESTMethods/BugReport.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/BugReport.cs
@@ -18,7 +18,7 @@
// 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;
@@ -31,7 +31,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
long id;
long.TryParse(key, out id);
- var tf = Program.DataConnection.GetTempFiles().FirstOrDefault(x => x.ID == 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);
diff --git a/Duplicati/Server/WebServer/RESTMethods/Captcha.cs b/Duplicati.Library.RestAPI/RESTMethods/Captcha.cs
similarity index 95%
rename from Duplicati/Server/WebServer/RESTMethods/Captcha.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Captcha.cs
index a9c0604c2..93345c8f8 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Captcha.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Captcha.cs
@@ -1,171 +1,172 @@
-// 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;
- 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 93%
rename from Duplicati/Server/WebServer/RESTMethods/Changelog.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Changelog.cs
index d991f813d..7a9712562 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Changelog.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Changelog.cs
@@ -18,7 +18,7 @@
// 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;
@@ -47,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 90%
rename from Duplicati/Server/WebServer/RESTMethods/CommandLine.cs
rename to Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs
index 9553a29a5..e4ff87db9 100644
--- a/Duplicati/Server/WebServer/RESTMethods/CommandLine.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs
@@ -1,288 +1,293 @@
-// 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.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 97%
rename from Duplicati/Server/WebServer/RESTMethods/Filesystem.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs
index 8845a7010..cc77df60e 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Filesystem.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs
@@ -18,7 +18,6 @@
// 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;
diff --git a/Duplicati/Server/WebServer/RESTMethods/Help.cs b/Duplicati.Library.RestAPI/RESTMethods/Help.cs
similarity index 97%
rename from Duplicati/Server/WebServer/RESTMethods/Help.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Help.cs
index 0cd860dc2..b409b7f77 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Help.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Help.cs
@@ -18,7 +18,6 @@
// 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 97%
rename from Duplicati/Server/WebServer/RESTMethods/HyperV.cs
rename to Duplicati.Library.RestAPI/RESTMethods/HyperV.cs
index d2eef2d4e..b957cfd48 100644
--- a/Duplicati/Server/WebServer/RESTMethods/HyperV.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/HyperV.cs
@@ -18,7 +18,6 @@
// 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;
diff --git a/Duplicati/Server/WebServer/RESTMethods/IRESTMethod.cs b/Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs
similarity index 97%
rename from Duplicati/Server/WebServer/RESTMethods/IRESTMethod.cs
rename to Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs
index 36b0d7ff8..488f4ce1d 100644
--- a/Duplicati/Server/WebServer/RESTMethods/IRESTMethod.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs
@@ -18,7 +18,6 @@
// 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;
diff --git a/Duplicati/Server/WebServer/RESTMethods/Licenses.cs b/Duplicati.Library.RestAPI/RESTMethods/Licenses.cs
similarity index 68%
rename from Duplicati/Server/WebServer/RESTMethods/Licenses.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Licenses.cs
index 289d7f379..39bd5d754 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Licenses.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Licenses.cs
@@ -18,8 +18,8 @@
// 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
{
@@ -27,7 +27,16 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
public void GET(string key, RequestInfo info)
{
- var path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(Duplicati.Library.Utility.Utility.getEntryAssembly().Location), "licenses");
+ 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 93%
rename from Duplicati/Server/WebServer/RESTMethods/LogData.cs
rename to Duplicati.Library.RestAPI/RESTMethods/LogData.cs
index c0d10001c..1b78b4898 100644
--- a/Duplicati/Server/WebServer/RESTMethods/LogData.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/LogData.cs
@@ -18,9 +18,9 @@
// 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
{
@@ -46,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 97%
rename from Duplicati/Server/WebServer/RESTMethods/MSSQL.cs
rename to Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs
index 944bf806b..be873efdb 100644
--- a/Duplicati/Server/WebServer/RESTMethods/MSSQL.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs
@@ -18,7 +18,6 @@
// 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;
diff --git a/Duplicati/Server/WebServer/RESTMethods/Notification.cs b/Duplicati.Library.RestAPI/RESTMethods/Notification.cs
similarity index 86%
rename from Duplicati/Server/WebServer/RESTMethods/Notification.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Notification.cs
index e9174695b..24199cf0b 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Notification.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Notification.cs
@@ -18,7 +18,7 @@
// 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;
@@ -35,7 +35,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
return;
}
- var el = Program.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id);
+ var el = FIXMEGlobal.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id);
if (el == null)
info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound);
else
@@ -51,12 +51,12 @@ namespace Duplicati.Server.WebServer.RESTMethods
return;
}
- var el = Program.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id);
+ var el = FIXMEGlobal.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id);
if (el == null)
info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound);
else
{
- Program.DataConnection.DismissNotification(id);
+ FIXMEGlobal.DataConnection.DismissNotification(id);
info.OutputOK();
}
}
diff --git a/Duplicati/Server/WebServer/RESTMethods/Notifications.cs b/Duplicati.Library.RestAPI/RESTMethods/Notifications.cs
similarity index 90%
rename from Duplicati/Server/WebServer/RESTMethods/Notifications.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Notifications.cs
index 9962abf18..3e4bb7b8b 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Notifications.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Notifications.cs
@@ -18,7 +18,7 @@
// 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
@@ -27,7 +27,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
public void GET(string key, RequestInfo info)
{
- info.OutputOK(Program.DataConnection.GetNotifications());
+ info.OutputOK(FIXMEGlobal.DataConnection.GetNotifications());
}
}
}
diff --git a/Duplicati/Server/WebServer/RESTMethods/ProgressState.cs b/Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs
similarity index 90%
rename from Duplicati/Server/WebServer/RESTMethods/ProgressState.cs
rename to Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs
index 18703e2c5..e85974dd4 100644
--- a/Duplicati/Server/WebServer/RESTMethods/ProgressState.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs
@@ -18,7 +18,7 @@
// 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;
@@ -28,10 +28,10 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
public void GET(string key, RequestInfo info)
{
- if (Program.GenerateProgressState == null)
+ if (FIXMEGlobal.GenerateProgressState == null)
info.ReportClientError("No active backup", System.Net.HttpStatusCode.NotFound);
else
- info.OutputOK(Program.GenerateProgressState());
+ info.OutputOK(FIXMEGlobal.GenerateProgressState());
}
public string Description { get { return "Return the progress of the currently running operation."; } }
diff --git a/Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs b/Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs
similarity index 64%
rename from Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs
rename to Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs
index 5c32ad896..f9a8ded5e 100644
--- a/Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs
@@ -23,6 +23,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
+using Duplicati.Library.Interface;
namespace Duplicati.Server.WebServer.RESTMethods
{
@@ -37,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()))
@@ -51,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();
}
}
@@ -65,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)
{
@@ -119,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 97%
rename from Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs
rename to Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs
index 5258a871d..2069b6cf9 100644
--- a/Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs
@@ -18,7 +18,6 @@
// 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
diff --git a/Duplicati/Server/WebServer/RESTMethods/ServerSetting.cs b/Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs
similarity index 85%
rename from Duplicati/Server/WebServer/RESTMethods/ServerSetting.cs
rename to Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs
index 74a92def2..418589837 100644
--- a/Duplicati/Server/WebServer/RESTMethods/ServerSetting.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs
@@ -18,7 +18,7 @@
// 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;
@@ -37,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
@@ -52,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));
}
}
@@ -72,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)
@@ -80,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);
}
@@ -93,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 86%
rename from Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs
rename to Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs
index 40c9d81c8..77258f307 100644
--- a/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs
@@ -1,131 +1,136 @@
-// 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;
-
-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 89%
rename from Duplicati/Server/WebServer/RESTMethods/ServerState.cs
rename to Duplicati.Library.RestAPI/RESTMethods/ServerState.cs
index 2bc6939b4..bd1fa914a 100644
--- a/Duplicati/Server/WebServer/RESTMethods/ServerState.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/ServerState.cs
@@ -18,7 +18,7 @@
// 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;
@@ -32,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();
@@ -64,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 81%
rename from Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs
rename to Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs
index 7a0c57686..3c84ad5c6 100644
--- a/Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs
@@ -18,16 +18,16 @@
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
-
using System;
using System.Linq;
using System.Collections.Generic;
using Duplicati.Library.Interface;
using Duplicati.Library.Common;
+using Duplicati.Library.RestAPI;
namespace Duplicati.Server.WebServer.RESTMethods
{
- public class SystemInfo : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented
+ public class SystemInfo : IRESTMethodGET, IRESTMethodDocumented
{
public string Description { get { return "Gets various system properties"; } }
public IEnumerable> Types
@@ -45,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;
@@ -76,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,
@@ -85,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(),
@@ -107,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/Server/WebServer/RESTMethods/SystemWideSettings.cs b/Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs
similarity index 97%
rename from Duplicati/Server/WebServer/RESTMethods/SystemWideSettings.cs
rename to Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs
index b2cc4369a..09eebe706 100644
--- a/Duplicati/Server/WebServer/RESTMethods/SystemWideSettings.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs
@@ -18,7 +18,6 @@
// 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;
diff --git a/Duplicati/Server/WebServer/RESTMethods/Tags.cs b/Duplicati.Library.RestAPI/RESTMethods/Tags.cs
similarity index 90%
rename from Duplicati/Server/WebServer/RESTMethods/Tags.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Tags.cs
index c1deecce8..90333250a 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Tags.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Tags.cs
@@ -18,7 +18,7 @@
// 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;
@@ -39,7 +39,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
select n.Key.ToLower(CultureInfo.InvariantCulture);
// Append all known tags
- r = r.Union(from n in Program.DataConnection.Backups select n.Tags into p from x in p select x.ToLower(CultureInfo.InvariantCulture));
+ 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);
}
diff --git a/Duplicati/Server/WebServer/RESTMethods/Task.cs b/Duplicati.Library.RestAPI/RESTMethods/Task.cs
similarity index 89%
rename from Duplicati/Server/WebServer/RESTMethods/Task.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Task.cs
index 7b0960df0..1af624e61 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Task.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Task.cs
@@ -18,96 +18,96 @@
// 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 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);
- }
- }
-}
-
+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/Server/WebServer/RESTMethods/Tasks.cs b/Duplicati.Library.RestAPI/RESTMethods/Tasks.cs
similarity index 88%
rename from Duplicati/Server/WebServer/RESTMethods/Tasks.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Tasks.cs
index 84d4b9a15..cd900c522 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Tasks.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Tasks.cs
@@ -18,7 +18,7 @@
// 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;
@@ -28,8 +28,8 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
public void GET(string key, RequestInfo info)
{
- var cur = Program.WorkThread.CurrentTask;
- var n = Program.WorkThread.CurrentTasks;
+ var cur = FIXMEGlobal.WorkThread.CurrentTask;
+ var n = FIXMEGlobal.WorkThread.CurrentTasks;
if (cur != null)
n.Insert(0, cur);
diff --git a/Duplicati/Server/WebServer/RESTMethods/UISettings.cs b/Duplicati.Library.RestAPI/RESTMethods/UISettings.cs
similarity index 86%
rename from Duplicati/Server/WebServer/RESTMethods/UISettings.cs
rename to Duplicati.Library.RestAPI/RESTMethods/UISettings.cs
index 440aa9825..37330516d 100644
--- a/Duplicati/Server/WebServer/RESTMethods/UISettings.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/UISettings.cs
@@ -18,11 +18,11 @@
// 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
{
@@ -32,11 +32,11 @@ namespace Duplicati.Server.WebServer.RESTMethods
{
if (string.IsNullOrWhiteSpace(key))
{
- info.OutputOK(Program.DataConnection.GetUISettingsSchemes());
+ info.OutputOK(FIXMEGlobal.DataConnection.GetUISettingsSchemes());
}
else
{
- info.OutputOK(Program.DataConnection.GetUISettings(key));
+ info.OutputOK(FIXMEGlobal.DataConnection.GetUISettings(key));
}
}
@@ -71,9 +71,9 @@ namespace Duplicati.Server.WebServer.RESTMethods
}
if (info.Request.Method == "POST")
- Program.DataConnection.SetUISettings(key, data);
+ FIXMEGlobal.DataConnection.SetUISettings(key, data);
else
- Program.DataConnection.UpdateUISettings(key, data);
+ FIXMEGlobal.DataConnection.UpdateUISettings(key, data);
info.OutputOK();
}
diff --git a/Duplicati/Server/WebServer/RESTMethods/Updates.cs b/Duplicati.Library.RestAPI/RESTMethods/Updates.cs
similarity index 69%
rename from Duplicati/Server/WebServer/RESTMethods/Updates.cs
rename to Duplicati.Library.RestAPI/RESTMethods/Updates.cs
index 34b782e14..94abc4491 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Updates.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/Updates.cs
@@ -18,7 +18,7 @@
// 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
@@ -30,26 +30,9 @@ namespace Duplicati.Server.WebServer.RESTMethods
switch ((key ?? "").ToLowerInvariant())
{
case "check":
- Program.UpdatePoller.CheckNow();
+ FIXMEGlobal.UpdatePoller.CheckNow();
info.OutputOK();
return;
-
- case "install":
- Program.UpdatePoller.InstallUpdate();
- info.OutputOK();
- return;
-
- case "activate":
- if (Program.WorkThread.CurrentTask != null || Program.WorkThread.CurrentTasks.Count != 0)
- {
- info.ReportServerError("Cannot activate update while task is running or scheduled");
- }
- else
- {
- Program.UpdatePoller.ActivateUpdate();
- info.OutputOK();
- }
- return;
default:
info.ReportClientError("No such action", System.Net.HttpStatusCode.NotFound);
diff --git a/Duplicati/Server/WebServer/RESTMethods/WebModule.cs b/Duplicati.Library.RestAPI/RESTMethods/WebModule.cs
similarity index 97%
rename from Duplicati/Server/WebServer/RESTMethods/WebModule.cs
rename to Duplicati.Library.RestAPI/RESTMethods/WebModule.cs
index 09f8bc970..676b3c963 100644
--- a/Duplicati/Server/WebServer/RESTMethods/WebModule.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/WebModule.cs
@@ -18,7 +18,6 @@
// 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;
diff --git a/Duplicati/Server/WebServer/RESTMethods/WebModules.cs b/Duplicati.Library.RestAPI/RESTMethods/WebModules.cs
similarity index 97%
rename from Duplicati/Server/WebServer/RESTMethods/WebModules.cs
rename to Duplicati.Library.RestAPI/RESTMethods/WebModules.cs
index 536c33800..9f90d91db 100644
--- a/Duplicati/Server/WebServer/RESTMethods/WebModules.cs
+++ b/Duplicati.Library.RestAPI/RESTMethods/WebModules.cs
@@ -18,7 +18,6 @@
// 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;
diff --git a/Duplicati/Server/Runner.cs b/Duplicati.Library.RestAPI/Runner.cs
similarity index 86%
rename from Duplicati/Server/Runner.cs
rename to Duplicati.Library.RestAPI/Runner.cs
index 2cd9a3e90..544bb3f92 100644
--- a/Duplicati/Server/Runner.cs
+++ b/Duplicati.Library.RestAPI/Runner.cs
@@ -18,953 +18,951 @@
// 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;
-
-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} \"{1}\" (id: {2})", data.Operation, data.Backup.Name, 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 && !result.Interrupted)
- {
- UpdateMetadata(backup, r2);
- }
-
- if (result is IBackendStatsticsReporter r3 && !result.Interrupted)
- {
- if (r3.BackendStatistics is IParsedBackendStatistics statistics)
- UpdateMetadata(backup, statistics);
- }
-
- if (result is ICompactResults r4 && !result.Interrupted)
- {
- UpdateMetadataLastCompact(backup, r4);
-
- if (r4.VacuumResults != null)
- UpdateMetadataLastVacuum(backup, r4.VacuumResults);
- }
-
- if (result is IVacuumResults r5 && !result.Interrupted)
- {
- UpdateMetadataLastVacuum(backup, r5);
- }
-
- if (result is IBackupResults r)
- {
- if (!result.Interrupted)
- {
- 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 != ParsedResultType.Success)
- {
- var type = result.ParsedResult == ParsedResultType.Warning
- ? NotificationType.Warning
- : NotificationType.Error;
-
- var title = result.ParsedResult == 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 == 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));
- }
- }
-}
-
+
+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 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 = FIXMEGlobal.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.INSTALLATIONDIR,
+ Library.AutoUpdater.PackageHelper.GetExecutableName(Library.AutoUpdater.PackageHelper.NamedExecutable.CommandLine)
+ );
+
+ var cmd = new System.Text.StringBuilder();
+ 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 = FIXMEGlobal.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);
+ FIXMEGlobal.GenerateProgressState = sink.Copy;
+ FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
+
+ task.Run(sink);
+ }
+ catch (Exception ex)
+ {
+ FIXMEGlobal.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)
+ {
+ FIXMEGlobal.GenerateProgressState = () => sink.Copy();
+ FIXMEGlobal.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 = FIXMEGlobal.DataConnection.RegisterTempFile("create-bug-report", r.TargetPath, DateTime.Now.AddDays(3));
+
+ if (string.Equals(tf, r.TargetPath, Library.Utility.Utility.ClientFilenameStringComparison))
+ tf.Protected = true;
+
+ FIXMEGlobal.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);
+ }
+ FIXMEGlobal.DataConnection.DeleteBackup(backup);
+ FIXMEGlobal.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)
+ {
+ FIXMEGlobal.DataConnection.LogError(data.Backup.ID, string.Format("Failed while executing {0} \"{1}\" (id: {2})", data.Operation, data.Backup.Name, 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 = FIXMEGlobal.DataConnection.Backups.Where(x => !x.IsTemporary).Select(x => FIXMEGlobal.DataConnection.PrepareBackupForExport(FIXMEGlobal.DataConnection.GetBackup(x.ID)));
+ else
+ taskdata = new[] { FIXMEGlobal.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)
+ FIXMEGlobal.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null);
+
+ string messageid = null;
+ if (ex is UserInformationException exception)
+ messageid = exception.HelpID;
+
+ FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.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 && !result.Interrupted)
+ {
+ UpdateMetadata(backup, r2);
+ }
+
+ if (result is IBackendStatsticsReporter r3 && !result.Interrupted)
+ {
+ if (r3.BackendStatistics is IParsedBackendStatistics statistics)
+ UpdateMetadata(backup, statistics);
+ }
+
+ if (result is ICompactResults r4 && !result.Interrupted)
+ {
+ UpdateMetadataLastCompact(backup, r4);
+
+ if (r4.VacuumResults != null)
+ UpdateMetadataLastVacuum(backup, r4.VacuumResults);
+ }
+
+ if (result is IVacuumResults r5 && !result.Interrupted)
+ {
+ UpdateMetadataLastVacuum(backup, r5);
+ }
+
+ if (result is IBackupResults r)
+ {
+ if (!result.Interrupted)
+ {
+ 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";
+ }
+
+ FIXMEGlobal.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 != ParsedResultType.Success)
+ {
+ var type = result.ParsedResult == ParsedResultType.Warning
+ ? NotificationType.Warning
+ : NotificationType.Error;
+
+ var title = result.ParsedResult == 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 == ParsedResultType.Warning
+ ? string.Format("Got {0} warning(s)", result.Warnings.Count())
+ : string.Format("Got {0} error(s)", result.Errors.Count());
+
+ FIXMEGlobal.DataConnection.RegisterNotification(
+ type,
+ title,
+ message,
+ null,
+ backup.ID,
+ "backup:show-log",
+ null,
+ null,
+ "backup:show-log",
+ (n, a) => n
+ );
+ }
+
+ if (!backup.IsTemporary)
+ FIXMEGlobal.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null);
+
+ FIXMEGlobal.IncrementLastDataUpdateID();
+ FIXMEGlobal.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;
+ options["backup-id"] = $"DB-{backup.ID}";
+
+ // 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 FIXMEGlobal.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 = FIXMEGlobal.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));
+ }
+ }
+}
+
diff --git a/Duplicati/Server/Scheduler.cs b/Duplicati.Library.RestAPI/Scheduler.cs
similarity index 88%
rename from Duplicati/Server/Scheduler.cs
rename to Duplicati.Library.RestAPI/Scheduler.cs
index 2b4d46a9e..81a8cdf41 100644
--- a/Duplicati/Server/Scheduler.cs
+++ b/Duplicati.Library.RestAPI/Scheduler.cs
@@ -1,409 +1,413 @@
-// Copyright (C) 2024, The Duplicati Team
-// https://duplicati.com, hello@duplicati.com
+#region Disclaimer / License
+// Copyright (C) 2015, The Duplicati Team
+// http://www.duplicati.com, info@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:
+// 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.
//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-// DEALINGS IN THE SOFTWARE.
-
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Linq;
-using System.Threading;
-using Duplicati.Library.Utility;
using Duplicati.Server.Serialization.Interface;
-namespace Duplicati.Server
-{
- ///
- /// This class handles scheduled runs of backups
- ///
- public class Scheduler
- {
- private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType();
-
- ///
- /// The thread that runs the scheduler
- ///
- private readonly Thread m_thread;
- ///
- /// A termination flag
- ///
- private volatile bool m_terminate;
- ///
- /// The worker thread that is invoked to do work
- ///
- private readonly WorkerThread m_worker;
- ///
- /// The wait event
- ///
- private readonly AutoResetEvent m_event;
- ///
- /// The data synchronization lock
- ///
- private readonly object m_lock = new object();
-
- ///
- /// An event that is raised when the schedule changes
- ///
- public event EventHandler NewSchedule;
-
- ///
- /// The currently scheduled items
- ///
- private KeyValuePair[] m_schedule;
-
- ///
- /// List of update tasks, used to set the timestamp on the schedule once completed
- ///
- private readonly Dictionary> m_updateTasks;
-
- ///
- /// Constructs a new scheduler
- ///
- /// The worker thread
- public Scheduler(WorkerThread worker)
- {
- m_thread = new Thread(new ThreadStart(Runner));
- m_worker = worker;
- m_worker.CompletedWork += OnCompleted;
- m_worker.StartingWork += OnStartingWork;
- m_schedule = new KeyValuePair[0];
- m_terminate = false;
- m_event = new AutoResetEvent(false);
- m_updateTasks = new Dictionary>();
- m_thread.IsBackground = true;
- m_thread.Name = "TaskScheduler";
- m_thread.Start();
- }
-
- ///
- /// Forces the scheduler to re-evaluate the order.
- /// Call this method if something changes
- ///
- public void Reschedule()
- {
- m_event.Set();
- }
-
- ///
- /// A snapshot copy of the current schedule list
- ///
- public List> Schedule
- {
- get
- {
- lock (m_lock)
- return m_schedule.ToList();
- }
- }
-
- ///
- /// A snapshot copy of the current worker queue, that is items that are scheduled, but waiting for execution
- ///
- public List WorkerQueue
- {
- get
- {
- return (from t in m_worker.CurrentTasks where t != null select t).ToList();
- }
- }
-
- ///
- /// Terminates the thread. Any items still in queue will be removed
- ///
- /// True if the call should block until the thread has exited, false otherwise
- public void Terminate(bool wait)
- {
- m_terminate = true;
- m_event.Set();
-
- if (wait)
- m_thread.Join();
- }
-
- ///
- /// Returns the next valid date, given the start and the interval
- ///
- /// The base time
- /// The first allowed date
- /// The repetition interval
- /// The days the backup is allowed to run
- /// The next valid date, or throws an exception if no such date can be found
- public static DateTime GetNextValidTime(DateTime basetime, DateTime firstdate, string repetition, DayOfWeek[] allowedDays)
- {
- var res = basetime;
-
- var i = 50000;
- while (res < firstdate && i-- > 0)
- res = Timeparser.ParseTimeInterval(repetition, res);
-
- // If we arrived somewhere after the first allowed date
- if (res >= firstdate)
- {
- var ts = Timeparser.ParseTimeSpan(repetition);
-
- if (ts.TotalDays >= 1)
- {
- // We jump in days, so we pick the first valid day after firstdate
-
- for (var n = 0; n < 8; n++)
- if (IsDateAllowed(res, allowedDays))
- break;
- else
- res = res.AddDays(1);
- }
- else
- {
- // We jump less than a day, so we keep adding the repetition until
- // we hit a valid day
-
- i = 50000;
- while (!IsDateAllowed(res, allowedDays) && i-- > 0)
- res = Timeparser.ParseTimeInterval(repetition, res);
- }
- }
-
- if (!IsDateAllowed(res, allowedDays) || res < firstdate)
- {
- StringBuilder sb = new StringBuilder();
- if (allowedDays != null)
- foreach (DayOfWeek w in allowedDays)
- {
- if (sb.Length != 0)
- sb.Append(", ");
- sb.Append(w.ToString());
- }
-
- throw new Exception(Strings.Scheduler.InvalidTimeSetupError(basetime, repetition, sb.ToString()));
- }
-
- return res;
- }
-
- private void OnCompleted(WorkerThread worker, Runner.IRunnerData task)
- {
- Tuple t = null;
- lock(m_lock)
- {
- if (task != null && m_updateTasks.TryGetValue(task, out t))
- m_updateTasks.Remove(task);
- }
-
- if (t != null)
- {
- t.Item1.Time = t.Item2;
- t.Item1.LastRun = t.Item3;
- Program.DataConnection.AddOrUpdateSchedule(t.Item1);
- }
-
- }
-
- private void OnStartingWork(WorkerThread worker, Runner.IRunnerData task)
- {
- if (task is null)
- {
- return;
- }
-
- lock(m_lock)
- {
- if (m_updateTasks.TryGetValue(task, out Tuple scheduleInfo))
- {
- // Item2 is the scheduled start time (Time in the Schedule table).
- // Item3 is the actual start time (LastRun in the Schedule table).
- m_updateTasks[task] = Tuple.Create(scheduleInfo.Item1, scheduleInfo.Item2, DateTime.UtcNow);
- }
- }
- }
-
- ///
- /// The actual scheduling procedure
- ///
- private void Runner()
- {
- var scheduled = new Dictionary>();
- while (!m_terminate)
- {
- //TODO: As this is executed repeatedly we should cache it
- // to avoid frequent db lookups
-
- //Determine schedule list
- var lst = Program.DataConnection.Schedules;
- foreach(var sc in lst)
- {
- if (!string.IsNullOrEmpty(sc.Repeat))
- {
- KeyValuePair startkey;
-
- DateTime last = new DateTime(0, DateTimeKind.Utc);
- DateTime start;
- var scticks = sc.Time.Ticks;
-
- if (!scheduled.TryGetValue(sc.ID, out startkey) || startkey.Key != scticks)
- {
- start = new DateTime(scticks, DateTimeKind.Utc);
- last = sc.LastRun;
- }
- else
- {
- start = startkey.Value;
- }
-
- try
- {
- // Recover from timedrift issues by overriding the dates if the last run date is in the future.
- if (last > DateTime.UtcNow)
- {
- start = DateTime.UtcNow;
- last = DateTime.UtcNow;
- }
- start = GetNextValidTime(start, last, sc.Repeat, sc.AllowedDays);
- }
- catch (Exception ex)
- {
- Program.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex);
- }
-
- //If time is exceeded, run it now
- if (start <= DateTime.UtcNow)
- {
- var jobsToRun = new List();
- //TODO: Cache this to avoid frequent lookups
- foreach(var id in Program.DataConnection.GetBackupIDsForTags(sc.Tags).Distinct().Select(x => x.ToString()))
- {
- //See if it is already queued
- var tmplst = from n in m_worker.CurrentTasks
- where n.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup
- select n.Backup;
- var tastTemp = m_worker.CurrentTask;
- if (tastTemp != null && tastTemp.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup)
- tmplst = tmplst.Union(new [] { tastTemp.Backup });
-
- //If it is not already in queue, put it there
- if (!tmplst.Any(x => x.ID == id))
- {
- var entry = Program.DataConnection.GetBackup(id);
- if (entry != null)
- {
- Dictionary options = Duplicati.Server.Runner.GetCommonOptions();
- Duplicati.Server.Runner.ApplyOptions(entry, options);
- if ((new Duplicati.Library.Main.Options(options)).DisableOnBattery && (Duplicati.Library.Utility.Power.PowerSupply.GetSource() == Duplicati.Library.Utility.Power.PowerSupply.Source.Battery))
- {
- Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "BackupDisabledOnBattery", "Scheduled backup disabled while on battery power.");
- }
- else
- {
- jobsToRun.Add(Server.Runner.CreateTask(Duplicati.Server.Serialization.DuplicatiOperation.Backup, entry));
- }
- }
- }
- }
-
- // Calculate next time, by finding the first entry later than now
- try
- {
- start = GetNextValidTime(start, new DateTime(Math.Max(DateTime.UtcNow.AddSeconds(1).Ticks, start.AddSeconds(1).Ticks), DateTimeKind.Utc), sc.Repeat, sc.AllowedDays);
- }
- catch(Exception ex)
- {
- Program.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex);
- continue;
- }
-
- Server.Runner.IRunnerData lastJob = jobsToRun.LastOrDefault();
- if (lastJob != null)
- {
- lock (m_lock)
- {
- // The actual last run time will be updated when the StartingWork event is raised.
- m_updateTasks[lastJob] = new Tuple(sc, start, DateTime.UtcNow);
- }
- }
-
- foreach (var job in jobsToRun)
- m_worker.AddTask(job);
-
- if (start < DateTime.UtcNow)
- {
- //TODO: Report this somehow
- continue;
- }
- }
-
- scheduled[sc.ID] = new KeyValuePair(scticks, start);
- }
- }
-
- var existing = lst.ToDictionary(x => x.ID);
- //Sort them, lock as we assign the m_schedule variable
- lock(m_lock)
- m_schedule = (from n in scheduled
- where existing.ContainsKey(n.Key)
- orderby n.Value.Value
- select new KeyValuePair(n.Value.Value, existing[n.Key])).ToArray();
-
- // Remove unused entries
- foreach(var c in (from n in scheduled where !existing.ContainsKey(n.Key) select n.Key).ToArray())
- scheduled.Remove(c);
-
- //Raise event if needed
- if (NewSchedule != null)
- NewSchedule(this, null);
-
- int waittime = 0;
-
- //Figure out a sensible amount of time to sleep the thread
- if (scheduled.Count > 0)
- {
- //When is the next run scheduled?
- TimeSpan nextrun = scheduled.Values.Min((x) => x.Value) - DateTime.UtcNow;
- if (nextrun.TotalMilliseconds < 0)
- continue;
-
- //Don't sleep for more than 5 minutes
- waittime = (int)Math.Min(nextrun.TotalMilliseconds, 60 * 1000 * 5);
- }
- else
- {
- //No tasks, check back later
- waittime = 60 * 1000;
- }
-
- //Waiting on the event, enables a wakeup call from termination
- // never use waittime = 0
- m_event.WaitOne(Math.Max(100, waittime), false);
- }
- }
-
- ///
- /// Returns true if the time is at an allowed weekday, false otherwise
- ///
- /// The time to evaluate
- /// The allowed days
- /// True if the backup is allowed to run, false otherwise
- private static bool IsDateAllowed(DateTime time, DayOfWeek[] allowedDays)
- {
- var localTime = time.ToLocalTime();
- if (allowedDays == null || allowedDays.Length == 0)
- return true;
- else
- return Array.IndexOf(allowedDays, localTime.DayOfWeek) >= 0;
- }
-
- }
-}
+
+#endregion
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Linq;
+using System.Threading;
+using Duplicati.Library.Utility;
+using Duplicati.Library.RestAPI;
+
+namespace Duplicati.Server
+{
+ ///
+ /// This class handles scheduled runs of backups
+ ///
+ public class Scheduler
+ {
+ private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType();
+
+ ///
+ /// The thread that runs the scheduler
+ ///
+ private readonly Thread m_thread;
+ ///
+ /// A termination flag
+ ///
+ private volatile bool m_terminate;
+ ///
+ /// The worker thread that is invoked to do work
+ ///
+ private readonly WorkerThread m_worker;
+ ///
+ /// The wait event
+ ///
+ private readonly AutoResetEvent m_event;
+ ///
+ /// The data synchronization lock
+ ///
+ private readonly object m_lock = new object();
+
+ ///
+ /// An event that is raised when the schedule changes
+ ///
+ public event EventHandler NewSchedule;
+
+ ///
+ /// The currently scheduled items
+ ///
+ private KeyValuePair[] m_schedule;
+
+ ///