Merge pull request #5112 from duplicati/feature/kestrel-avalonia-upgrade
Kestrel+avalonia upgrade
This commit is contained in:
@@ -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."
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- Thank you for taking the time to submit an issue using this template. By following the instructions and filling out the sections below, you will help the developers get the necessary information to fix your issue. You may remove sections that aren't relevant to your particular case. You can also preview your report before submitting it. -->
|
||||
<!-- Please note that the issues are a tool for Duplicati developers. If you post here, you are supposed to want to help the project by providing timely information on your problem so it can be fixed. If this is not the case, please use the forum instead -->
|
||||
<!-- Love Duplicati? Please consider supporting our collective at https://opencollective.com/duplicati/donate. -->
|
||||
|
||||
|
||||
<!-- Please search to see if an issue has already been created for your report. -->
|
||||
<!-- Replace the empty checkbox [ ] below with a checked one [x] if you already searched for duplicate bugs. -->
|
||||
|
||||
@@ -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
|
||||
@@ -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/*
|
||||
+17
-75
@@ -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
|
||||
|
||||
@@ -23,5 +23,10 @@ changelog-news.txt
|
||||
binfiles.wxs
|
||||
.idea/
|
||||
.vs/
|
||||
/publish/
|
||||
|
||||
Installer/debian/*.buildinfo
|
||||
/*.deb
|
||||
/*.rpm
|
||||
|
||||
ReleaseBuilder/build-temp/
|
||||
-83
@@ -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
|
||||
Vendored
+48
@@ -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"
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
Vendored
+24
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Duplicati\Library\Compression\Duplicati.Library.Compression.csproj" />
|
||||
<ProjectReference Include="..\..\Duplicati\Library\Encryption\Duplicati.Library.Encryption.csproj" />
|
||||
<ProjectReference Include="..\..\Duplicati\Library\Interface\Duplicati.Library.Interface.csproj" />
|
||||
<ProjectReference Include="..\..\Duplicati\Library\Logging\Duplicati.Library.Logging.csproj" />
|
||||
<ProjectReference Include="..\..\Duplicati\Library\Utility\Duplicati.Library.Utility.csproj" />
|
||||
<ProjectReference Include="..\..\Duplicati\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj" />
|
||||
<ProjectReference Include="..\..\Duplicati\License\Duplicati.License.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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
|
||||
@@ -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<string, string>()))
|
||||
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<string>(_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<string, string>()))
|
||||
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<Duplicati.Library.AutoUpdater.UpdateInfo>(jr);
|
||||
|
||||
var isopts = new Dictionary<string, string>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Mono.Cecil" Version="0.11.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,17 +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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Duplicati\Library\Interface\Duplicati.Library.Interface.csproj" />
|
||||
<ProjectReference Include="..\..\Duplicati\Library\Encryption\Duplicati.Library.Encryption.csproj" />
|
||||
<ProjectReference Include="..\..\Duplicati\Library\Utility\Duplicati.Library.Utility.csproj" />
|
||||
<ProjectReference Include="..\..\Duplicati\Library\Interface\Duplicati.Library.Interface.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,10 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Duplicati\Library\Utility\Duplicati.Library.Utility.csproj" />
|
||||
<ProjectReference Include="..\..\Duplicati\Library\Common\Duplicati.Library.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+16
-20
@@ -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;
|
||||
+1262
-1265
File diff suppressed because it is too large
Load Diff
+14
-14
@@ -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
|
||||
);
|
||||
|
||||
+9
-9
@@ -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
|
||||
);
|
||||
|
||||
+11
-11
@@ -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
|
||||
);
|
||||
|
||||
+158
-158
@@ -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);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+16
-20
@@ -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;
|
||||
+564
-567
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Database\Database schema\**" />
|
||||
<Compile Remove="Database\Database schema\DatabaseConnectionSchemaMarker.cs" />
|
||||
<EmbeddedResource Include="Database\DatabaseConnectionSchemaMarker.cs" />
|
||||
<EmbeddedResource Include="newbackup.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Win32.SystemEvents" Version="6.0.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="HttpServer">
|
||||
<HintPath>..\thirdparty\HttpServer\HttpServer.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Duplicati\CommandLine\CLI\Duplicati.CommandLine.csproj" />
|
||||
<ProjectReference Include="..\Duplicati\Library\Common\Duplicati.Library.Common.csproj" />
|
||||
<ProjectReference Include="..\Duplicati\Library\Encryption\Duplicati.Library.Encryption.csproj" />
|
||||
<ProjectReference Include="..\Duplicati\Library\Main\Duplicati.Library.Main.csproj" />
|
||||
<ProjectReference Include="..\Duplicati\Library\Snapshots\Duplicati.Library.Snapshots.csproj" />
|
||||
<ProjectReference Include="..\Duplicati\Library\SQLiteHelper\Duplicati.Library.SQLiteHelper.csproj" />
|
||||
<ProjectReference Include="..\Duplicati\Server\Duplicati.Server.Serialization\Duplicati.Server.Serialization.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+77
-97
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This class handles synchronized waiting for events
|
||||
/// </summary>
|
||||
public class EventPollNotify
|
||||
{
|
||||
/// <summary>
|
||||
/// The lock that grants exclusive access to control structures
|
||||
/// </summary>
|
||||
private readonly object m_lock = new object();
|
||||
/// <summary>
|
||||
/// The current eventID
|
||||
/// </summary>
|
||||
private long m_eventNo = 0;
|
||||
/// <summary>
|
||||
/// The list of subscribed waiting threads
|
||||
/// </summary>
|
||||
private readonly Queue<System.Threading.ManualResetEvent> m_waitQueue = new Queue<System.Threading.ManualResetEvent>();
|
||||
|
||||
/// <summary>
|
||||
/// An eventhandler for subscribing to event updates without blocking
|
||||
/// </summary>
|
||||
public event EventHandler NewEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current event ID
|
||||
/// </summary>
|
||||
public long EventNo { get { return m_eventNo; } }
|
||||
|
||||
/// <summary>
|
||||
/// Call to wait for an event that is newer than the current known event
|
||||
/// </summary>
|
||||
/// <param name="eventId">The last known event id</param>
|
||||
/// <param name="milliseconds">The number of milliseconds to block</param>
|
||||
/// <returns>The current event id</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signals that an event has occurred and notifies all waiting threads
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// This class handles synchronized waiting for events
|
||||
/// </summary>
|
||||
public class EventPollNotify
|
||||
{
|
||||
/// <summary>
|
||||
/// The lock that grants exclusive access to control structures
|
||||
/// </summary>
|
||||
private readonly object m_lock = new object();
|
||||
/// <summary>
|
||||
/// The current eventID
|
||||
/// </summary>
|
||||
private long m_eventNo = 0;
|
||||
/// <summary>
|
||||
/// The list of subscribed waiting threads
|
||||
/// </summary>
|
||||
private readonly Queue<System.Threading.ManualResetEvent> m_waitQueue = new Queue<System.Threading.ManualResetEvent>();
|
||||
|
||||
/// <summary>
|
||||
/// An eventhandler for subscribing to event updates without blocking
|
||||
/// </summary>
|
||||
public event EventHandler NewEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current event ID
|
||||
/// </summary>
|
||||
public long EventNo { get { return m_eventNo; } }
|
||||
|
||||
/// <summary>
|
||||
/// Call to wait for an event that is newer than the current known event
|
||||
/// </summary>
|
||||
/// <param name="eventId">The last known event id</param>
|
||||
/// <param name="milliseconds">The number of milliseconds to block</param>
|
||||
/// <returns>The current event id</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signals that an event has occurred and notifies all waiting threads
|
||||
/// </summary>
|
||||
public void SignalNewEvent()
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
m_eventNo++;
|
||||
while (m_waitQueue.Count > 0)
|
||||
m_waitQueue.Dequeue().Set();
|
||||
}
|
||||
|
||||
if (NewEvent != null)
|
||||
NewEvent(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// This is the only access to the database
|
||||
/// </summary>
|
||||
public static Server.Database.Connection DataConnection;
|
||||
|
||||
/// <summary>
|
||||
/// The controller interface for pause/resume and throttle options
|
||||
/// </summary>
|
||||
public static LiveControls LiveControl;
|
||||
|
||||
/// <summary>
|
||||
/// A delegate method for creating a copy of the current progress state
|
||||
/// </summary>
|
||||
public static Func<Duplicati.Server.Serialization.Interface.IProgressEventData> GenerateProgressState;
|
||||
|
||||
/// <summary>
|
||||
/// The status event signaler, used to control long polling of status updates
|
||||
/// </summary>
|
||||
public static readonly EventPollNotify StatusEventNotifyer = new EventPollNotify();
|
||||
|
||||
/// <summary>
|
||||
/// This is the working thread
|
||||
/// </summary>
|
||||
public static Duplicati.Library.Utility.WorkerThread<Runner.IRunnerData> WorkThread;
|
||||
|
||||
public static Func<long> PeekLastDataUpdateID;
|
||||
public static Func<long> PeekLastNotificationUpdateID;
|
||||
|
||||
public static Action IncrementLastDataUpdateID;
|
||||
|
||||
public static Action IncrementLastNotificationUpdateID;
|
||||
|
||||
public static Action StartOrStopUsageReporter;
|
||||
|
||||
public static Action UpdateThrottleSpeeds;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder where Duplicati data is stored
|
||||
/// </summary>
|
||||
public static string DataFolder;
|
||||
|
||||
/// <summary>
|
||||
/// This is the scheduling thread
|
||||
/// </summary>
|
||||
public static Scheduler Scheduler;
|
||||
|
||||
/// <summary>
|
||||
/// The log redirect handler
|
||||
/// </summary>
|
||||
public static readonly LogWriteHandler LogHandler = new LogWriteHandler();
|
||||
|
||||
public static Func<Dictionary<string, string>, Server.Database.Connection> GetDatabaseConnection;
|
||||
|
||||
/// <summary>
|
||||
/// The update poll thread.
|
||||
/// </summary>
|
||||
public static UpdatePollThread UpdatePoller;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Used to check the origin of the web server (e.g. Tray icon or a stand alone Server)
|
||||
/// </summary>
|
||||
public static string Origin = "Server";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The application exit event
|
||||
/// </summary>
|
||||
public static System.Threading.ManualResetEvent ApplicationExitEvent;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// List of completed task results
|
||||
/// </summary>
|
||||
public static readonly List<KeyValuePair<long, Exception>> TaskResultCache = new List<KeyValuePair<long, Exception>>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This is the lock to be used before manipulating the shared resources
|
||||
/// </summary>
|
||||
public static readonly object MainLock = new object();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This class keeps track of the users modifications regarding
|
||||
/// throttling and pause/resume
|
||||
/// </summary>
|
||||
public class LiveControls
|
||||
{
|
||||
/// <summary>
|
||||
/// The tag used for logging
|
||||
/// </summary>
|
||||
private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType<LiveControls>();
|
||||
|
||||
/// <summary>
|
||||
/// An event that is activated when the pause state changes
|
||||
/// </summary>
|
||||
public event EventHandler StateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// An event that is activated when the thread priority changes
|
||||
/// </summary>
|
||||
public event EventHandler ThreadPriorityChanged;
|
||||
|
||||
/// <summary>
|
||||
/// An event that is activated when the throttle speed changes
|
||||
/// </summary>
|
||||
public event EventHandler ThrottleSpeedChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The possible states for the live control
|
||||
/// </summary>
|
||||
public enum LiveControlState
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that the backups are running
|
||||
/// </summary>
|
||||
Running,
|
||||
/// <summary>
|
||||
/// Indicates that the backups are currently suspended
|
||||
/// </summary>
|
||||
Paused
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current control state
|
||||
/// </summary>
|
||||
private LiveControlState m_state;
|
||||
|
||||
/// <summary>
|
||||
/// A value that indicates if the current pause state is caused by being suspended
|
||||
/// </summary>
|
||||
private bool m_pausedForSuspend = false;
|
||||
|
||||
/// <summary>
|
||||
/// The time to pause for, used to ensure that a user set pause can override the suspend pause
|
||||
/// </summary>
|
||||
private DateTime m_suspendMinimumPause = new DateTime(0);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current state for the control
|
||||
/// </summary>
|
||||
public LiveControlState State { get { return m_state; } }
|
||||
|
||||
/// <summary>
|
||||
/// The internal variable that tracks the the priority
|
||||
/// </summary>
|
||||
private System.Threading.ThreadPriority? m_priority;
|
||||
|
||||
/// <summary>
|
||||
/// The internal variable that tracks the upload limit
|
||||
/// </summary>
|
||||
private long? m_uploadLimit;
|
||||
|
||||
/// <summary>
|
||||
/// The internal variable that tracks the download limit
|
||||
/// </summary>
|
||||
private long? m_downloadLimit;
|
||||
|
||||
/// <summary>
|
||||
/// The object that ensures concurrent operations
|
||||
/// </summary>
|
||||
private readonly object m_lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current overridden thread priority
|
||||
/// </summary>
|
||||
public System.Threading.ThreadPriority? ThreadPriority
|
||||
{
|
||||
get { return m_priority; }
|
||||
set
|
||||
{
|
||||
if (m_priority != value)
|
||||
{
|
||||
m_priority = value;
|
||||
if (ThreadPriorityChanged != null)
|
||||
ThreadPriorityChanged(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current upload limit in bps
|
||||
/// </summary>
|
||||
public long? UploadLimit
|
||||
{
|
||||
get { return m_uploadLimit; }
|
||||
set
|
||||
{
|
||||
if (m_uploadLimit != value)
|
||||
{
|
||||
m_uploadLimit = value;
|
||||
if (ThrottleSpeedChanged != null)
|
||||
ThrottleSpeedChanged(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the download limit in bps
|
||||
/// </summary>
|
||||
public long? DownloadLimit
|
||||
{
|
||||
get { return m_downloadLimit; }
|
||||
set
|
||||
{
|
||||
if (m_downloadLimit != value)
|
||||
{
|
||||
m_downloadLimit = value;
|
||||
if (ThrottleSpeedChanged != null)
|
||||
ThrottleSpeedChanged(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The timer that is activated after a pause period.
|
||||
/// </summary>
|
||||
private readonly System.Threading.Timer m_waitTimer;
|
||||
|
||||
/// <summary>
|
||||
/// The time that the current pause is expected to expire
|
||||
/// </summary>
|
||||
private DateTime m_waitTimeExpiration = new DateTime(0);
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of the LiveControl
|
||||
/// </summary>
|
||||
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 { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event that occurs when the timeout duration is exceeded
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender of the event</param>
|
||||
private void m_waitTimer_Tick(object sender)
|
||||
{
|
||||
lock (m_lock)
|
||||
Resume();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal helper to reset the timeout timer
|
||||
/// </summary>
|
||||
/// <param name="timeout">The time to wait</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal helper to set the pause mode
|
||||
/// </summary>
|
||||
private void SetPauseMode()
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
if (m_state == LiveControlState.Running)
|
||||
{
|
||||
m_state = LiveControlState.Paused;
|
||||
if (StateChanged != null)
|
||||
StateChanged(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pauses the backups until resumed
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resumes a backups to the running state
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suspends the backups for a given period
|
||||
/// </summary>
|
||||
/// <param name="timeout">The duration to wait</param>
|
||||
public void Pause(string timeout)
|
||||
{
|
||||
Pause(Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suspends the backups for a given period
|
||||
/// </summary>
|
||||
/// <param name="timeout">The duration to wait</param>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time the current pause is expected to end
|
||||
/// </summary>
|
||||
public DateTime EstimatedPauseEnd { get { return m_waitTimeExpiration; } }
|
||||
|
||||
/// <summary>
|
||||
/// Method for calling a Win32 API
|
||||
/// </summary>
|
||||
private void RegisterHibernateMonitor()
|
||||
{
|
||||
Microsoft.Win32.SystemEvents.PowerModeChanged += new Microsoft.Win32.PowerModeChangedEventHandler(SystemEvents_PowerModeChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A monitor for detecting when the system hibernates or resumes
|
||||
/// </summary>
|
||||
/// <param name="sender">Unused sender parameter</param>
|
||||
/// <param name="_e">The event information</param>
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// This class keeps track of the users modifications regarding
|
||||
/// throttling and pause/resume
|
||||
/// </summary>
|
||||
public class LiveControls
|
||||
{
|
||||
/// <summary>
|
||||
/// The tag used for logging
|
||||
/// </summary>
|
||||
private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType<LiveControls>();
|
||||
|
||||
/// <summary>
|
||||
/// An event that is activated when the pause state changes
|
||||
/// </summary>
|
||||
public event EventHandler StateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// An event that is activated when the thread priority changes
|
||||
/// </summary>
|
||||
public event EventHandler ThreadPriorityChanged;
|
||||
|
||||
/// <summary>
|
||||
/// An event that is activated when the throttle speed changes
|
||||
/// </summary>
|
||||
public event EventHandler ThrottleSpeedChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The possible states for the live control
|
||||
/// </summary>
|
||||
public enum LiveControlState
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that the backups are running
|
||||
/// </summary>
|
||||
Running,
|
||||
/// <summary>
|
||||
/// Indicates that the backups are currently suspended
|
||||
/// </summary>
|
||||
Paused
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current control state
|
||||
/// </summary>
|
||||
private LiveControlState m_state;
|
||||
|
||||
/// <summary>
|
||||
/// A value that indicates if the current pause state is caused by being suspended
|
||||
/// </summary>
|
||||
private bool m_pausedForSuspend = false;
|
||||
|
||||
/// <summary>
|
||||
/// The time to pause for, used to ensure that a user set pause can override the suspend pause
|
||||
/// </summary>
|
||||
private DateTime m_suspendMinimumPause = new DateTime(0);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current state for the control
|
||||
/// </summary>
|
||||
public LiveControlState State { get { return m_state; } }
|
||||
|
||||
/// <summary>
|
||||
/// The internal variable that tracks the the priority
|
||||
/// </summary>
|
||||
private System.Threading.ThreadPriority? m_priority;
|
||||
|
||||
/// <summary>
|
||||
/// The internal variable that tracks the upload limit
|
||||
/// </summary>
|
||||
private long? m_uploadLimit;
|
||||
|
||||
/// <summary>
|
||||
/// The internal variable that tracks the download limit
|
||||
/// </summary>
|
||||
private long? m_downloadLimit;
|
||||
|
||||
/// <summary>
|
||||
/// The object that ensures concurrent operations
|
||||
/// </summary>
|
||||
private readonly object m_lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current overridden thread priority
|
||||
/// </summary>
|
||||
public System.Threading.ThreadPriority? ThreadPriority
|
||||
{
|
||||
get { return m_priority; }
|
||||
set
|
||||
{
|
||||
if (m_priority != value)
|
||||
{
|
||||
m_priority = value;
|
||||
if (ThreadPriorityChanged != null)
|
||||
ThreadPriorityChanged(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current upload limit in bps
|
||||
/// </summary>
|
||||
public long? UploadLimit
|
||||
{
|
||||
get { return m_uploadLimit; }
|
||||
set
|
||||
{
|
||||
if (m_uploadLimit != value)
|
||||
{
|
||||
m_uploadLimit = value;
|
||||
if (ThrottleSpeedChanged != null)
|
||||
ThrottleSpeedChanged(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the download limit in bps
|
||||
/// </summary>
|
||||
public long? DownloadLimit
|
||||
{
|
||||
get { return m_downloadLimit; }
|
||||
set
|
||||
{
|
||||
if (m_downloadLimit != value)
|
||||
{
|
||||
m_downloadLimit = value;
|
||||
if (ThrottleSpeedChanged != null)
|
||||
ThrottleSpeedChanged(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The timer that is activated after a pause period.
|
||||
/// </summary>
|
||||
private readonly System.Threading.Timer m_waitTimer;
|
||||
|
||||
/// <summary>
|
||||
/// The time that the current pause is expected to expire
|
||||
/// </summary>
|
||||
private DateTime m_waitTimeExpiration = new DateTime(0);
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of the LiveControl
|
||||
/// </summary>
|
||||
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 { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event that occurs when the timeout duration is exceeded
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender of the event</param>
|
||||
private void m_waitTimer_Tick(object sender)
|
||||
{
|
||||
lock (m_lock)
|
||||
Resume();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal helper to reset the timeout timer
|
||||
/// </summary>
|
||||
/// <param name="timeout">The time to wait</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal helper to set the pause mode
|
||||
/// </summary>
|
||||
private void SetPauseMode()
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
if (m_state == LiveControlState.Running)
|
||||
{
|
||||
m_state = LiveControlState.Paused;
|
||||
if (StateChanged != null)
|
||||
StateChanged(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pauses the backups until resumed
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resumes a backups to the running state
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suspends the backups for a given period
|
||||
/// </summary>
|
||||
/// <param name="timeout">The duration to wait</param>
|
||||
public void Pause(string timeout)
|
||||
{
|
||||
Pause(Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Suspends the backups for a given period
|
||||
/// </summary>
|
||||
/// <param name="timeout">The duration to wait</param>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time the current pause is expected to end
|
||||
/// </summary>
|
||||
public DateTime EstimatedPauseEnd { get { return m_waitTimeExpiration; } }
|
||||
|
||||
/// <summary>
|
||||
/// Method for calling a Win32 API
|
||||
/// </summary>
|
||||
private void RegisterHibernateMonitor()
|
||||
{
|
||||
Microsoft.Win32.SystemEvents.PowerModeChanged += new Microsoft.Win32.PowerModeChangedEventHandler(SystemEvents_PowerModeChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A monitor for detecting when the system hibernates or resumes
|
||||
/// </summary>
|
||||
/// <param name="sender">Unused sender parameter</param>
|
||||
/// <param name="_e">The event information</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+16
-20
@@ -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;
|
||||
-1
@@ -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;
|
||||
+742
-737
File diff suppressed because it is too large
Load Diff
+141
-136
@@ -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<BackupDefaults>();
|
||||
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
// Start with a scratch object
|
||||
var o = new Newtonsoft.Json.Linq.JObject();
|
||||
|
||||
// Add application wide settings
|
||||
o.Add("ApplicationOptions", new Newtonsoft.Json.Linq.JArray(
|
||||
from n in FIXMEGlobal.DataConnection.Settings
|
||||
select Newtonsoft.Json.Linq.JObject.FromObject(n)
|
||||
));
|
||||
|
||||
try
|
||||
{
|
||||
// Add built-in defaults
|
||||
Newtonsoft.Json.Linq.JObject n;
|
||||
using(var s = new System.IO.StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(FIXMEGlobal), "newbackup.json")))
|
||||
n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(s.ReadToEnd());
|
||||
|
||||
MergeJsonObjects(o, n);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Library.Logging.Log.WriteErrorMessage(LOGTAG, "BackupDefaultsError", e, "Failed to locate embeded backup defaults");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Add install defaults/overrides, if present
|
||||
var path = SystemIO.IO_OS.PathCombine(Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR, "newbackup.json");
|
||||
if (System.IO.File.Exists(path))
|
||||
{
|
||||
Newtonsoft.Json.Linq.JObject n;
|
||||
n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(System.IO.File.ReadAllText(path));
|
||||
|
||||
MergeJsonObjects(o, n);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Library.Logging.Log.WriteErrorMessage(LOGTAG, "BackupDefaultsError", e, "Failed to process newbackup.json");
|
||||
}
|
||||
|
||||
info.OutputOK(new
|
||||
{
|
||||
success = true,
|
||||
data = o
|
||||
});
|
||||
}
|
||||
|
||||
private static void MergeJsonObjects(Newtonsoft.Json.Linq.JObject self, Newtonsoft.Json.Linq.JObject other)
|
||||
{
|
||||
foreach(var p in other.Properties())
|
||||
{
|
||||
var sp = self.Property(p.Name);
|
||||
if (sp == null)
|
||||
self.Add(p);
|
||||
else
|
||||
{
|
||||
switch (p.Type)
|
||||
{
|
||||
// Primitives override
|
||||
case Newtonsoft.Json.Linq.JTokenType.Boolean:
|
||||
case Newtonsoft.Json.Linq.JTokenType.Bytes:
|
||||
case Newtonsoft.Json.Linq.JTokenType.Comment:
|
||||
case Newtonsoft.Json.Linq.JTokenType.Constructor:
|
||||
case Newtonsoft.Json.Linq.JTokenType.Date:
|
||||
case Newtonsoft.Json.Linq.JTokenType.Float:
|
||||
case Newtonsoft.Json.Linq.JTokenType.Guid:
|
||||
case Newtonsoft.Json.Linq.JTokenType.Integer:
|
||||
case Newtonsoft.Json.Linq.JTokenType.String:
|
||||
case Newtonsoft.Json.Linq.JTokenType.TimeSpan:
|
||||
case Newtonsoft.Json.Linq.JTokenType.Uri:
|
||||
case Newtonsoft.Json.Linq.JTokenType.None:
|
||||
case Newtonsoft.Json.Linq.JTokenType.Null:
|
||||
case Newtonsoft.Json.Linq.JTokenType.Undefined:
|
||||
self.Replace(p);
|
||||
break;
|
||||
|
||||
// Arrays merge
|
||||
case Newtonsoft.Json.Linq.JTokenType.Array:
|
||||
if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Array)
|
||||
sp.Value = new Newtonsoft.Json.Linq.JArray(((Newtonsoft.Json.Linq.JArray)sp.Value).Union((Newtonsoft.Json.Linq.JArray)p.Value));
|
||||
else
|
||||
{
|
||||
var a = new Newtonsoft.Json.Linq.JArray(sp.Value);
|
||||
sp.Value = new Newtonsoft.Json.Linq.JArray(a.Union((Newtonsoft.Json.Linq.JArray)p.Value));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Objects merge
|
||||
case Newtonsoft.Json.Linq.JTokenType.Object:
|
||||
if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Object)
|
||||
MergeJsonObjects((Newtonsoft.Json.Linq.JObject)sp.Value, (Newtonsoft.Json.Linq.JObject)p.Value);
|
||||
else
|
||||
sp.Value = p.Value;
|
||||
break;
|
||||
|
||||
// Ignore other stuff
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+310
-304
@@ -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 = "<html><body><script type=\"text/javascript\">var jso = 'JSO'; var rp = null; try { rp = parent['CBM']; } catch (e) {}; if (rp) { rp('MSG', jso); } else { alert; rp('MSG'); };</script></body></html>";
|
||||
//output_template = "<html><body><script type=\"text/javascript\">alert('MSG');</script></body></html>";
|
||||
try
|
||||
{
|
||||
var input = info.Request.Form;
|
||||
var cmdline = Library.Utility.Utility.ParseBool(input["cmdline"].Value, false);
|
||||
var import_metadata = Library.Utility.Utility.ParseBool(input["import_metadata"].Value, false);
|
||||
var direct = Library.Utility.Utility.ParseBool(input["direct"].Value, false);
|
||||
output_template = output_template.Replace("CBM", input["callback"].Value);
|
||||
if (cmdline)
|
||||
{
|
||||
info.Response.ContentType = "text/html";
|
||||
info.BodyWriter.Write(output_template.Replace("MSG", "Import from commandline not yet implemented"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var file = info.Request.Form.GetFile("config");
|
||||
if (file == null)
|
||||
throw new Exception("No file uploaded");
|
||||
|
||||
Serializable.ImportExportStructure ipx = Backups.LoadConfiguration(file.Filename, import_metadata, () => input["passphrase"].Value);
|
||||
if (direct)
|
||||
{
|
||||
lock (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<string> getPassword, Dictionary<string, string> advancedOptions)
|
||||
{
|
||||
// This removes the ID and DBPath from the backup configuration.
|
||||
Serializable.ImportExportStructure importedStructure = Backups.LoadConfiguration(configurationFile, importMetadata, getPassword);
|
||||
|
||||
// This will create the Duplicati-server.sqlite database file if it doesn't exist.
|
||||
using (Duplicati.Server.Database.Connection connection = 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<string> getPassword)
|
||||
{
|
||||
Serializable.ImportExportStructure ipx;
|
||||
|
||||
var buf = new byte[3];
|
||||
using (var fs = System.IO.File.OpenRead(filename))
|
||||
{
|
||||
Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buf, buf.Length);
|
||||
|
||||
fs.Position = 0;
|
||||
if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S')
|
||||
{
|
||||
using (var m = new Duplicati.Library.Encryption.AESEncryption(getPassword(), new Dictionary<string, string>()))
|
||||
{
|
||||
using (var m2 = m.Decrypt(fs))
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(m2))
|
||||
{
|
||||
ipx = Serializer.Deserialize<Serializable.ImportExportStructure>(sr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(fs))
|
||||
{
|
||||
ipx = Serializer.Deserialize<Serializable.ImportExportStructure>(sr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ipx.Backup == null)
|
||||
{
|
||||
throw new Exception("No backup found in document");
|
||||
}
|
||||
|
||||
if (ipx.Backup.Metadata == null)
|
||||
{
|
||||
ipx.Backup.Metadata = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
if (!importMetadata)
|
||||
{
|
||||
ipx.Backup.Metadata.Clear();
|
||||
}
|
||||
|
||||
ipx.Backup.ID = null;
|
||||
ipx.Backup.DBPath = null;
|
||||
|
||||
if (ipx.Schedule != null)
|
||||
{
|
||||
ipx.Schedule.ID = -1;
|
||||
}
|
||||
|
||||
return ipx;
|
||||
}
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
if ("import".Equals(key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ImportBackup(info);
|
||||
return;
|
||||
}
|
||||
|
||||
AddOrUpdateBackupData data = null;
|
||||
try
|
||||
{
|
||||
var str = info.Request.Form["data"].Value;
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
str = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8).ReadToEnd();
|
||||
|
||||
data = Serializer.Deserialize<AddOrUpdateBackupData>(new StringReader(str));
|
||||
if (data.Backup == null)
|
||||
{
|
||||
info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
data.Backup.ID = null;
|
||||
|
||||
if (Duplicati.Library.Utility.Utility.ParseBool(info.Request.Form["temporary"].Value, false))
|
||||
{
|
||||
using(var tf = new Duplicati.Library.Utility.TempFile())
|
||||
data.Backup.DBPath = tf;
|
||||
|
||||
data.Backup.Filters = data.Backup.Filters ?? new Duplicati.Server.Serialization.Interface.IFilter[0];
|
||||
data.Backup.Settings = data.Backup.Settings ?? new Duplicati.Server.Serialization.Interface.ISetting[0];
|
||||
|
||||
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<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(AddOrUpdateBackupData[])),
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Post, typeof(AddOrUpdateBackupData))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Server.Serialization;
|
||||
using System.IO;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class Backups : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented
|
||||
{
|
||||
public class AddOrUpdateBackupData
|
||||
{
|
||||
public Boolean IsUnencryptedOrPassphraseStored { get; set;}
|
||||
public Database.Schedule Schedule { get; set;}
|
||||
public Database.Backup Backup { get; set;}
|
||||
}
|
||||
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
var schedules = FIXMEGlobal.DataConnection.Schedules;
|
||||
var backups = FIXMEGlobal.DataConnection.Backups;
|
||||
|
||||
var all = from n in backups
|
||||
select new AddOrUpdateBackupData {
|
||||
IsUnencryptedOrPassphraseStored = FIXMEGlobal.DataConnection.IsUnencryptedOrPassphraseStored(long.Parse(n.ID)),
|
||||
Backup = (Database.Backup)n,
|
||||
Schedule =
|
||||
(from x in schedules
|
||||
where x.Tags != null && x.Tags.Contains("ID=" + n.ID)
|
||||
select (Database.Schedule)x).FirstOrDefault()
|
||||
};
|
||||
|
||||
info.BodyWriter.OutputOK(all.ToArray());
|
||||
}
|
||||
|
||||
private void ImportBackup(RequestInfo info)
|
||||
{
|
||||
var output_template = "<html><body><script type=\"text/javascript\">var jso = 'JSO'; var rp = null; try { rp = parent['CBM']; } catch (e) {}; if (rp) { rp('MSG', jso); } else { alert; rp('MSG'); };</script></body></html>";
|
||||
//output_template = "<html><body><script type=\"text/javascript\">alert('MSG');</script></body></html>";
|
||||
try
|
||||
{
|
||||
var input = info.Request.Form;
|
||||
var cmdline = Library.Utility.Utility.ParseBool(input["cmdline"].Value, false);
|
||||
var import_metadata = Library.Utility.Utility.ParseBool(input["import_metadata"].Value, false);
|
||||
var direct = Library.Utility.Utility.ParseBool(input["direct"].Value, false);
|
||||
output_template = output_template.Replace("CBM", input["callback"].Value);
|
||||
if (cmdline)
|
||||
{
|
||||
info.Response.ContentType = "text/html";
|
||||
info.BodyWriter.Write(output_template.Replace("MSG", "Import from commandline not yet implemented"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var file = info.Request.Form.GetFile("config");
|
||||
if (file == null)
|
||||
throw new Exception("No file uploaded");
|
||||
|
||||
Serializable.ImportExportStructure ipx = Backups.LoadConfiguration(file.Filename, import_metadata, () => input["passphrase"].Value);
|
||||
if (direct)
|
||||
{
|
||||
lock (FIXMEGlobal.DataConnection.m_lock)
|
||||
{
|
||||
var basename = ipx.Backup.Name;
|
||||
var c = 0;
|
||||
while (c++ < 100 && FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
ipx.Backup.Name = basename + " (" + c.ToString() + ")";
|
||||
|
||||
if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
info.BodyWriter.SetOK();
|
||||
info.Response.ContentType = "text/html";
|
||||
info.BodyWriter.Write(output_template.Replace("MSG", "There already exists a backup with the name: " + basename.Replace("\'", "\\'")));
|
||||
}
|
||||
|
||||
var err = FIXMEGlobal.DataConnection.ValidateBackup(ipx.Backup, ipx.Schedule);
|
||||
if (!string.IsNullOrWhiteSpace(err))
|
||||
{
|
||||
info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(ipx.Backup, ipx.Schedule);
|
||||
}
|
||||
|
||||
info.Response.ContentType = "text/html";
|
||||
info.BodyWriter.Write(output_template.Replace("MSG", "OK"));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var sw = new StringWriter())
|
||||
{
|
||||
Serializer.SerializeJson(sw, ipx, true);
|
||||
output_template = output_template.Replace("'JSO'", sw.ToString());
|
||||
}
|
||||
info.BodyWriter.Write(output_template.Replace("MSG", "Import completed, but a browser issue prevents loading the contents. Try using the direct import method instead."));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.LogError("", "Failed to import backup", ex);
|
||||
info.Response.ContentType = "text/html";
|
||||
info.BodyWriter.Write(output_template.Replace("MSG", ex.Message.Replace("\'", "\\'").Replace("\r", "\\r").Replace("\n", "\\n")));
|
||||
}
|
||||
}
|
||||
|
||||
public static Serializable.ImportExportStructure ImportBackup(string configurationFile, bool importMetadata, Func<string> getPassword, Dictionary<string, string> advancedOptions)
|
||||
{
|
||||
// This removes the ID and DBPath from the backup configuration.
|
||||
Serializable.ImportExportStructure importedStructure = Backups.LoadConfiguration(configurationFile, importMetadata, getPassword);
|
||||
|
||||
// This will create the Duplicati-server.sqlite database file if it doesn't exist.
|
||||
using (Duplicati.Server.Database.Connection connection = FIXMEGlobal.GetDatabaseConnection(advancedOptions))
|
||||
{
|
||||
if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists.");
|
||||
}
|
||||
|
||||
string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule);
|
||||
if (!string.IsNullOrWhiteSpace(error))
|
||||
{
|
||||
throw new InvalidOperationException(error);
|
||||
}
|
||||
|
||||
// This creates a new ID and DBPath.
|
||||
connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule);
|
||||
}
|
||||
|
||||
return importedStructure;
|
||||
}
|
||||
|
||||
private static Serializable.ImportExportStructure LoadConfiguration(string filename, bool importMetadata, Func<string> getPassword)
|
||||
{
|
||||
Serializable.ImportExportStructure ipx;
|
||||
|
||||
var buf = new byte[3];
|
||||
using (var fs = System.IO.File.OpenRead(filename))
|
||||
{
|
||||
Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buf, buf.Length);
|
||||
|
||||
fs.Position = 0;
|
||||
if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S')
|
||||
{
|
||||
using (var m = new Duplicati.Library.Encryption.AESEncryption(getPassword(), new Dictionary<string, string>()))
|
||||
{
|
||||
using (var m2 = m.Decrypt(fs))
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(m2))
|
||||
{
|
||||
ipx = Serializer.Deserialize<Serializable.ImportExportStructure>(sr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(fs))
|
||||
{
|
||||
ipx = Serializer.Deserialize<Serializable.ImportExportStructure>(sr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ipx.Backup == null)
|
||||
{
|
||||
throw new Exception("No backup found in document");
|
||||
}
|
||||
|
||||
if (ipx.Backup.Metadata == null)
|
||||
{
|
||||
ipx.Backup.Metadata = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
if (!importMetadata)
|
||||
{
|
||||
ipx.Backup.Metadata.Clear();
|
||||
}
|
||||
|
||||
ipx.Backup.ID = null;
|
||||
ipx.Backup.DBPath = null;
|
||||
|
||||
if (ipx.Schedule != null)
|
||||
{
|
||||
ipx.Schedule.ID = -1;
|
||||
}
|
||||
|
||||
return ipx;
|
||||
}
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
if ("import".Equals(key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ImportBackup(info);
|
||||
return;
|
||||
}
|
||||
|
||||
AddOrUpdateBackupData data = null;
|
||||
try
|
||||
{
|
||||
var str = info.Request.Form["data"].Value;
|
||||
if (string.IsNullOrWhiteSpace(str)) {
|
||||
str = System.Threading.Tasks.Task.Run(async () =>
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
|
||||
|
||||
return await sr.ReadToEndAsync();
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
data = Serializer.Deserialize<AddOrUpdateBackupData>(new StringReader(str));
|
||||
if (data.Backup == null)
|
||||
{
|
||||
info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
data.Backup.ID = null;
|
||||
|
||||
if (Duplicati.Library.Utility.Utility.ParseBool(info.Request.Form["temporary"].Value, false))
|
||||
{
|
||||
using(var tf = new Duplicati.Library.Utility.TempFile())
|
||||
data.Backup.DBPath = tf;
|
||||
|
||||
data.Backup.Filters = data.Backup.Filters ?? new Duplicati.Server.Serialization.Interface.IFilter[0];
|
||||
data.Backup.Settings = data.Backup.Settings ?? new Duplicati.Server.Serialization.Interface.ISetting[0];
|
||||
|
||||
FIXMEGlobal.DataConnection.RegisterTemporaryBackup(data.Backup);
|
||||
|
||||
info.OutputOK(new { status = "OK", ID = data.Backup.ID });
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Library.Utility.Utility.ParseBool(info.Request.Form["existing_db"].Value, false))
|
||||
{
|
||||
data.Backup.DBPath = Library.Main.DatabaseLocator.GetDatabasePath(data.Backup.TargetURL, null, false, false);
|
||||
if (string.IsNullOrWhiteSpace(data.Backup.DBPath))
|
||||
throw new Exception("Unable to find remote db path?");
|
||||
}
|
||||
|
||||
|
||||
lock(FIXMEGlobal.DataConnection.m_lock)
|
||||
{
|
||||
if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict);
|
||||
return;
|
||||
}
|
||||
|
||||
var err = FIXMEGlobal.DataConnection.ValidateBackup(data.Backup, data.Schedule);
|
||||
if (!string.IsNullOrWhiteSpace(err))
|
||||
{
|
||||
info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule);
|
||||
}
|
||||
|
||||
info.OutputOK(new { status = "OK", ID = data.Backup.ID });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (data == null)
|
||||
info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
|
||||
else
|
||||
info.ReportClientError(string.Format("Unable to save schedule or backup object: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public string Description { get { return "Return a list of current backups and their schedules"; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(AddOrUpdateBackupData[])),
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Post, typeof(AddOrUpdateBackupData))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
+172
-171
@@ -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<string, CaptchaEntry> m_captchas = new Dictionary<string, CaptchaEntry>();
|
||||
|
||||
public static bool SolvedCaptcha(string token, string target, string answer)
|
||||
{
|
||||
lock(m_lock)
|
||||
{
|
||||
CaptchaEntry tp;
|
||||
m_captchas.TryGetValue(token ?? string.Empty, out tp);
|
||||
if (tp == null)
|
||||
return false;
|
||||
|
||||
if (tp.Attempts > 0)
|
||||
tp.Attempts--;
|
||||
|
||||
return tp.Attempts >= 0 && string.Equals(tp.Answer, answer, StringComparison.OrdinalIgnoreCase) && tp.Target == target && tp.Expires >= DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
info.ReportClientError("Missing token value", System.Net.HttpStatusCode.Unauthorized);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
string answer = null;
|
||||
lock (m_lock)
|
||||
{
|
||||
CaptchaEntry tp;
|
||||
m_captchas.TryGetValue(key, out tp);
|
||||
if (tp != null && tp.Expires > DateTime.Now)
|
||||
answer = tp.Answer;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(answer))
|
||||
{
|
||||
info.ReportClientError("No such entry", System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
using (var bmp = CaptchaUtil.CreateCaptcha(answer))
|
||||
using (var ms = new System.IO.MemoryStream())
|
||||
{
|
||||
info.Response.ContentType = "image/jpeg";
|
||||
info.Response.ContentLength = ms.Length;
|
||||
bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
|
||||
ms.Position = 0;
|
||||
|
||||
info.Response.ContentType = "image/jpeg";
|
||||
info.Response.ContentLength = ms.Length;
|
||||
info.Response.SendHeaders();
|
||||
ms.CopyTo(info.Response.Body);
|
||||
info.Response.Send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
var target = info.Request.Param["target"].Value;
|
||||
if (string.IsNullOrWhiteSpace(target))
|
||||
{
|
||||
info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
var answer = CaptchaUtil.CreateRandomAnswer(minlength: 6, maxlength: 6);
|
||||
var nonce = Guid.NewGuid().ToString();
|
||||
|
||||
string token;
|
||||
using (var ms = new System.IO.MemoryStream())
|
||||
{
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(answer + nonce);
|
||||
ms.Write(bytes, 0, bytes.Length);
|
||||
ms.Position = 0;
|
||||
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<string, CaptchaEntry> m_captchas = new Dictionary<string, CaptchaEntry>();
|
||||
|
||||
public static bool SolvedCaptcha(string token, string target, string answer)
|
||||
{
|
||||
lock(m_lock)
|
||||
{
|
||||
CaptchaEntry tp;
|
||||
m_captchas.TryGetValue(token ?? string.Empty, out tp);
|
||||
if (tp == null)
|
||||
return false;
|
||||
|
||||
if (tp.Attempts > 0)
|
||||
tp.Attempts--;
|
||||
|
||||
return tp.Attempts >= 0 && string.Equals(tp.Answer, answer, StringComparison.OrdinalIgnoreCase) && tp.Target == target && tp.Expires >= DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
info.ReportClientError("Missing token value", System.Net.HttpStatusCode.Unauthorized);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
string answer = null;
|
||||
lock (m_lock)
|
||||
{
|
||||
CaptchaEntry tp;
|
||||
m_captchas.TryGetValue(key, out tp);
|
||||
if (tp != null && tp.Expires > DateTime.Now)
|
||||
answer = tp.Answer;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(answer))
|
||||
{
|
||||
info.ReportClientError("No such entry", System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
using (var bmp = CaptchaUtil.CreateCaptcha(answer))
|
||||
using (var ms = new System.IO.MemoryStream())
|
||||
{
|
||||
info.Response.ContentType = "image/jpeg";
|
||||
info.Response.ContentLength = ms.Length;
|
||||
bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
|
||||
ms.Position = 0;
|
||||
|
||||
info.Response.ContentType = "image/jpeg";
|
||||
info.Response.ContentLength = ms.Length;
|
||||
info.Response.SendHeaders();
|
||||
ms.CopyTo(info.Response.Body);
|
||||
info.Response.Send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
var target = info.Request.Param["target"].Value;
|
||||
if (string.IsNullOrWhiteSpace(target))
|
||||
{
|
||||
info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
var answer = CaptchaUtil.CreateRandomAnswer(minlength: 6, maxlength: 6);
|
||||
var nonce = Guid.NewGuid().ToString();
|
||||
|
||||
string token;
|
||||
using (var ms = new System.IO.MemoryStream())
|
||||
{
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(answer + nonce);
|
||||
ms.Write(bytes, 0, bytes.Length);
|
||||
ms.Position = 0;
|
||||
using(var hasher = Library.Utility.HashFactory.CreateHasher(Library.Utility.HashFactory.SHA256)){
|
||||
token = Library.Utility.Utility.Base64PlainToBase64Url(Convert.ToBase64String(hasher.ComputeHash(ms)));
|
||||
}
|
||||
}
|
||||
|
||||
lock (m_lock)
|
||||
{
|
||||
var expired = m_captchas.Where(x => x.Value.Expires < DateTime.Now).Select(x => x.Key).ToArray();
|
||||
foreach (var x in expired)
|
||||
m_captchas.Remove(x);
|
||||
|
||||
if (m_captchas.Count > 3)
|
||||
{
|
||||
info.ReportClientError("Too many captchas, wait 2 minutes and try again", System.Net.HttpStatusCode.ServiceUnavailable);
|
||||
return;
|
||||
}
|
||||
|
||||
m_captchas[token] = new CaptchaEntry(answer, target);
|
||||
}
|
||||
|
||||
info.OutputOK(new
|
||||
{
|
||||
token = token
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var answer = info.Request.Param["answer"].Value;
|
||||
var target = info.Request.Param["target"].Value;
|
||||
if (string.IsNullOrWhiteSpace(answer))
|
||||
{
|
||||
info.ReportClientError("Missing answer parameter", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(target))
|
||||
{
|
||||
info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
if (SolvedCaptcha(key, target, answer))
|
||||
info.OutputOK();
|
||||
else
|
||||
info.ReportClientError("Incorrect", System.Net.HttpStatusCode.Forbidden);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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);
|
||||
+293
-288
@@ -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<CommandLine>();
|
||||
|
||||
private class LogWriter : System.IO.TextWriter
|
||||
{
|
||||
private readonly ActiveRun m_target;
|
||||
private readonly StringBuilder m_sb = new StringBuilder();
|
||||
private int m_newlinechars = 0;
|
||||
|
||||
public LogWriter(ActiveRun target)
|
||||
{
|
||||
m_target = target;
|
||||
}
|
||||
|
||||
public override Encoding Encoding { get { return System.Text.Encoding.UTF8; } }
|
||||
|
||||
public override void Write(char value)
|
||||
{
|
||||
lock(m_target.Lock)
|
||||
{
|
||||
m_sb.Append(value);
|
||||
if (NewLine[m_newlinechars] == value)
|
||||
{
|
||||
m_newlinechars++;
|
||||
if (m_newlinechars == NewLine.Length)
|
||||
WriteLine(string.Empty);
|
||||
}
|
||||
else
|
||||
m_newlinechars = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteLine(string value)
|
||||
{
|
||||
value = value ?? string.Empty;
|
||||
lock(m_target.Lock)
|
||||
{
|
||||
m_target.LastAccess = DateTime.Now;
|
||||
|
||||
//Avoid writing the log if it does not exist
|
||||
if (m_target.IsLogDisposed)
|
||||
{
|
||||
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<string, ActiveRun> m_activeItems = new Dictionary<string, ActiveRun>();
|
||||
private System.Threading.Tasks.Task m_cleanupTask;
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
string[] args;
|
||||
using (var sr = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
|
||||
args = Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>(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<string>();
|
||||
long count;
|
||||
bool started;
|
||||
bool finished;
|
||||
|
||||
lock(t.Lock)
|
||||
{
|
||||
t.LastAccess = DateTime.Now;
|
||||
count = t.Log.Count;
|
||||
offset = Math.Min((int)count, offset);
|
||||
items.AddRange(t.Log.Skip(offset).Take(pagesize));
|
||||
finished = t.Finished;
|
||||
started = t.Started;
|
||||
}
|
||||
|
||||
info.OutputOK(new
|
||||
{
|
||||
Pagesize = pagesize,
|
||||
Offset = offset,
|
||||
Count = count,
|
||||
Items = items,
|
||||
Finished = finished,
|
||||
Started = started
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.using Duplicati.Library.RestAPI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer.RESTMethods
|
||||
{
|
||||
public class CommandLine : IRESTMethodGET, IRESTMethodPOST
|
||||
{
|
||||
private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType<CommandLine>();
|
||||
|
||||
private class LogWriter : System.IO.TextWriter
|
||||
{
|
||||
private readonly ActiveRun m_target;
|
||||
private readonly StringBuilder m_sb = new StringBuilder();
|
||||
private int m_newlinechars = 0;
|
||||
|
||||
public LogWriter(ActiveRun target)
|
||||
{
|
||||
m_target = target;
|
||||
}
|
||||
|
||||
public override Encoding Encoding { get { return System.Text.Encoding.UTF8; } }
|
||||
|
||||
public override void Write(char value)
|
||||
{
|
||||
lock(m_target.Lock)
|
||||
{
|
||||
m_sb.Append(value);
|
||||
if (NewLine[m_newlinechars] == value)
|
||||
{
|
||||
m_newlinechars++;
|
||||
if (m_newlinechars == NewLine.Length)
|
||||
WriteLine(string.Empty);
|
||||
}
|
||||
else
|
||||
m_newlinechars = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteLine(string value)
|
||||
{
|
||||
value = value ?? string.Empty;
|
||||
lock(m_target.Lock)
|
||||
{
|
||||
m_target.LastAccess = DateTime.Now;
|
||||
|
||||
//Avoid writing the log if it does not exist
|
||||
if (m_target.IsLogDisposed)
|
||||
{
|
||||
FIXMEGlobal.LogHandler.WriteMessage(new Library.Logging.LogEntry("Attempted to write message after closing: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", null));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (m_sb.Length != 0)
|
||||
{
|
||||
m_target.Log.Add(m_sb + value);
|
||||
m_sb.Length = 0;
|
||||
m_newlinechars = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_target.Log.Add(value);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// This can happen on a very unlucky race where IsLogDisposed is set right after the check
|
||||
FIXMEGlobal.LogHandler.WriteMessage(new Library.Logging.LogEntry("Failed to forward commandline message: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ActiveRun
|
||||
{
|
||||
public readonly string ID = Guid.NewGuid().ToString();
|
||||
public DateTime LastAccess = DateTime.Now;
|
||||
public readonly Library.Utility.FileBackedStringList Log = new Library.Utility.FileBackedStringList();
|
||||
public Runner.IRunnerData Task;
|
||||
public LogWriter Writer;
|
||||
public readonly object Lock = new object();
|
||||
public bool Finished = false;
|
||||
public bool Started = false;
|
||||
public bool IsLogDisposed = false;
|
||||
public System.Threading.Thread Thread;
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, ActiveRun> m_activeItems = new Dictionary<string, ActiveRun>();
|
||||
private System.Threading.Tasks.Task m_cleanupTask;
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
string[] args;
|
||||
var str = System.Threading.Tasks.Task.Run(async () =>
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
|
||||
|
||||
return await sr.ReadToEndAsync();
|
||||
}).GetAwaiter().GetResult();
|
||||
args = Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>(str);
|
||||
|
||||
var k = new ActiveRun();
|
||||
k.Writer = new LogWriter(k);
|
||||
|
||||
m_activeItems[k.ID] = k;
|
||||
StartCleanupTask();
|
||||
|
||||
k.Task = Runner.CreateCustomTask((sink) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
k.Thread = System.Threading.Thread.CurrentThread;
|
||||
k.Started = true;
|
||||
|
||||
var code = Duplicati.CommandLine.Program.RunCommandLine(k.Writer, k.Writer, c => {
|
||||
k.Task.SetController(c);
|
||||
c.AppendSink(sink);
|
||||
}, args);
|
||||
k.Writer.WriteLine("Return code: {0}", code);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var rx = ex;
|
||||
if (rx is System.Reflection.TargetInvocationException)
|
||||
rx = rx.InnerException;
|
||||
|
||||
if (rx is Library.Interface.UserInformationException)
|
||||
k.Log.Add(rx.Message);
|
||||
else
|
||||
k.Log.Add(rx.ToString());
|
||||
|
||||
throw rx;
|
||||
}
|
||||
finally
|
||||
{
|
||||
k.Finished = true;
|
||||
k.Thread = null;
|
||||
}
|
||||
});
|
||||
|
||||
FIXMEGlobal.WorkThread.AddTask(k.Task);
|
||||
|
||||
info.OutputOK(new
|
||||
{
|
||||
ID = k.ID
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!key.EndsWith("/abort", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
info.ReportClientError("Only abort commands are allowed", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
key = key.Substring(0, key.Length - "/abort".Length);
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
info.ReportClientError("No task key found", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
ActiveRun t;
|
||||
if (!m_activeItems.TryGetValue(key, out t))
|
||||
{
|
||||
info.OutputError(code: System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
var tt = t.Task;
|
||||
if (tt != null)
|
||||
tt.Abort();
|
||||
|
||||
var tr = t.Thread;
|
||||
if (tr != null)
|
||||
tr.Interrupt();
|
||||
|
||||
info.OutputOK();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartCleanupTask()
|
||||
{
|
||||
if (m_cleanupTask == null || m_cleanupTask.IsCompleted || m_cleanupTask.IsFaulted || m_cleanupTask.IsCanceled)
|
||||
m_cleanupTask = RunCleanupAsync();
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task RunCleanupAsync()
|
||||
{
|
||||
while (m_activeItems.Count > 0)
|
||||
{
|
||||
var oldest = m_activeItems.Values
|
||||
.OrderBy(x => x.LastAccess)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (oldest != null)
|
||||
{
|
||||
// If the task has finished, we just wait a little to allow the UI to pick it up
|
||||
var timeout = oldest.Finished ? TimeSpan.FromMinutes(5) : TimeSpan.FromDays(1);
|
||||
if (DateTime.Now - oldest.LastAccess > timeout)
|
||||
{
|
||||
oldest.IsLogDisposed = true;
|
||||
m_activeItems.Remove(oldest.ID);
|
||||
oldest.Log.Dispose();
|
||||
|
||||
// Fix all expired, or stop running
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
await System.Threading.Tasks.Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void GET(string key, RequestInfo info)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
info.OutputOK(
|
||||
Duplicati.CommandLine.Program.SupportedCommands
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
ActiveRun t;
|
||||
if (!m_activeItems.TryGetValue(key, out t))
|
||||
{
|
||||
info.OutputError(code: System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
int pagesize;
|
||||
int offset;
|
||||
|
||||
int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize);
|
||||
int.TryParse(info.Request.QueryString["offset"].Value, out offset);
|
||||
pagesize = Math.Max(10, Math.Min(500, pagesize));
|
||||
offset = Math.Max(0, offset);
|
||||
var items = new List<string>();
|
||||
long count;
|
||||
bool started;
|
||||
bool finished;
|
||||
|
||||
lock(t.Lock)
|
||||
{
|
||||
t.LastAccess = DateTime.Now;
|
||||
count = t.Log.Count;
|
||||
offset = Math.Min((int)count, offset);
|
||||
items.AddRange(t.Log.Skip(offset).Take(pagesize));
|
||||
finished = t.Finished;
|
||||
started = t.Started;
|
||||
}
|
||||
|
||||
info.OutputOK(new
|
||||
{
|
||||
Pagesize = pagesize,
|
||||
Offset = offset,
|
||||
Count = count,
|
||||
Items = items,
|
||||
Finished = finished,
|
||||
Started = started
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
@@ -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;
|
||||
-1
@@ -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;
|
||||
-1
@@ -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;
|
||||
-1
@@ -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;
|
||||
|
||||
+11
-2
@@ -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));
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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<Dictionary<string, object>> 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);
|
||||
});
|
||||
-1
@@ -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;
|
||||
+4
-4
@@ -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();
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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."; } }
|
||||
+86
-24
@@ -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<string, string> ParseUrlOptions(Library.Utility.Uri uri)
|
||||
{
|
||||
var qp = uri.QueryParameters;
|
||||
|
||||
var opts = Runner.GetCommonOptions();
|
||||
foreach (var k in qp.Keys.Cast<string>())
|
||||
opts[k] = qp[k];
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
private IEnumerable<IGenericModule> ConfigureModules(IDictionary<string, string> opts)
|
||||
{
|
||||
// TODO: This works because the generic modules are implemented
|
||||
// with pre .NetCore logic, using static methods
|
||||
// The modules are created to allow multipe dispose,
|
||||
// which violates the .Net patterns
|
||||
var modules = (from n in Library.DynamicLoader.GenericLoader.Modules
|
||||
where n is Library.Interface.IConnectionModule
|
||||
select n).ToArray();
|
||||
|
||||
foreach (var n in modules)
|
||||
n.Configure(opts);
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
|
||||
private class TupleDisposeWrapper : IDisposable
|
||||
{
|
||||
public IBackend Backend { get; set; }
|
||||
public IEnumerable<IGenericModule> Modules { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Backend.Dispose();
|
||||
DisposeModules();
|
||||
}
|
||||
|
||||
public void DisposeModules()
|
||||
{
|
||||
foreach (var n in Modules)
|
||||
if (n is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private TupleDisposeWrapper GetBackend(string url)
|
||||
{
|
||||
var uri = new Library.Utility.Uri(url);
|
||||
var opts = ParseUrlOptions(uri);
|
||||
var modules = ConfigureModules(opts);
|
||||
var backend = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(url, new Dictionary<string, string>());
|
||||
return new TupleDisposeWrapper() { Backend = backend, Modules = modules };
|
||||
}
|
||||
|
||||
private void CreateFolder(string uri, RequestInfo info)
|
||||
{
|
||||
using(var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary<string, string>()))
|
||||
@@ -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<string, string>()))
|
||||
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<string, string>()))
|
||||
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<string, string>();
|
||||
foreach (var k in qp.Keys.Cast<string>())
|
||||
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<string, string>()))
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -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
|
||||
+7
-7
@@ -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<string, string>();
|
||||
dict[key] = info.Request.Form["data"].Value;
|
||||
Program.DataConnection.ApplicationSettings.UpdateSettings(dict, false);
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.UpdateSettings(dict, false);
|
||||
info.OutputOK();
|
||||
}
|
||||
}
|
||||
+136
-131
@@ -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<string, string>();
|
||||
foreach (var n in adv_props)
|
||||
dict[n.Name] = n.Value;
|
||||
|
||||
string sslcert;
|
||||
dict.TryGetValue("server-ssl-certificate", out sslcert);
|
||||
dict["server-ssl-certificate"] = (!string.IsNullOrWhiteSpace(sslcert)).ToString();
|
||||
|
||||
info.OutputOK(dict);
|
||||
}
|
||||
|
||||
public void PATCH(string key, RequestInfo info)
|
||||
{
|
||||
string str = info.Request.Form["data"].Value;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
str = 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<string, string> data = null;
|
||||
try
|
||||
{
|
||||
data = Serializer.Deserialize<Dictionary<string, string>>(new StringReader(str));
|
||||
if (data == null)
|
||||
{
|
||||
info.ReportClientError("Data object had no entry", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Split into server settings and global settings
|
||||
|
||||
var serversettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key)).ToDictionary(x => x.Key, x => x.Key.StartsWith("--", StringComparison.Ordinal) ? null : x.Value);
|
||||
var globalsettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Key.StartsWith("--", StringComparison.Ordinal));
|
||||
|
||||
serversettings.Remove("server-ssl-certificate");
|
||||
serversettings.Remove("ServerSSLCertificate");
|
||||
|
||||
if (serversettings.Any())
|
||||
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<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(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<string, string>();
|
||||
foreach (var n in adv_props)
|
||||
dict[n.Name] = n.Value;
|
||||
|
||||
string sslcert;
|
||||
dict.TryGetValue("server-ssl-certificate", out sslcert);
|
||||
dict["server-ssl-certificate"] = (!string.IsNullOrWhiteSpace(sslcert)).ToString();
|
||||
|
||||
info.OutputOK(dict);
|
||||
}
|
||||
|
||||
public void PATCH(string key, RequestInfo info)
|
||||
{
|
||||
string str = info.Request.Form["data"].Value;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
str = System.Threading.Tasks.Task.Run(async () =>
|
||||
{
|
||||
using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
|
||||
|
||||
return await sr.ReadToEndAsync();
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
{
|
||||
info.ReportClientError("Missing data object", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, string> data = null;
|
||||
try
|
||||
{
|
||||
data = Serializer.Deserialize<Dictionary<string, string>>(new StringReader(str));
|
||||
if (data == null)
|
||||
{
|
||||
info.ReportClientError("Data object had no entry", System.Net.HttpStatusCode.BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Split into server settings and global settings
|
||||
|
||||
var serversettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key)).ToDictionary(x => x.Key, x => x.Key.StartsWith("--", StringComparison.Ordinal) ? null : x.Value);
|
||||
var globalsettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Key.StartsWith("--", StringComparison.Ordinal));
|
||||
|
||||
serversettings.Remove("server-ssl-certificate");
|
||||
serversettings.Remove("ServerSSLCertificate");
|
||||
|
||||
if (serversettings.Any())
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.UpdateSettings(serversettings, false);
|
||||
|
||||
if (globalsettings.Any())
|
||||
{
|
||||
// Update based on inputs
|
||||
var existing = FIXMEGlobal.DataConnection.Settings.ToDictionary(x => x.Name, x => x);
|
||||
foreach (var g in globalsettings)
|
||||
if (g.Value == null)
|
||||
existing.Remove(g.Key);
|
||||
else
|
||||
{
|
||||
if (existing.ContainsKey(g.Key))
|
||||
existing[g.Key].Value = g.Value;
|
||||
else
|
||||
existing[g.Key] = new Setting() { Name = g.Key, Value = g.Value };
|
||||
}
|
||||
|
||||
FIXMEGlobal.DataConnection.Settings = existing.Select(x => x.Value).ToArray();
|
||||
}
|
||||
|
||||
info.OutputOK();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (data == null)
|
||||
info.ReportClientError(string.Format("Unable to parse data object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest);
|
||||
else
|
||||
info.ReportClientError(string.Format("Unable to save settings: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
public string Description { get { return "Return a list of settings for the server"; } }
|
||||
|
||||
public IEnumerable<KeyValuePair<string, Type>> Types
|
||||
{
|
||||
get
|
||||
{
|
||||
return new KeyValuePair<string, Type>[] {
|
||||
new KeyValuePair<string, Type>(HttpServer.Method.Get, typeof(Database.ServerSettings))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -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;
|
||||
|
||||
+4
-26
@@ -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<KeyValuePair<string, Type>> 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
|
||||
{
|
||||
-1
@@ -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;
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
|
||||
+93
-93
@@ -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<long, Exception>[] 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<long, Exception>[] matches;
|
||||
lock(FIXMEGlobal.MainLock)
|
||||
matches = FIXMEGlobal.TaskResultCache.Where(x => x.Key == taskid).ToArray();
|
||||
|
||||
if (matches.Length == 0)
|
||||
info.ReportClientError("No such task found", System.Net.HttpStatusCode.NotFound);
|
||||
else
|
||||
info.OutputOK(new {
|
||||
Status = matches[0].Value == null ? "Completed" : "Failed",
|
||||
ErrorMessage = matches[0].Value == null ? null : matches[0].Value.Message,
|
||||
Exception = matches[0].Value == null ? null : matches[0].Value.ToString()
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
info.OutputOK(new { Status = "Waiting" });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest);
|
||||
}
|
||||
}
|
||||
|
||||
public void POST(string key, RequestInfo info)
|
||||
{
|
||||
var parts = (key ?? "").Split(new char[] { '/' }, 2);
|
||||
long taskid;
|
||||
if (parts.Length == 2 && long.TryParse(parts.First(), out taskid))
|
||||
{
|
||||
var task = FIXMEGlobal.WorkThread.CurrentTask;
|
||||
var tasks = FIXMEGlobal.WorkThread.CurrentTasks;
|
||||
|
||||
if (task != null)
|
||||
tasks.Insert(0, task);
|
||||
|
||||
task = tasks.FirstOrDefault(x => x.TaskID == taskid);
|
||||
if (task == null)
|
||||
{
|
||||
info.ReportClientError("No such task", System.Net.HttpStatusCode.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (parts.Last().ToLowerInvariant())
|
||||
{
|
||||
case "stopaftercurrentfile":
|
||||
task.Stop(allowCurrentFileToFinish: true);
|
||||
info.OutputOK();
|
||||
return;
|
||||
|
||||
case "stopnow":
|
||||
task.Stop(allowCurrentFileToFinish: false);
|
||||
info.OutputOK();
|
||||
return;
|
||||
|
||||
case "abort":
|
||||
task.Abort();
|
||||
info.OutputOK();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
info.ReportClientError("Invalid or missing task id", System.Net.HttpStatusCode.NotFound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
+5
-5
@@ -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();
|
||||
}
|
||||
|
||||
+2
-19
@@ -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);
|
||||
-1
@@ -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;
|
||||
|
||||
-1
@@ -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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This class handles scheduled runs of backups
|
||||
/// </summary>
|
||||
public class Scheduler
|
||||
{
|
||||
private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType<Scheduler>();
|
||||
|
||||
/// <summary>
|
||||
/// The thread that runs the scheduler
|
||||
/// </summary>
|
||||
private readonly Thread m_thread;
|
||||
/// <summary>
|
||||
/// A termination flag
|
||||
/// </summary>
|
||||
private volatile bool m_terminate;
|
||||
/// <summary>
|
||||
/// The worker thread that is invoked to do work
|
||||
/// </summary>
|
||||
private readonly WorkerThread<Runner.IRunnerData> m_worker;
|
||||
/// <summary>
|
||||
/// The wait event
|
||||
/// </summary>
|
||||
private readonly AutoResetEvent m_event;
|
||||
/// <summary>
|
||||
/// The data synchronization lock
|
||||
/// </summary>
|
||||
private readonly object m_lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// An event that is raised when the schedule changes
|
||||
/// </summary>
|
||||
public event EventHandler NewSchedule;
|
||||
|
||||
/// <summary>
|
||||
/// The currently scheduled items
|
||||
/// </summary>
|
||||
private KeyValuePair<DateTime, ISchedule>[] m_schedule;
|
||||
|
||||
/// <summary>
|
||||
/// List of update tasks, used to set the timestamp on the schedule once completed
|
||||
/// </summary>
|
||||
private readonly Dictionary<Server.Runner.IRunnerData, Tuple<ISchedule, DateTime, DateTime>> m_updateTasks;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new scheduler
|
||||
/// </summary>
|
||||
/// <param name="worker">The worker thread</param>
|
||||
public Scheduler(WorkerThread<Server.Runner.IRunnerData> worker)
|
||||
{
|
||||
m_thread = new Thread(new ThreadStart(Runner));
|
||||
m_worker = worker;
|
||||
m_worker.CompletedWork += OnCompleted;
|
||||
m_worker.StartingWork += OnStartingWork;
|
||||
m_schedule = new KeyValuePair<DateTime, ISchedule>[0];
|
||||
m_terminate = false;
|
||||
m_event = new AutoResetEvent(false);
|
||||
m_updateTasks = new Dictionary<Server.Runner.IRunnerData, Tuple<ISchedule, DateTime, DateTime>>();
|
||||
m_thread.IsBackground = true;
|
||||
m_thread.Name = "TaskScheduler";
|
||||
m_thread.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces the scheduler to re-evaluate the order.
|
||||
/// Call this method if something changes
|
||||
/// </summary>
|
||||
public void Reschedule()
|
||||
{
|
||||
m_event.Set();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A snapshot copy of the current schedule list
|
||||
/// </summary>
|
||||
public List<KeyValuePair<DateTime, ISchedule>> Schedule
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (m_lock)
|
||||
return m_schedule.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A snapshot copy of the current worker queue, that is items that are scheduled, but waiting for execution
|
||||
/// </summary>
|
||||
public List<Runner.IRunnerData> WorkerQueue
|
||||
{
|
||||
get
|
||||
{
|
||||
return (from t in m_worker.CurrentTasks where t != null select t).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the thread. Any items still in queue will be removed
|
||||
/// </summary>
|
||||
/// <param name="wait">True if the call should block until the thread has exited, false otherwise</param>
|
||||
public void Terminate(bool wait)
|
||||
{
|
||||
m_terminate = true;
|
||||
m_event.Set();
|
||||
|
||||
if (wait)
|
||||
m_thread.Join();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next valid date, given the start and the interval
|
||||
/// </summary>
|
||||
/// <param name="basetime">The base time</param>
|
||||
/// <param name="firstdate">The first allowed date</param>
|
||||
/// <param name="repetition">The repetition interval</param>
|
||||
/// <param name="allowedDays">The days the backup is allowed to run</param>
|
||||
/// <returns>The next valid date, or throws an exception if no such date can be found</returns>
|
||||
public static DateTime GetNextValidTime(DateTime basetime, DateTime firstdate, string repetition, DayOfWeek[] allowedDays)
|
||||
{
|
||||
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<Runner.IRunnerData> worker, Runner.IRunnerData task)
|
||||
{
|
||||
Tuple<ISchedule, DateTime, DateTime> 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<Runner.IRunnerData> worker, Runner.IRunnerData task)
|
||||
{
|
||||
if (task is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock(m_lock)
|
||||
{
|
||||
if (m_updateTasks.TryGetValue(task, out Tuple<ISchedule, DateTime, DateTime> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The actual scheduling procedure
|
||||
/// </summary>
|
||||
private void Runner()
|
||||
{
|
||||
var scheduled = new Dictionary<long, KeyValuePair<long, DateTime>>();
|
||||
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<long, DateTime> 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<Server.Runner.IRunnerData>();
|
||||
//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<string, string> options = Duplicati.Server.Runner.GetCommonOptions();
|
||||
Duplicati.Server.Runner.ApplyOptions(entry, options);
|
||||
if ((new Duplicati.Library.Main.Options(options)).DisableOnBattery && (Duplicati.Library.Utility.Power.PowerSupply.GetSource() == Duplicati.Library.Utility.Power.PowerSupply.Source.Battery))
|
||||
{
|
||||
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<ISchedule, DateTime, DateTime>(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<long,DateTime>(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<DateTime, ISchedule>(n.Value.Value, existing[n.Key])).ToArray();
|
||||
|
||||
// Remove unused entries
|
||||
foreach(var c in (from n in scheduled where !existing.ContainsKey(n.Key) select n.Key).ToArray())
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the time is at an allowed weekday, false otherwise
|
||||
/// </summary>
|
||||
/// <param name="time">The time to evaluate</param>
|
||||
/// <param name="allowedDays">The allowed days</param>
|
||||
/// <returns>True if the backup is allowed to run, false otherwise</returns>
|
||||
private static bool IsDateAllowed(DateTime time, DayOfWeek[] allowedDays)
|
||||
{
|
||||
var localTime = time.ToLocalTime();
|
||||
if (allowedDays == null || allowedDays.Length == 0)
|
||||
return true;
|
||||
else
|
||||
return Array.IndexOf<DayOfWeek>(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
|
||||
{
|
||||
/// <summary>
|
||||
/// This class handles scheduled runs of backups
|
||||
/// </summary>
|
||||
public class Scheduler
|
||||
{
|
||||
private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType<Scheduler>();
|
||||
|
||||
/// <summary>
|
||||
/// The thread that runs the scheduler
|
||||
/// </summary>
|
||||
private readonly Thread m_thread;
|
||||
/// <summary>
|
||||
/// A termination flag
|
||||
/// </summary>
|
||||
private volatile bool m_terminate;
|
||||
/// <summary>
|
||||
/// The worker thread that is invoked to do work
|
||||
/// </summary>
|
||||
private readonly WorkerThread<Runner.IRunnerData> m_worker;
|
||||
/// <summary>
|
||||
/// The wait event
|
||||
/// </summary>
|
||||
private readonly AutoResetEvent m_event;
|
||||
/// <summary>
|
||||
/// The data synchronization lock
|
||||
/// </summary>
|
||||
private readonly object m_lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// An event that is raised when the schedule changes
|
||||
/// </summary>
|
||||
public event EventHandler NewSchedule;
|
||||
|
||||
/// <summary>
|
||||
/// The currently scheduled items
|
||||
/// </summary>
|
||||
private KeyValuePair<DateTime, ISchedule>[] m_schedule;
|
||||
|
||||
/// <summary>
|
||||
/// List of update tasks, used to set the timestamp on the schedule once completed
|
||||
/// </summary>
|
||||
private readonly Dictionary<Server.Runner.IRunnerData, Tuple<ISchedule, DateTime, DateTime>> m_updateTasks;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new scheduler
|
||||
/// </summary>
|
||||
/// <param name="worker">The worker thread</param>
|
||||
public Scheduler(WorkerThread<Server.Runner.IRunnerData> worker)
|
||||
{
|
||||
m_thread = new Thread(new ThreadStart(Runner));
|
||||
m_worker = worker;
|
||||
m_worker.CompletedWork += OnCompleted;
|
||||
m_worker.StartingWork += OnStartingWork;
|
||||
m_schedule = new KeyValuePair<DateTime, ISchedule>[0];
|
||||
m_terminate = false;
|
||||
m_event = new AutoResetEvent(false);
|
||||
m_updateTasks = new Dictionary<Server.Runner.IRunnerData, Tuple<ISchedule, DateTime, DateTime>>();
|
||||
m_thread.IsBackground = true;
|
||||
m_thread.Name = "TaskScheduler";
|
||||
m_thread.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forces the scheduler to re-evaluate the order.
|
||||
/// Call this method if something changes
|
||||
/// </summary>
|
||||
public void Reschedule()
|
||||
{
|
||||
m_event.Set();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A snapshot copy of the current schedule list
|
||||
/// </summary>
|
||||
public List<KeyValuePair<DateTime, ISchedule>> Schedule
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (m_lock)
|
||||
return m_schedule.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A snapshot copy of the current worker queue, that is items that are scheduled, but waiting for execution
|
||||
/// </summary>
|
||||
public List<Runner.IRunnerData> WorkerQueue
|
||||
{
|
||||
get
|
||||
{
|
||||
return (from t in m_worker.CurrentTasks where t != null select t).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the thread. Any items still in queue will be removed
|
||||
/// </summary>
|
||||
/// <param name="wait">True if the call should block until the thread has exited, false otherwise</param>
|
||||
public void Terminate(bool wait)
|
||||
{
|
||||
m_terminate = true;
|
||||
m_event.Set();
|
||||
|
||||
if (wait)
|
||||
{
|
||||
try { m_thread.Join(); }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next valid date, given the start and the interval
|
||||
/// </summary>
|
||||
/// <param name="basetime">The base time</param>
|
||||
/// <param name="firstdate">The first allowed date</param>
|
||||
/// <param name="repetition">The repetition interval</param>
|
||||
/// <param name="allowedDays">The days the backup is allowed to run</param>
|
||||
/// <returns>The next valid date, or throws an exception if no such date can be found</returns>
|
||||
public static DateTime GetNextValidTime(DateTime basetime, DateTime firstdate, string repetition, DayOfWeek[] allowedDays)
|
||||
{
|
||||
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<Runner.IRunnerData> worker, Runner.IRunnerData task)
|
||||
{
|
||||
Tuple<ISchedule, DateTime, DateTime> 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;
|
||||
FIXMEGlobal.DataConnection.AddOrUpdateSchedule(t.Item1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void OnStartingWork(WorkerThread<Runner.IRunnerData> worker, Runner.IRunnerData task)
|
||||
{
|
||||
if (task is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock(m_lock)
|
||||
{
|
||||
if (m_updateTasks.TryGetValue(task, out Tuple<ISchedule, DateTime, DateTime> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The actual scheduling procedure
|
||||
/// </summary>
|
||||
private void Runner()
|
||||
{
|
||||
var scheduled = new Dictionary<long, KeyValuePair<long, DateTime>>();
|
||||
while (!m_terminate)
|
||||
{
|
||||
//TODO: As this is executed repeatedly we should cache it
|
||||
// to avoid frequent db lookups
|
||||
|
||||
//Determine schedule list
|
||||
var lst = FIXMEGlobal.DataConnection.Schedules;
|
||||
foreach(var sc in lst)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(sc.Repeat))
|
||||
{
|
||||
KeyValuePair<long, DateTime> 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)
|
||||
{
|
||||
FIXMEGlobal.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<Server.Runner.IRunnerData>();
|
||||
//TODO: Cache this to avoid frequent lookups
|
||||
foreach(var id in FIXMEGlobal.DataConnection.GetBackupIDsForTags(sc.Tags).Distinct().Select(x => x.ToString()))
|
||||
{
|
||||
//See if it is already queued
|
||||
var tmplst = from n in m_worker.CurrentTasks
|
||||
where n.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup
|
||||
select n.Backup;
|
||||
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 = FIXMEGlobal.DataConnection.GetBackup(id);
|
||||
if (entry != null)
|
||||
{
|
||||
Dictionary<string, string> options = Duplicati.Server.Runner.GetCommonOptions();
|
||||
Duplicati.Server.Runner.ApplyOptions(entry, options);
|
||||
if ((new Duplicati.Library.Main.Options(options)).DisableOnBattery && (Duplicati.Library.Utility.Power.PowerSupply.GetSource() == Duplicati.Library.Utility.Power.PowerSupply.Source.Battery))
|
||||
{
|
||||
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)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex);
|
||||
continue;
|
||||
}
|
||||
|
||||
Server.Runner.IRunnerData lastJob = jobsToRun.LastOrDefault();
|
||||
if (lastJob != null)
|
||||
{
|
||||
lock (m_lock)
|
||||
{
|
||||
// The actual last run time will be updated when the StartingWork event is raised.
|
||||
m_updateTasks[lastJob] = new Tuple<ISchedule, DateTime, DateTime>(sc, start, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var job in jobsToRun)
|
||||
m_worker.AddTask(job);
|
||||
|
||||
if (start < DateTime.UtcNow)
|
||||
{
|
||||
//TODO: Report this somehow
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
scheduled[sc.ID] = new KeyValuePair<long,DateTime>(scticks, start);
|
||||
}
|
||||
}
|
||||
|
||||
var existing = lst.ToDictionary(x => x.ID);
|
||||
//Sort them, lock as we assign the m_schedule variable
|
||||
lock(m_lock)
|
||||
m_schedule = (from n in scheduled
|
||||
where existing.ContainsKey(n.Key)
|
||||
orderby n.Value.Value
|
||||
select new KeyValuePair<DateTime, ISchedule>(n.Value.Value, existing[n.Key])).ToArray();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the time is at an allowed weekday, false otherwise
|
||||
/// </summary>
|
||||
/// <param name="time">The time to evaluate</param>
|
||||
/// <param name="allowedDays">The allowed days</param>
|
||||
/// <returns>True if the backup is allowed to run, false otherwise</returns>
|
||||
private static bool IsDateAllowed(DateTime time, DayOfWeek[] allowedDays)
|
||||
{
|
||||
var localTime = time.ToLocalTime();
|
||||
if (allowedDays == null || allowedDays.Length == 0)
|
||||
return true;
|
||||
else
|
||||
return Array.IndexOf<DayOfWeek>(allowedDays, localTime.DayOfWeek) >= 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
-1
@@ -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.Collections.Generic;
|
||||
|
||||
namespace Duplicati.Server.Serializable
|
||||
+242
-242
@@ -1,242 +1,242 @@
|
||||
// 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.Server.Serialization.Interface;
|
||||
|
||||
namespace Duplicati.Server.Serializable
|
||||
{
|
||||
/// <summary>
|
||||
/// The server config
|
||||
/// </summary>
|
||||
public static class ServerSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared implementation for reporting dynamic modules
|
||||
/// </summary>
|
||||
private class DynamicModule : IDynamicModule
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor for backend interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.IBackend backend)
|
||||
{
|
||||
this.Key = backend.ProtocolKey;
|
||||
this.Description = backend.Description;
|
||||
this.DisplayName = backend.DisplayName;
|
||||
if (backend.SupportedCommands != null)
|
||||
this.Options = backend.SupportedCommands.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for compression module interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.ICompression module)
|
||||
{
|
||||
this.Key = module.FilenameExtension;
|
||||
this.Description = module.Description;
|
||||
this.DisplayName = module.DisplayName;
|
||||
if (module.SupportedCommands != null)
|
||||
this.Options = module.SupportedCommands.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for encryption module interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.IEncryption module)
|
||||
{
|
||||
this.Key = module.FilenameExtension;
|
||||
this.Description = module.Description;
|
||||
this.DisplayName = module.DisplayName;
|
||||
if (module.SupportedCommands != null)
|
||||
this.Options = module.SupportedCommands.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for generic module interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.IGenericModule module)
|
||||
{
|
||||
this.Key = module.Key;
|
||||
this.Description = module.Description;
|
||||
this.DisplayName = module.DisplayName;
|
||||
if (module.SupportedCommands != null)
|
||||
this.Options = module.SupportedCommands.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for webmodule interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.IWebModule module)
|
||||
{
|
||||
this.Key = module.Key;
|
||||
this.Description = module.Description;
|
||||
this.DisplayName = module.DisplayName;
|
||||
if (module.SupportedCommands != null)
|
||||
this.Options = module.SupportedCommands.ToArray();
|
||||
}
|
||||
/// <summary>
|
||||
/// The module key
|
||||
/// </summary>
|
||||
public string Key { get; private set; }
|
||||
/// <summary>
|
||||
/// The localized module description
|
||||
/// </summary>
|
||||
public string Description { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets the localized display name
|
||||
/// </summary>
|
||||
/// <value>The display name.</value>
|
||||
public string DisplayName { get; private set; }
|
||||
/// <summary>
|
||||
/// The options supported by the module
|
||||
/// </summary>
|
||||
public Duplicati.Library.Interface.ICommandLineArgument[] Options { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all supported options
|
||||
/// </summary>
|
||||
public static Duplicati.Library.Interface.ICommandLineArgument[] Options
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Duplicati.Library.Main.Options(new System.Collections.Generic.Dictionary<string, string>()).SupportedCommands.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The backend modules known by the server
|
||||
/// </summary>
|
||||
public static IDynamicModule[] BackendModules
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
(from n in Library.DynamicLoader.BackendLoader.Backends
|
||||
select new DynamicModule(n))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// The encryption modules known by the server
|
||||
/// </summary>
|
||||
public static IDynamicModule[] EncryptionModules
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
(from n in Library.DynamicLoader.EncryptionLoader.Modules
|
||||
select new DynamicModule(n))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The compression modules known by the server
|
||||
/// </summary>
|
||||
public static IDynamicModule[] CompressionModules
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
(from n in Library.DynamicLoader.CompressionLoader.Modules
|
||||
select new DynamicModule(n))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The generic modules known by the server
|
||||
/// </summary>
|
||||
public static IDynamicModule[] GenericModules
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
(from n in Library.DynamicLoader.GenericLoader.Modules
|
||||
select new DynamicModule(n))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The web modules known by the server
|
||||
/// </summary>
|
||||
public static IDynamicModule[] WebModules
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
(from n in Library.DynamicLoader.WebLoader.Modules
|
||||
select new DynamicModule(n))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The web modules known by the server
|
||||
/// </summary>
|
||||
public static IDynamicModule[] ConnectionModules
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
(from n in Library.DynamicLoader.GenericLoader.Modules
|
||||
where n is Library.Interface.IConnectionModule
|
||||
select new DynamicModule(n))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The server modules known by the server
|
||||
/// </summary>
|
||||
public static object[] ServerModules
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
(from n in Library.DynamicLoader.GenericLoader.Modules
|
||||
where n is Library.Interface.IGenericServerModule
|
||||
select n)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The filters that are applied to all backups
|
||||
/// </summary>
|
||||
public static IFilter[] Filters
|
||||
{
|
||||
get { return Program.DataConnection.Filters; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The settings applied to all backups by default
|
||||
/// </summary>
|
||||
public static ISetting[] Settings
|
||||
{
|
||||
get { return Program.DataConnection.Settings; }
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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.RestAPI;
|
||||
using Duplicati.Server.Serialization.Interface;
|
||||
|
||||
namespace Duplicati.Server.Serializable
|
||||
{
|
||||
/// <summary>
|
||||
/// The server config
|
||||
/// </summary>
|
||||
public static class ServerSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared implementation for reporting dynamic modules
|
||||
/// </summary>
|
||||
private class DynamicModule : IDynamicModule
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor for backend interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.IBackend backend)
|
||||
{
|
||||
this.Key = backend.ProtocolKey;
|
||||
this.Description = backend.Description;
|
||||
this.DisplayName = backend.DisplayName;
|
||||
if (backend.SupportedCommands != null)
|
||||
this.Options = backend.SupportedCommands.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for compression module interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.ICompression module)
|
||||
{
|
||||
this.Key = module.FilenameExtension;
|
||||
this.Description = module.Description;
|
||||
this.DisplayName = module.DisplayName;
|
||||
if (module.SupportedCommands != null)
|
||||
this.Options = module.SupportedCommands.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for encryption module interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.IEncryption module)
|
||||
{
|
||||
this.Key = module.FilenameExtension;
|
||||
this.Description = module.Description;
|
||||
this.DisplayName = module.DisplayName;
|
||||
if (module.SupportedCommands != null)
|
||||
this.Options = module.SupportedCommands.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for generic module interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.IGenericModule module)
|
||||
{
|
||||
this.Key = module.Key;
|
||||
this.Description = module.Description;
|
||||
this.DisplayName = module.DisplayName;
|
||||
if (module.SupportedCommands != null)
|
||||
this.Options = module.SupportedCommands.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for webmodule interface
|
||||
/// </summary>
|
||||
public DynamicModule(Duplicati.Library.Interface.IWebModule module)
|
||||
{
|
||||
this.Key = module.Key;
|
||||
this.Description = module.Description;
|
||||
this.DisplayName = module.DisplayName;
|
||||
if (module.SupportedCommands != null)
|
||||
this.Options = module.SupportedCommands.ToArray();
|
||||
}
|
||||
/// <summary>
|
||||
/// The module key
|
||||
/// </summary>
|
||||
public string Key { get; private set; }
|
||||
/// <summary>
|
||||
/// The localized module description
|
||||
/// </summary>
|
||||
public string Description { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets the localized display name
|
||||
/// </summary>
|
||||
/// <value>The display name.</value>
|
||||
public string DisplayName { get; private set; }
|
||||
/// <summary>
|
||||
/// The options supported by the module
|
||||
/// </summary>
|
||||
public Duplicati.Library.Interface.ICommandLineArgument[] Options { get; private set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all supported options
|
||||
/// </summary>
|
||||
public static Duplicati.Library.Interface.ICommandLineArgument[] Options
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Duplicati.Library.Main.Options(new System.Collections.Generic.Dictionary<string, string>()).SupportedCommands.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The backend modules known by the server
|
||||
/// </summary>
|
||||
public static IDynamicModule[] BackendModules
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
(from n in Library.DynamicLoader.BackendLoader.Backends
|
||||
select new DynamicModule(n))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// The encryption modules known by the server
|
||||
/// </summary>
|
||||
public static IDynamicModule[] EncryptionModules
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
(from n in Library.DynamicLoader.EncryptionLoader.Modules
|
||||
select new DynamicModule(n))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The compression modules known by the server
|
||||
/// </summary>
|
||||
public static IDynamicModule[] CompressionModules
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
(from n in Library.DynamicLoader.CompressionLoader.Modules
|
||||
select new DynamicModule(n))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The generic modules known by the server
|
||||
/// </summary>
|
||||
public static IDynamicModule[] GenericModules
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
(from n in Library.DynamicLoader.GenericLoader.Modules
|
||||
select new DynamicModule(n))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The web modules known by the server
|
||||
/// </summary>
|
||||
public static IDynamicModule[] WebModules
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
(from n in Library.DynamicLoader.WebLoader.Modules
|
||||
select new DynamicModule(n))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The web modules known by the server
|
||||
/// </summary>
|
||||
public static IDynamicModule[] ConnectionModules
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
(from n in Library.DynamicLoader.GenericLoader.Modules
|
||||
where n is Library.Interface.IConnectionModule
|
||||
select new DynamicModule(n))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The server modules known by the server
|
||||
/// </summary>
|
||||
public static object[] ServerModules
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
(from n in Library.DynamicLoader.GenericLoader.Modules
|
||||
where n is Library.Interface.IGenericServerModule
|
||||
select n)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The filters that are applied to all backups
|
||||
/// </summary>
|
||||
public static IFilter[] Filters
|
||||
{
|
||||
get { return FIXMEGlobal.DataConnection.Filters; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The settings applied to all backups by default
|
||||
/// </summary>
|
||||
public static ISetting[] Settings
|
||||
{
|
||||
get { return FIXMEGlobal.DataConnection.Settings; }
|
||||
}
|
||||
}
|
||||
}
|
||||
+148
-148
@@ -1,148 +1,148 @@
|
||||
// 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;
|
||||
|
||||
namespace Duplicati.Server.Serializable
|
||||
{
|
||||
/// <summary>
|
||||
/// This class collects all reportable status properties into a single class that can be exported as JSON
|
||||
/// </summary>
|
||||
public class ServerStatus : Duplicati.Server.Serialization.Interface.IServerStatus
|
||||
{
|
||||
public LiveControlState ProgramState
|
||||
{
|
||||
get { return EnumConverter.Convert<LiveControlState>(Program.LiveControl.State); }
|
||||
}
|
||||
|
||||
public string UpdatedVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
var u = Program.DataConnection.ApplicationSettings.UpdatedVersion;
|
||||
if (u == null)
|
||||
return null;
|
||||
|
||||
Version v;
|
||||
if (!Version.TryParse(u.Version, out v))
|
||||
return null;
|
||||
|
||||
if (v <= System.Reflection.Assembly.GetExecutingAssembly().GetName().Version)
|
||||
return null;
|
||||
|
||||
return u.Displayname;
|
||||
}
|
||||
}
|
||||
|
||||
public UpdatePollerStates UpdaterState { get { return Program.UpdatePoller.ThreadState; } }
|
||||
|
||||
public bool UpdateReady { get { return Duplicati.Library.AutoUpdater.UpdaterManager.HasUpdateInstalled; } }
|
||||
|
||||
public double UpdateDownloadProgress { get { return Program.UpdatePoller.DownloadProgess; } }
|
||||
|
||||
|
||||
public Tuple<long, string> ActiveTask
|
||||
{
|
||||
get
|
||||
{
|
||||
var t = Program.WorkThread.CurrentTask;
|
||||
if (t == null)
|
||||
return null;
|
||||
else
|
||||
return new Tuple<long, string>(t.TaskID, t.Backup == null ? null : t.Backup.ID);
|
||||
}
|
||||
}
|
||||
|
||||
public IList<Tuple<long, string>> SchedulerQueueIds
|
||||
{
|
||||
get { return (from n in Program.Scheduler.WorkerQueue where n.Backup != null select new Tuple<long, string>(n.TaskID, n.Backup.ID)).ToList(); }
|
||||
}
|
||||
|
||||
public IList<Tuple<string, DateTime>> ProposedSchedule
|
||||
{
|
||||
get
|
||||
{
|
||||
return (
|
||||
from n in Program.Scheduler.Schedule
|
||||
let backupid = (from t in n.Value.Tags
|
||||
where t != null && t.StartsWith("ID=", StringComparison.Ordinal)
|
||||
select t.Substring("ID=".Length)).FirstOrDefault()
|
||||
where !string.IsNullOrWhiteSpace(backupid)
|
||||
select new Tuple<string, DateTime>(backupid, n.Key)
|
||||
).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasWarning { get { return Program.DataConnection.ApplicationSettings.UnackedWarning; } }
|
||||
public bool HasError { get { return Program.DataConnection.ApplicationSettings.UnackedError; } }
|
||||
|
||||
public SuggestedStatusIcon SuggestedStatusIcon
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.ActiveTask == null)
|
||||
{
|
||||
if (this.ProgramState == LiveControlState.Paused)
|
||||
return SuggestedStatusIcon.Paused;
|
||||
|
||||
if (this.HasError)
|
||||
return SuggestedStatusIcon.ReadyError;
|
||||
if (this.HasWarning)
|
||||
return SuggestedStatusIcon.ReadyWarning;
|
||||
|
||||
return SuggestedStatusIcon.Ready;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.ProgramState == LiveControlState.Running)
|
||||
return SuggestedStatusIcon.Active;
|
||||
else
|
||||
return SuggestedStatusIcon.ActivePaused;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime EstimatedPauseEnd
|
||||
{
|
||||
get
|
||||
{
|
||||
return Program.LiveControl.EstimatedPauseEnd;
|
||||
}
|
||||
}
|
||||
|
||||
private long m_lastEventID = Program.StatusEventNotifyer.EventNo;
|
||||
|
||||
public long LastEventID
|
||||
{
|
||||
get { return m_lastEventID; }
|
||||
set { m_lastEventID = value; }
|
||||
}
|
||||
|
||||
public long LastDataUpdateID { get { return Program.LastDataUpdateID; } }
|
||||
|
||||
public long LastNotificationUpdateID { get { return Program.LastNotificationUpdateID; } }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 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 Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.Serializable
|
||||
{
|
||||
/// <summary>
|
||||
/// This class collects all reportable status properties into a single class that can be exported as JSON
|
||||
/// </summary>
|
||||
public class ServerStatus : Duplicati.Server.Serialization.Interface.IServerStatus
|
||||
{
|
||||
public LiveControlState ProgramState
|
||||
{
|
||||
get { return EnumConverter.Convert<LiveControlState>(FIXMEGlobal.LiveControl.State); }
|
||||
}
|
||||
|
||||
public string UpdatedVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
var u = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion;
|
||||
if (u == null)
|
||||
return null;
|
||||
|
||||
Version v;
|
||||
if (!Version.TryParse(u.Version, out v))
|
||||
return null;
|
||||
|
||||
if (v <= System.Reflection.Assembly.GetExecutingAssembly().GetName().Version)
|
||||
return null;
|
||||
|
||||
return u.Displayname;
|
||||
}
|
||||
}
|
||||
|
||||
public string UpdateDownloadLink => FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion?.GetUpdateUrls()?.FirstOrDefault();
|
||||
|
||||
public UpdatePollerStates UpdaterState { get { return FIXMEGlobal.UpdatePoller.ThreadState; } }
|
||||
|
||||
public double UpdateDownloadProgress { get { return FIXMEGlobal.UpdatePoller.DownloadProgess; } }
|
||||
|
||||
|
||||
public Tuple<long, string> ActiveTask
|
||||
{
|
||||
get
|
||||
{
|
||||
var t = FIXMEGlobal.WorkThread.CurrentTask;
|
||||
if (t == null)
|
||||
return null;
|
||||
else
|
||||
return new Tuple<long, string>(t.TaskID, t.Backup == null ? null : t.Backup.ID);
|
||||
}
|
||||
}
|
||||
|
||||
public IList<Tuple<long, string>> SchedulerQueueIds
|
||||
{
|
||||
get { return (from n in FIXMEGlobal.Scheduler.WorkerQueue where n.Backup != null select new Tuple<long, string>(n.TaskID, n.Backup.ID)).ToList(); }
|
||||
}
|
||||
|
||||
public IList<Tuple<string, DateTime>> ProposedSchedule
|
||||
{
|
||||
get
|
||||
{
|
||||
return (
|
||||
from n in FIXMEGlobal.Scheduler.Schedule
|
||||
let backupid = (from t in n.Value.Tags
|
||||
where t != null && t.StartsWith("ID=", StringComparison.Ordinal)
|
||||
select t.Substring("ID=".Length)).FirstOrDefault()
|
||||
where !string.IsNullOrWhiteSpace(backupid)
|
||||
select new Tuple<string, DateTime>(backupid, n.Key)
|
||||
).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasWarning { get { return FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning; } }
|
||||
public bool HasError { get { return FIXMEGlobal.DataConnection.ApplicationSettings.UnackedError; } }
|
||||
|
||||
public SuggestedStatusIcon SuggestedStatusIcon
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.ActiveTask == null)
|
||||
{
|
||||
if (this.ProgramState == LiveControlState.Paused)
|
||||
return SuggestedStatusIcon.Paused;
|
||||
|
||||
if (this.HasError)
|
||||
return SuggestedStatusIcon.ReadyError;
|
||||
if (this.HasWarning)
|
||||
return SuggestedStatusIcon.ReadyWarning;
|
||||
|
||||
return SuggestedStatusIcon.Ready;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.ProgramState == LiveControlState.Running)
|
||||
return SuggestedStatusIcon.Active;
|
||||
else
|
||||
return SuggestedStatusIcon.ActivePaused;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime EstimatedPauseEnd
|
||||
{
|
||||
get
|
||||
{
|
||||
return FIXMEGlobal.LiveControl.EstimatedPauseEnd;
|
||||
}
|
||||
}
|
||||
|
||||
private long m_lastEventID = FIXMEGlobal.StatusEventNotifyer.EventNo;
|
||||
|
||||
public long LastEventID
|
||||
{
|
||||
get { return m_lastEventID; }
|
||||
set { m_lastEventID = value; }
|
||||
}
|
||||
|
||||
public long LastDataUpdateID { get { return FIXMEGlobal.PeekLastDataUpdateID(); } }
|
||||
|
||||
public long LastNotificationUpdateID { get { return FIXMEGlobal.PeekLastNotificationUpdateID(); } }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+60
-80
@@ -1,80 +1,60 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Duplicati.Server.Serializable
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of a ExtJS treenode-like class for easy JSON export
|
||||
/// </summary>
|
||||
public class TreeNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The text displayed for the node
|
||||
/// </summary>
|
||||
public string text { get; set; }
|
||||
/// <summary>
|
||||
/// The node id
|
||||
/// </summary>
|
||||
public string id { get; set; }
|
||||
/// <summary>
|
||||
/// The class applied to the node
|
||||
/// </summary>
|
||||
public string cls { get; set; }
|
||||
/// <summary>
|
||||
/// The class applied to the icon
|
||||
/// </summary>
|
||||
public string iconCls { get; set; }
|
||||
/// <summary>
|
||||
/// True if the element should be checked
|
||||
/// </summary>
|
||||
public bool check { get; set; }
|
||||
/// <summary>
|
||||
/// True if the element is a leaf node
|
||||
/// </summary>
|
||||
public bool leaf { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the current path, if the item is a symbolic path
|
||||
/// </summary>
|
||||
public string resolvedpath { get; set; }
|
||||
/// <summary>
|
||||
/// True if the element is hidden
|
||||
/// </summary>
|
||||
public bool hidden { get; set; }
|
||||
/// <summary>
|
||||
/// True if the element is a symlink
|
||||
/// </summary>
|
||||
public bool symlink { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new TreeNode
|
||||
/// </summary>
|
||||
public TreeNode()
|
||||
{
|
||||
this.cls = "folder";
|
||||
this.iconCls = "x-tree-icon-parent";
|
||||
this.check = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Duplicati.Server.Serializable
|
||||
{
|
||||
/// <summary>
|
||||
/// Implementation of a ExtJS treenode-like class for easy JSON export
|
||||
/// </summary>
|
||||
public class TreeNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The text displayed for the node
|
||||
/// </summary>
|
||||
public string text { get; set; }
|
||||
/// <summary>
|
||||
/// The node id
|
||||
/// </summary>
|
||||
public string id { get; set; }
|
||||
/// <summary>
|
||||
/// The class applied to the node
|
||||
/// </summary>
|
||||
public string cls { get; set; }
|
||||
/// <summary>
|
||||
/// The class applied to the icon
|
||||
/// </summary>
|
||||
public string iconCls { get; set; }
|
||||
/// <summary>
|
||||
/// True if the element should be checked
|
||||
/// </summary>
|
||||
public bool check { get; set; }
|
||||
/// <summary>
|
||||
/// True if the element is a leaf node
|
||||
/// </summary>
|
||||
public bool leaf { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the current path, if the item is a symbolic path
|
||||
/// </summary>
|
||||
public string resolvedpath { get; set; }
|
||||
/// <summary>
|
||||
/// True if the element is hidden
|
||||
/// </summary>
|
||||
public bool hidden { get; set; }
|
||||
/// <summary>
|
||||
/// True if the element is a symlink
|
||||
/// </summary>
|
||||
public bool symlink { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new TreeNode
|
||||
/// </summary>
|
||||
public TreeNode()
|
||||
{
|
||||
this.cls = "folder";
|
||||
this.iconCls = "x-tree-icon-parent";
|
||||
this.check = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 System.Collections.Generic;
|
||||
@@ -1,77 +1,57 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.Localization.Short;
|
||||
using System.Linq;
|
||||
|
||||
namespace Duplicati.Server.Strings {
|
||||
internal static class Program {
|
||||
public static string AnotherInstanceDetected { get { return LC.L(@"Another instance is running, and was notified"); } }
|
||||
public static string DatabaseOpenError(string message) { return LC.L(@"Failed to create, open or upgrade the database.
|
||||
Error message: {0}", message); }
|
||||
public static string HelpCommandDescription { get { return LC.L(@"Displays this help"); } }
|
||||
public static string HelpDisplayDialog { get { return LC.L(@"Supported commandline arguments:
|
||||
|
||||
"); } }
|
||||
public static string HelpDisplayFormat(string optionname, string optiontext) { return LC.L(@"--{0}: {1}", optionname, optiontext); }
|
||||
public static string ParametersFileOptionLong2 { get { return LC.L(@"This option can be used to store some or all of the options given to the commandline client. The file must be a plain text file, UTF-8 encoding is preferred. Each line in the file should be of the format --option=value. The special options --{0} and --{1} can be used to override the localpath and the remote destination uri, respectively. The options in this file take precedence over the options provided on the commandline. You cannot specify filters in both the file and on the commandline. Instead, you can use the special --{2}, --{3}, or --{4} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {5} ", "source", "target", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } }
|
||||
public static string ParametersFileOptionShort { get { return LC.L(@"Path to a file with parameters"); } }
|
||||
public static string FiltersCannotBeUsedWithFileError2 { get { return LC.L(@"Filters cannot be specified on the commandline if filters are also present in the parameter file. Use the special --{0}, --{1}, or --{2} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {3}", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } }
|
||||
public static string FailedToParseParametersFileError(string path, string message) { return LC.L(@"Unable to read the parameters file ""{0}"", reason: {1}", path, message); }
|
||||
public static string SkippingSourceArgumentsOnNonBackupOperation { get { return @"The --source argument was specified in the parameter file, but the current operation is not a backup operation, so the argument is ignored"; } }
|
||||
public static string LogfileCommandDescription { get { return LC.L(@"Outputs log information to the file given"); } }
|
||||
public static string LoglevelCommandDescription { get { return LC.L(@"Determines the amount of information written in the log file"); } }
|
||||
public static string PortablemodeCommandDescription { get { return LC.L(@"Activates portable mode where the database is placed below the program executable"); } }
|
||||
public static string SeriousError(string message) { return LC.L(@"A serious error occurred in Duplicati: {0}", message); }
|
||||
public static string StartupFailure(System.Exception error) { return LC.L(@"Unable to start up, perhaps another process is already running?
|
||||
Error message: {0}", error); }
|
||||
public static string UnencrypteddatabaseCommandDescription { get { return LC.L(@"Disables database encryption"); } }
|
||||
public static string WrongSQLiteVersion(System.Version actualversion, string expectedversion) { return LC.L(@"Unsupported version of SQLite detected ({0}), must be {1} or higher", actualversion, expectedversion); }
|
||||
public static string WebserverWebrootDescription { get { return LC.L(@"The path to the folder where the static files for the webserver is present. The folder must be located beneath the installation folder"); } }
|
||||
public static string WebserverPortDescription { get { return LC.L(@"The port the webserver listens on. Multiple values may be supplied with a comma in between."); } }
|
||||
public static string WebserverCertificateFileDescription { get { return LC.L(@"The certificate and key file in PKCS #12 format the webserver use for SSL. Only RSA/DSA keys are supported."); } }
|
||||
public static string WebserverCertificatePasswordDescription { get { return LC.L(@"The password for decryption of certificate PKCS #12 file."); } }
|
||||
public static string WebserverInterfaceDescription { get { return LC.L(@"The interface the webserver listens on. The special values ""*"" and ""any"" means any interface. The special value ""loopback"" means the loopback adapter."); } }
|
||||
public static string WebserverPasswordDescription { get { return LC.L(@"The password required to access the webserver. This option is saved so you do not need to set it on each run. Setting an empty value disables the password."); } }
|
||||
public static string WebserverAllowedhostnamesDescription { get { return LC.L(@"The hostnames that are accepted, separated with semicolons. If any of the hostnames are ""*"", all hostnames are allowed and the hostname checking is disabled."); } }
|
||||
public static string PingpongkeepaliveShort { get { return LC.L(@"Enables the ping-pong responder"); } }
|
||||
public static string PingpongkeepaliveLong { get { return LC.L(@"When running as a server, the service daemon must verify that the process is responding. If this option is enabled, the server reads stdin and writes a reply to each line read"); } }
|
||||
public static string LogretentionShort { get { return LC.L(@"Clean up old log data"); } }
|
||||
public static string LogretentionLong { get { return LC.L(@"Set the time after which log data will be purged from the database."); } }
|
||||
public static string ServerdatafolderShort { get { return LC.L(@"Sets the folder where settings are stored"); } }
|
||||
public static string ServerdatafolderLong(string envname) { return LC.L(@"Duplicati needs to store a small database with all settings. Use this option to choose where the settings are stored. This option can also be set with the environment variable {0}.", envname); }
|
||||
public static string ServerencryptionkeyShort { get { return LC.L(@"Sets the database encryption key"); } }
|
||||
public static string ServerencryptionkeyLong(string envname, string decryptionoption) { return LC.L(@"This option sets the encryption key used to scramble the local settings database. This option can also be set with the environment variable {0}. Use the option --{1} to disable the database scrambling.", envname, decryptionoption); }
|
||||
public static string TempdirShort { get { return LC.L(@"Temporary storage folder"); } }
|
||||
public static string TempdirLong { get { return LC.L(@"This option can be used to supply an alternative folder for temporary storage. By default the system default temporary folder is used. Note that also SQLite will put temporary files in this temporary folder."); } }
|
||||
}
|
||||
internal static class Scheduler {
|
||||
public static string InvalidTimeSetupError(System.DateTime startdate, string interval, string alloweddays) { return LC.L(@"Unable to find a valid date, given the start date {0}, the repetition interval {1} and the allowed days {2}", startdate, interval, alloweddays); }
|
||||
}
|
||||
internal static class Server
|
||||
{
|
||||
public static string DefectSSLCertInDatabase { get { return @"Unable to create SSL certificate using data from database. Starting without SSL."; } }
|
||||
public static string StartedServer(string ip, int port) { return LC.L(@"Server has started and is listening on {0}, port {1}", ip, port); }
|
||||
public static string SSLCertificateFailure(string errormessage) { return LC.L(@"Unable to create SSL certificate using provided parameters. Exception detail: {0}", errormessage); }
|
||||
public static string ServerStartFailure(IEnumerable<int> portstried) { return LC.L(@"Unable to open a socket for listening, tried ports: {0}", string.Join(",", from n in (portstried ?? new int[0]) select n.ToString())); }
|
||||
}
|
||||
|
||||
}
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.Localization.Short;
|
||||
using System.Linq;
|
||||
|
||||
namespace Duplicati.Server.Strings {
|
||||
public static class Program {
|
||||
public static string AnotherInstanceDetected { get { return LC.L(@"Another instance is running, and was notified"); } }
|
||||
public static string DatabaseOpenError(string message) { return LC.L(@"Failed to create, open or upgrade the database.
|
||||
Error message: {0}", message); }
|
||||
public static string HelpCommandDescription { get { return LC.L(@"Displays this help"); } }
|
||||
public static string HelpDisplayDialog { get { return LC.L(@"Supported commandline arguments:
|
||||
|
||||
"); } }
|
||||
public static string HelpDisplayFormat(string optionname, string optiontext) { return LC.L(@"--{0}: {1}", optionname, optiontext); }
|
||||
public static string ParametersFileOptionLong2 { get { return LC.L(@"This option can be used to store some or all of the options given to the commandline client. The file must be a plain text file, UTF-8 encoding is preferred. Each line in the file should be of the format --option=value. The special options --{0} and --{1} can be used to override the localpath and the remote destination uri, respectively. The options in this file take precedence over the options provided on the commandline. You cannot specify filters in both the file and on the commandline. Instead, you can use the special --{2}, --{3}, or --{4} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {5} ", "source", "target", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } }
|
||||
public static string ParametersFileOptionShort { get { return LC.L(@"Path to a file with parameters"); } }
|
||||
public static string FiltersCannotBeUsedWithFileError2 { get { return LC.L(@"Filters cannot be specified on the commandline if filters are also present in the parameter file. Use the special --{0}, --{1}, or --{2} options to specify filters inside the parameter file. Each filter must be prefixed with either a + or a -, and multiple filters must be joined with {3}", "replace-filter", "append-filter", "prepend-filter", System.IO.Path.PathSeparator); } }
|
||||
public static string FailedToParseParametersFileError(string path, string message) { return LC.L(@"Unable to read the parameters file ""{0}"", reason: {1}", path, message); }
|
||||
public static string SkippingSourceArgumentsOnNonBackupOperation { get { return @"The --source argument was specified in the parameter file, but the current operation is not a backup operation, so the argument is ignored"; } }
|
||||
public static string LogfileCommandDescription { get { return LC.L(@"Outputs log information to the file given"); } }
|
||||
public static string LoglevelCommandDescription { get { return LC.L(@"Determines the amount of information written in the log file"); } }
|
||||
public static string PortablemodeCommandDescription { get { return LC.L(@"Activates portable mode where the database is placed below the program executable"); } }
|
||||
public static string SeriousError(string message) { return LC.L(@"A serious error occurred in Duplicati: {0}", message); }
|
||||
public static string StartupFailure(System.Exception error) { return LC.L(@"Unable to start up, perhaps another process is already running?
|
||||
Error message: {0}", error); }
|
||||
public static string UnencrypteddatabaseCommandDescription { get { return LC.L(@"Disables database encryption"); } }
|
||||
public static string WrongSQLiteVersion(System.Version actualversion, string expectedversion) { return LC.L(@"Unsupported version of SQLite detected ({0}), must be {1} or higher", actualversion, expectedversion); }
|
||||
public static string WebserverWebrootDescription { get { return LC.L(@"The path to the folder where the static files for the webserver is present. The folder must be located beneath the installation folder"); } }
|
||||
public static string WebserverPortDescription { get { return LC.L(@"The port the webserver listens on. Multiple values may be supplied with a comma in between."); } }
|
||||
public static string WebserverCertificateFileDescription { get { return LC.L(@"The certificate and key file in PKCS #12 format the webserver use for SSL. Only RSA/DSA keys are supported."); } }
|
||||
public static string WebserverCertificatePasswordDescription { get { return LC.L(@"The password for decryption of certificate PKCS #12 file."); } }
|
||||
public static string WebserverInterfaceDescription { get { return LC.L(@"The interface the webserver listens on. The special values ""*"" and ""any"" means any interface. The special value ""loopback"" means the loopback adapter."); } }
|
||||
public static string WebserverPasswordDescription { get { return LC.L(@"The password required to access the webserver. This option is saved so you do not need to set it on each run. Setting an empty value disables the password."); } }
|
||||
public static string WebserverAllowedhostnamesDescription { get { return LC.L(@"The hostnames that are accepted, separated with semicolons. If any of the hostnames are ""*"", all hostnames are allowed and the hostname checking is disabled."); } }
|
||||
public static string PingpongkeepaliveShort { get { return LC.L(@"Enables the ping-pong responder"); } }
|
||||
public static string PingpongkeepaliveLong { get { return LC.L(@"When running as a server, the service daemon must verify that the process is responding. If this option is enabled, the server reads stdin and writes a reply to each line read"); } }
|
||||
public static string LogretentionShort { get { return LC.L(@"Clean up old log data"); } }
|
||||
public static string LogretentionLong { get { return LC.L(@"Set the time after which log data will be purged from the database."); } }
|
||||
public static string ServerdatafolderShort { get { return LC.L(@"Sets the folder where settings are stored"); } }
|
||||
public static string ServerdatafolderLong(string envname) { return LC.L(@"Duplicati needs to store a small database with all settings. Use this option to choose where the settings are stored. This option can also be set with the environment variable {0}.", envname); }
|
||||
public static string ServerencryptionkeyShort { get { return LC.L(@"Sets the database encryption key"); } }
|
||||
public static string ServerencryptionkeyLong(string envname, string decryptionoption) { return LC.L(@"This option sets the encryption key used to scramble the local settings database. This option can also be set with the environment variable {0}. Use the option --{1} to disable the database scrambling.", envname, decryptionoption); }
|
||||
public static string TempdirShort { get { return LC.L(@"Temporary storage folder"); } }
|
||||
public static string TempdirLong { get { return LC.L(@"This option can be used to supply an alternative folder for temporary storage. By default the system default temporary folder is used. Note that also SQLite will put temporary files in this temporary folder."); } }
|
||||
}
|
||||
internal static class Scheduler {
|
||||
public static string InvalidTimeSetupError(System.DateTime startdate, string interval, string alloweddays) { return LC.L(@"Unable to find a valid date, given the start date {0}, the repetition interval {1} and the allowed days {2}", startdate, interval, alloweddays); }
|
||||
}
|
||||
internal static class Server
|
||||
{
|
||||
public static string DefectSSLCertInDatabase { get { return @"Unable to create SSL certificate using data from database. Starting without SSL."; } }
|
||||
public static string StartedServer(string ip, int port) { return LC.L(@"Server has started and is listening on {0}, port {1}", ip, port); }
|
||||
public static string SSLCertificateFailure(string errormessage) { return LC.L(@"Unable to create SSL certificate using provided parameters. Exception detail: {0}", errormessage); }
|
||||
public static string ServerStartFailure(IEnumerable<int> portstried) { return LC.L(@"Unable to open a socket for listening, tried ports: {0}", string.Join(",", from n in (portstried ?? new int[0]) select n.ToString())); }
|
||||
}
|
||||
|
||||
}
|
||||
+49
-84
@@ -1,27 +1,24 @@
|
||||
// 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 System.Threading;
|
||||
using Duplicati.Library.RestAPI;
|
||||
using Duplicati.Server.Serialization;
|
||||
|
||||
namespace Duplicati.Server
|
||||
@@ -33,7 +30,6 @@ namespace Duplicati.Server
|
||||
{
|
||||
private readonly Thread m_thread;
|
||||
private volatile bool m_terminated = false;
|
||||
private volatile bool m_download = false;
|
||||
private volatile bool m_forceCheck = false;
|
||||
private readonly object m_lock = new object();
|
||||
private readonly AutoResetEvent m_waitSignal;
|
||||
@@ -44,17 +40,17 @@ namespace Duplicati.Server
|
||||
public UpdatePollerStates ThreadState { get; private set; }
|
||||
public double DownloadProgess
|
||||
{
|
||||
get { return m_downloadProgress ; }
|
||||
get { return m_downloadProgress; }
|
||||
|
||||
private set
|
||||
{
|
||||
var oldv = m_downloadProgress;
|
||||
m_downloadProgress = value;
|
||||
if ((int)(oldv * 100) != (int)(value * 100))
|
||||
Program.StatusEventNotifyer.SignalNewEvent();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public UpdatePollThread()
|
||||
{
|
||||
m_waitSignal = new AutoResetEvent(false);
|
||||
@@ -67,35 +63,16 @@ namespace Duplicati.Server
|
||||
|
||||
public void CheckNow()
|
||||
{
|
||||
lock(m_lock)
|
||||
lock (m_lock)
|
||||
{
|
||||
m_forceCheck = true;
|
||||
m_waitSignal.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void InstallUpdate()
|
||||
{
|
||||
lock(m_lock)
|
||||
{
|
||||
m_forceCheck = true;
|
||||
m_download = true;
|
||||
m_waitSignal.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void ActivateUpdate()
|
||||
{
|
||||
if (Duplicati.Library.AutoUpdater.UpdaterManager.SetRunUpdate())
|
||||
{
|
||||
IsUpdateRequested = true;
|
||||
Program.ApplicationExitEvent.Set();
|
||||
}
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
lock(m_lock)
|
||||
lock (m_lock)
|
||||
{
|
||||
m_terminated = true;
|
||||
m_waitSignal.Set();
|
||||
@@ -114,12 +91,12 @@ namespace Duplicati.Server
|
||||
|
||||
while (!m_terminated)
|
||||
{
|
||||
var nextCheck = Program.DataConnection.ApplicationSettings.NextUpdateCheck;
|
||||
var nextCheck = FIXMEGlobal.DataConnection.ApplicationSettings.NextUpdateCheck;
|
||||
|
||||
var maxcheck = TimeSpan.FromDays(7);
|
||||
try
|
||||
{
|
||||
maxcheck = Library.Utility.Timeparser.ParseTimeSpan(Program.DataConnection.ApplicationSettings.UpdateCheckInterval);
|
||||
maxcheck = Library.Utility.Timeparser.ParseTimeSpan(FIXMEGlobal.DataConnection.ApplicationSettings.UpdateCheckInterval);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -131,28 +108,28 @@ namespace Duplicati.Server
|
||||
|
||||
if (nextCheck < DateTime.UtcNow || m_forceCheck)
|
||||
{
|
||||
lock(m_lock)
|
||||
lock (m_lock)
|
||||
m_forceCheck = false;
|
||||
|
||||
ThreadState = UpdatePollerStates.Checking;
|
||||
Program.StatusEventNotifyer.SignalNewEvent();
|
||||
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
|
||||
DateTime started = DateTime.UtcNow;
|
||||
Program.DataConnection.ApplicationSettings.LastUpdateCheck = started;
|
||||
nextCheck = Program.DataConnection.ApplicationSettings.NextUpdateCheck;
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.LastUpdateCheck = started;
|
||||
nextCheck = FIXMEGlobal.DataConnection.ApplicationSettings.NextUpdateCheck;
|
||||
|
||||
Library.AutoUpdater.ReleaseType rt;
|
||||
if (!Enum.TryParse<Library.AutoUpdater.ReleaseType>(Program.DataConnection.ApplicationSettings.UpdateChannel, true, out rt))
|
||||
if (!Enum.TryParse<Library.AutoUpdater.ReleaseType>(FIXMEGlobal.DataConnection.ApplicationSettings.UpdateChannel, true, out rt))
|
||||
rt = Duplicati.Library.AutoUpdater.ReleaseType.Unknown;
|
||||
|
||||
// Choose the default channel in case we have unknown
|
||||
rt = rt == Duplicati.Library.AutoUpdater.ReleaseType.Unknown ? Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel : rt;
|
||||
|
||||
try
|
||||
{
|
||||
{
|
||||
var update = Duplicati.Library.AutoUpdater.UpdaterManager.CheckForUpdate(rt);
|
||||
if (update != null)
|
||||
Program.DataConnection.ApplicationSettings.UpdatedVersion = update;
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion = update;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -161,64 +138,52 @@ namespace Duplicati.Server
|
||||
// It could be that we have registered an update from a more unstable channel,
|
||||
// but the user has switched to a more stable channel.
|
||||
// In that case we discard the old update to avoid offering it.
|
||||
if (Program.DataConnection.ApplicationSettings.UpdatedVersion != null)
|
||||
if (FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion != null)
|
||||
{
|
||||
Library.AutoUpdater.ReleaseType updatert;
|
||||
var updatertstring = Program.DataConnection.ApplicationSettings.UpdatedVersion.ReleaseType;
|
||||
var updatertstring = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion.ReleaseType;
|
||||
if (string.Equals(updatertstring, "preview", StringComparison.OrdinalIgnoreCase))
|
||||
updatertstring = Library.AutoUpdater.ReleaseType.Experimental.ToString();
|
||||
|
||||
|
||||
if (!Enum.TryParse<Library.AutoUpdater.ReleaseType>(updatertstring, true, out updatert))
|
||||
updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly;
|
||||
|
||||
if (updatert == Duplicati.Library.AutoUpdater.ReleaseType.Unknown)
|
||||
updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly;
|
||||
|
||||
|
||||
if (updatert > rt)
|
||||
Program.DataConnection.ApplicationSettings.UpdatedVersion = null;
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion = null;
|
||||
}
|
||||
|
||||
if (Program.DataConnection.ApplicationSettings.UpdatedVersion != null && Duplicati.Library.AutoUpdater.UpdaterManager.TryParseVersion(Program.DataConnection.ApplicationSettings.UpdatedVersion.Version) > System.Reflection.Assembly.GetExecutingAssembly().GetName().Version)
|
||||
var updatedinfo = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion;
|
||||
if (updatedinfo != null && Duplicati.Library.AutoUpdater.UpdaterManager.TryParseVersion(updatedinfo.Version) > System.Reflection.Assembly.GetExecutingAssembly().GetName().Version)
|
||||
{
|
||||
Program.DataConnection.RegisterNotification(
|
||||
var package = updatedinfo.FindPackage();
|
||||
|
||||
FIXMEGlobal.DataConnection.RegisterNotification(
|
||||
NotificationType.Information,
|
||||
"Found update",
|
||||
Program.DataConnection.ApplicationSettings.UpdatedVersion.Displayname,
|
||||
updatedinfo.Displayname,
|
||||
null,
|
||||
null,
|
||||
"update:new",
|
||||
null,
|
||||
"NewUpdateFound",
|
||||
null,
|
||||
(self, all) => {
|
||||
(self, all) =>
|
||||
{
|
||||
return all.FirstOrDefault(x => x.Action == "update:new") ?? self;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_download)
|
||||
{
|
||||
lock(m_lock)
|
||||
m_download = false;
|
||||
|
||||
var v = Program.DataConnection.ApplicationSettings.UpdatedVersion;
|
||||
if (v != null)
|
||||
{
|
||||
ThreadState = UpdatePollerStates.Downloading;
|
||||
Program.StatusEventNotifyer.SignalNewEvent();
|
||||
|
||||
if (Duplicati.Library.AutoUpdater.UpdaterManager.DownloadAndUnpackUpdate(v, (pg) => { DownloadProgess = pg; }))
|
||||
Program.StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
}
|
||||
|
||||
DownloadProgess = 0;
|
||||
|
||||
if (ThreadState != UpdatePollerStates.Waiting)
|
||||
{
|
||||
ThreadState = UpdatePollerStates.Waiting;
|
||||
Program.StatusEventNotifyer.SignalNewEvent();
|
||||
FIXMEGlobal.StatusEventNotifyer.SignalNewEvent();
|
||||
}
|
||||
|
||||
var waitTime = nextCheck - DateTime.UtcNow;
|
||||
@@ -226,14 +191,14 @@ namespace Duplicati.Server
|
||||
// Guard against spin-loop
|
||||
if (waitTime.TotalSeconds < 5)
|
||||
waitTime = TimeSpan.FromSeconds(5);
|
||||
|
||||
|
||||
// Guard against year-long waits
|
||||
// A re-check does not cause an update check
|
||||
if (waitTime.TotalDays > 1)
|
||||
waitTime = TimeSpan.FromDays(1);
|
||||
|
||||
|
||||
m_waitSignal.WaitOne(waitTime, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+342
-342
@@ -1,342 +1,342 @@
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using HttpServer;
|
||||
using HttpServer.HttpModules;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
internal class AuthenticationHandler : HttpModule
|
||||
{
|
||||
private const string AUTH_COOKIE_NAME = "session-auth";
|
||||
private const string NONCE_COOKIE_NAME = "session-nonce";
|
||||
|
||||
private const string XSRF_COOKIE_NAME = "xsrf-token";
|
||||
private const string XSRF_HEADER_NAME = "X-XSRF-Token";
|
||||
|
||||
private const string TRAYICONPASSWORDSOURCE_HEADER = "X-TrayIcon-PasswordSource";
|
||||
|
||||
public const string LOGIN_SCRIPT_URI = "/login.cgi";
|
||||
public const string LOGOUT_SCRIPT_URI = "/logout.cgi";
|
||||
public const string CAPTCHA_IMAGE_URI = RESTHandler.API_URI_PATH + "/captcha/";
|
||||
|
||||
private const int XSRF_TIMEOUT_MINUTES = 10;
|
||||
private const int AUTH_TIMEOUT_MINUTES = 10;
|
||||
|
||||
private readonly ConcurrentDictionary<string, DateTime> m_activeTokens = new ConcurrentDictionary<string, DateTime>();
|
||||
private readonly ConcurrentDictionary<string, Tuple<DateTime, string>> m_activeNonces = new ConcurrentDictionary<string, Tuple<DateTime, string>>();
|
||||
private readonly ConcurrentDictionary<string, DateTime> m_activexsrf = new ConcurrentDictionary<string, DateTime>();
|
||||
|
||||
readonly System.Security.Cryptography.RandomNumberGenerator m_prng = System.Security.Cryptography.RNGCryptoServiceProvider.Create();
|
||||
|
||||
private string FindXSRFToken(HttpServer.IHttpRequest request)
|
||||
{
|
||||
string xsrftoken = request.Headers[XSRF_HEADER_NAME] ?? "";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(xsrftoken))
|
||||
{
|
||||
var xsrfq = request.Form[XSRF_HEADER_NAME] ?? request.Form[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)];
|
||||
xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(xsrftoken))
|
||||
{
|
||||
var xsrfq = request.QueryString[XSRF_HEADER_NAME] ?? request.QueryString[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)];
|
||||
xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value;
|
||||
}
|
||||
|
||||
return xsrftoken;
|
||||
}
|
||||
|
||||
private bool AddXSRFTokenToRespone(HttpServer.IHttpResponse response)
|
||||
{
|
||||
if (m_activexsrf.Count > 500)
|
||||
return false;
|
||||
|
||||
var buf = new byte[32];
|
||||
var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
m_prng.GetBytes(buf);
|
||||
var token = Convert.ToBase64String(buf);
|
||||
|
||||
m_activexsrf.AddOrUpdate(token, key => expires, (key, existingExpires) =>
|
||||
{
|
||||
// Simulate the original behavior => if the random token, against all odds, is already used
|
||||
// we throw an ArgumentException
|
||||
throw new ArgumentException("An element with the same key already exists in the dictionary.");
|
||||
});
|
||||
|
||||
response.Cookies.Add(new HttpServer.ResponseCookie(XSRF_COOKIE_NAME, token, expires));
|
||||
return true;
|
||||
}
|
||||
|
||||
private string FindAuthCookie(HttpServer.IHttpRequest request)
|
||||
{
|
||||
var authcookie = request.Cookies[AUTH_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(AUTH_COOKIE_NAME)];
|
||||
var authform = request.Form["auth-token"] ?? request.Form[Library.Utility.Uri.UrlEncode("auth-token")];
|
||||
var authquery = request.QueryString["auth-token"] ?? request.QueryString[Library.Utility.Uri.UrlEncode("auth-token")];
|
||||
|
||||
var auth_token = string.IsNullOrWhiteSpace(authcookie?.Value) ? null : authcookie.Value;
|
||||
if (!string.IsNullOrWhiteSpace(authquery?.Value))
|
||||
auth_token = authquery.Value;
|
||||
if (!string.IsNullOrWhiteSpace(authform?.Value))
|
||||
auth_token = authform.Value;
|
||||
|
||||
return auth_token;
|
||||
}
|
||||
|
||||
private bool HasXSRFCookie(HttpServer.IHttpRequest request)
|
||||
{
|
||||
// Clean up expired XSRF cookies
|
||||
foreach (var k in (from n in m_activexsrf where DateTime.UtcNow > n.Value select n.Key))
|
||||
m_activexsrf.TryRemove(k, out _);
|
||||
|
||||
var xsrfcookie = request.Cookies[XSRF_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(XSRF_COOKIE_NAME)];
|
||||
var value = xsrfcookie == null ? null : xsrfcookie.Value;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return false;
|
||||
|
||||
if (m_activexsrf.ContainsKey(value))
|
||||
{
|
||||
m_activexsrf[value] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
return true;
|
||||
}
|
||||
else if (m_activexsrf.ContainsKey(Library.Utility.Uri.UrlDecode(value)))
|
||||
{
|
||||
m_activexsrf[Library.Utility.Uri.UrlDecode(value)] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
HttpServer.HttpInput input = String.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? request.Form : request.QueryString;
|
||||
|
||||
var auth_token = FindAuthCookie(request);
|
||||
var xsrf_token = FindXSRFToken(request);
|
||||
|
||||
if (!HasXSRFCookie(request))
|
||||
{
|
||||
var cookieAdded = AddXSRFTokenToRespone(response);
|
||||
|
||||
if (!cookieAdded)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.ServiceUnavailable;
|
||||
response.Reason = "Too Many Concurrent Request, try again later";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (LOGOUT_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(auth_token))
|
||||
{
|
||||
// Remove the active auth token
|
||||
m_activeTokens.TryRemove(auth_token, out _);
|
||||
}
|
||||
|
||||
response.Status = System.Net.HttpStatusCode.NoContent;
|
||||
response.Reason = "OK";
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (LOGIN_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Remove expired nonces
|
||||
foreach(var k in (from n in m_activeNonces where DateTime.UtcNow > n.Value.Item1 select n.Key))
|
||||
m_activeNonces.TryRemove(k, out _);
|
||||
|
||||
if (input["get-nonce"] != null && !string.IsNullOrWhiteSpace(input["get-nonce"].Value))
|
||||
{
|
||||
if (m_activeNonces.Count > 50)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.ServiceUnavailable;
|
||||
response.Reason = "Too many active login attempts";
|
||||
return true;
|
||||
}
|
||||
|
||||
var password = Program.DataConnection.ApplicationSettings.WebserverPassword;
|
||||
|
||||
if (request.Headers[TRAYICONPASSWORDSOURCE_HEADER] == "database")
|
||||
password = Program.DataConnection.ApplicationSettings.WebserverPasswordTrayIconHash;
|
||||
|
||||
var buf = new byte[32];
|
||||
var expires = DateTime.UtcNow.AddMinutes(AUTH_TIMEOUT_MINUTES);
|
||||
m_prng.GetBytes(buf);
|
||||
var nonce = Convert.ToBase64String(buf);
|
||||
|
||||
var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||
sha256.TransformBlock(buf, 0, buf.Length, buf, 0);
|
||||
buf = Convert.FromBase64String(password);
|
||||
sha256.TransformFinalBlock(buf, 0, buf.Length);
|
||||
var pwd = Convert.ToBase64String(sha256.Hash);
|
||||
|
||||
m_activeNonces.AddOrUpdate(nonce, key => new Tuple<DateTime, string>(expires, pwd), (key, existingValue) =>
|
||||
{
|
||||
// Simulate the original behavior => if the nonce, against all odds, is already used
|
||||
// we throw an ArgumentException
|
||||
throw new ArgumentException("An element with the same key already exists in the dictionary.");
|
||||
});
|
||||
|
||||
response.Cookies.Add(new HttpServer.ResponseCookie(NONCE_COOKIE_NAME, nonce, expires));
|
||||
using(var bw = new BodyWriter(response, request))
|
||||
{
|
||||
bw.OutputOK(new {
|
||||
Status = "OK",
|
||||
Nonce = nonce,
|
||||
Salt = Program.DataConnection.ApplicationSettings.WebserverPasswordSalt
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (input["password"] != null && !string.IsNullOrWhiteSpace(input["password"].Value))
|
||||
{
|
||||
var nonce_el = request.Cookies[NONCE_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(NONCE_COOKIE_NAME)];
|
||||
var nonce = nonce_el == null || string.IsNullOrWhiteSpace(nonce_el.Value) ? "" : nonce_el.Value;
|
||||
var urldecoded = nonce == null ? "" : Duplicati.Library.Utility.Uri.UrlDecode(nonce);
|
||||
if (m_activeNonces.ContainsKey(urldecoded))
|
||||
nonce = urldecoded;
|
||||
|
||||
if (!m_activeNonces.ContainsKey(nonce))
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.Unauthorized;
|
||||
response.Reason = "Unauthorized";
|
||||
response.ContentType = "application/json";
|
||||
return true;
|
||||
}
|
||||
|
||||
var pwd = m_activeNonces[nonce].Item2;
|
||||
|
||||
// Remove the nonce
|
||||
m_activeNonces.TryRemove(nonce, out _);
|
||||
|
||||
if (pwd != input["password"].Value)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.Unauthorized;
|
||||
response.Reason = "Unauthorized";
|
||||
response.ContentType = "application/json";
|
||||
return true;
|
||||
}
|
||||
|
||||
var buf = new byte[32];
|
||||
var expires = DateTime.UtcNow.AddHours(1);
|
||||
m_prng.GetBytes(buf);
|
||||
var token = Duplicati.Library.Utility.Utility.Base64UrlEncode(buf);
|
||||
while (token.Length > 0 && token.EndsWith("=", StringComparison.Ordinal))
|
||||
token = token.Substring(0, token.Length - 1);
|
||||
|
||||
m_activeTokens.AddOrUpdate(token, key => expires, (key, existingValue) =>
|
||||
{
|
||||
// Simulate the original behavior => if the token, against all odds, is already used
|
||||
// we throw an ArgumentException
|
||||
throw new ArgumentException("An element with the same key already exists in the dictionary.");
|
||||
});
|
||||
|
||||
response.Cookies.Add(new HttpServer.ResponseCookie(AUTH_COOKIE_NAME, token, expires));
|
||||
|
||||
using(var bw = new BodyWriter(response, request))
|
||||
bw.OutputOK();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var limitedAccess =
|
||||
request.Uri.AbsolutePath.StartsWith(RESTHandler.API_URI_PATH, StringComparison.OrdinalIgnoreCase)
|
||||
;
|
||||
|
||||
// Override to allow the CAPTCHA call to go through
|
||||
if (request.Uri.AbsolutePath.StartsWith(CAPTCHA_IMAGE_URI, StringComparison.OrdinalIgnoreCase) && request.Method == "GET")
|
||||
limitedAccess = false;
|
||||
|
||||
if (limitedAccess)
|
||||
{
|
||||
if (xsrf_token != null && m_activexsrf.ContainsKey(xsrf_token))
|
||||
{
|
||||
var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
m_activexsrf[xsrf_token] = expires;
|
||||
response.Cookies.Add(new ResponseCookie(XSRF_COOKIE_NAME, xsrf_token, expires));
|
||||
}
|
||||
else
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.BadRequest;
|
||||
response.Reason = "Missing XSRF Token. Please reload the page";
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Program.DataConnection.ApplicationSettings.WebserverPassword))
|
||||
return false;
|
||||
|
||||
foreach(var k in (from n in m_activeTokens where DateTime.UtcNow > n.Value select n.Key))
|
||||
m_activeTokens.TryRemove(k, out _);
|
||||
|
||||
|
||||
// If we have a valid token, proceed
|
||||
if (!string.IsNullOrWhiteSpace(auth_token))
|
||||
{
|
||||
DateTime expires;
|
||||
var found = m_activeTokens.TryGetValue(auth_token, out expires);
|
||||
if (!found)
|
||||
{
|
||||
auth_token = Duplicati.Library.Utility.Uri.UrlDecode(auth_token);
|
||||
found = m_activeTokens.TryGetValue(auth_token, out expires);
|
||||
}
|
||||
|
||||
if (found && DateTime.UtcNow < expires)
|
||||
{
|
||||
expires = DateTime.UtcNow.AddHours(1);
|
||||
|
||||
m_activeTokens[auth_token] = expires;
|
||||
response.Cookies.Add(new ResponseCookie(AUTH_COOKIE_NAME, auth_token, expires));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ("/".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase) || "/index.html".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
response.Redirect("/login.html");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (limitedAccess)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.Unauthorized;
|
||||
response.Reason = "Not logged in";
|
||||
response.AddHeader("Location", "login.html");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using HttpServer;
|
||||
using HttpServer.HttpModules;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
public class AuthenticationHandler : HttpModule
|
||||
{
|
||||
private const string AUTH_COOKIE_NAME = "session-auth";
|
||||
private const string NONCE_COOKIE_NAME = "session-nonce";
|
||||
|
||||
private const string XSRF_COOKIE_NAME = "xsrf-token";
|
||||
private const string XSRF_HEADER_NAME = "X-XSRF-Token";
|
||||
|
||||
private const string TRAYICONPASSWORDSOURCE_HEADER = "X-TrayIcon-PasswordSource";
|
||||
|
||||
public const string LOGIN_SCRIPT_URI = "/login.cgi";
|
||||
public const string LOGOUT_SCRIPT_URI = "/logout.cgi";
|
||||
public const string CAPTCHA_IMAGE_URI = RESTHandler.API_URI_PATH + "/captcha/";
|
||||
|
||||
private const int XSRF_TIMEOUT_MINUTES = 10;
|
||||
private const int AUTH_TIMEOUT_MINUTES = 10;
|
||||
|
||||
private readonly ConcurrentDictionary<string, DateTime> m_activeTokens = new ConcurrentDictionary<string, DateTime>();
|
||||
private readonly ConcurrentDictionary<string, Tuple<DateTime, string>> m_activeNonces = new ConcurrentDictionary<string, Tuple<DateTime, string>>();
|
||||
private readonly ConcurrentDictionary<string, DateTime> m_activexsrf = new ConcurrentDictionary<string, DateTime>();
|
||||
|
||||
readonly System.Security.Cryptography.RandomNumberGenerator m_prng = System.Security.Cryptography.RNGCryptoServiceProvider.Create();
|
||||
|
||||
private string FindXSRFToken(HttpServer.IHttpRequest request)
|
||||
{
|
||||
string xsrftoken = request.Headers[XSRF_HEADER_NAME] ?? "";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(xsrftoken))
|
||||
{
|
||||
var xsrfq = request.Form[XSRF_HEADER_NAME] ?? request.Form[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)];
|
||||
xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(xsrftoken))
|
||||
{
|
||||
var xsrfq = request.QueryString[XSRF_HEADER_NAME] ?? request.QueryString[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)];
|
||||
xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value;
|
||||
}
|
||||
|
||||
return xsrftoken;
|
||||
}
|
||||
|
||||
private bool AddXSRFTokenToRespone(HttpServer.IHttpResponse response)
|
||||
{
|
||||
if (m_activexsrf.Count > 500)
|
||||
return false;
|
||||
|
||||
var buf = new byte[32];
|
||||
var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
m_prng.GetBytes(buf);
|
||||
var token = Convert.ToBase64String(buf);
|
||||
|
||||
m_activexsrf.AddOrUpdate(token, key => expires, (key, existingExpires) =>
|
||||
{
|
||||
// Simulate the original behavior => if the random token, against all odds, is already used
|
||||
// we throw an ArgumentException
|
||||
throw new ArgumentException("An element with the same key already exists in the dictionary.");
|
||||
});
|
||||
|
||||
response.Cookies.Add(new HttpServer.ResponseCookie(XSRF_COOKIE_NAME, token, expires));
|
||||
return true;
|
||||
}
|
||||
|
||||
private string FindAuthCookie(HttpServer.IHttpRequest request)
|
||||
{
|
||||
var authcookie = request.Cookies[AUTH_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(AUTH_COOKIE_NAME)];
|
||||
var authform = request.Form["auth-token"] ?? request.Form[Library.Utility.Uri.UrlEncode("auth-token")];
|
||||
var authquery = request.QueryString["auth-token"] ?? request.QueryString[Library.Utility.Uri.UrlEncode("auth-token")];
|
||||
|
||||
var auth_token = string.IsNullOrWhiteSpace(authcookie?.Value) ? null : authcookie.Value;
|
||||
if (!string.IsNullOrWhiteSpace(authquery?.Value))
|
||||
auth_token = authquery.Value;
|
||||
if (!string.IsNullOrWhiteSpace(authform?.Value))
|
||||
auth_token = authform.Value;
|
||||
|
||||
return auth_token;
|
||||
}
|
||||
|
||||
private bool HasXSRFCookie(HttpServer.IHttpRequest request)
|
||||
{
|
||||
// Clean up expired XSRF cookies
|
||||
foreach (var k in (from n in m_activexsrf where DateTime.UtcNow > n.Value select n.Key))
|
||||
m_activexsrf.TryRemove(k, out _);
|
||||
|
||||
var xsrfcookie = request.Cookies[XSRF_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(XSRF_COOKIE_NAME)];
|
||||
var value = xsrfcookie == null ? null : xsrfcookie.Value;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return false;
|
||||
|
||||
if (m_activexsrf.ContainsKey(value))
|
||||
{
|
||||
m_activexsrf[value] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
return true;
|
||||
}
|
||||
else if (m_activexsrf.ContainsKey(Library.Utility.Uri.UrlDecode(value)))
|
||||
{
|
||||
m_activexsrf[Library.Utility.Uri.UrlDecode(value)] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
HttpServer.HttpInput input = String.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? request.Form : request.QueryString;
|
||||
|
||||
var auth_token = FindAuthCookie(request);
|
||||
var xsrf_token = FindXSRFToken(request);
|
||||
|
||||
if (!HasXSRFCookie(request))
|
||||
{
|
||||
var cookieAdded = AddXSRFTokenToRespone(response);
|
||||
|
||||
if (!cookieAdded)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.ServiceUnavailable;
|
||||
response.Reason = "Too Many Concurrent Request, try again later";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (LOGOUT_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(auth_token))
|
||||
{
|
||||
// Remove the active auth token
|
||||
m_activeTokens.TryRemove(auth_token, out _);
|
||||
}
|
||||
|
||||
response.Status = System.Net.HttpStatusCode.NoContent;
|
||||
response.Reason = "OK";
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (LOGIN_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Remove expired nonces
|
||||
foreach(var k in (from n in m_activeNonces where DateTime.UtcNow > n.Value.Item1 select n.Key))
|
||||
m_activeNonces.TryRemove(k, out _);
|
||||
|
||||
if (input["get-nonce"] != null && !string.IsNullOrWhiteSpace(input["get-nonce"].Value))
|
||||
{
|
||||
if (m_activeNonces.Count > 50)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.ServiceUnavailable;
|
||||
response.Reason = "Too many active login attempts";
|
||||
return true;
|
||||
}
|
||||
|
||||
var password = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPassword;
|
||||
|
||||
if (request.Headers[TRAYICONPASSWORDSOURCE_HEADER] == "database")
|
||||
password = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPasswordTrayIconHash;
|
||||
|
||||
var buf = new byte[32];
|
||||
var expires = DateTime.UtcNow.AddMinutes(AUTH_TIMEOUT_MINUTES);
|
||||
m_prng.GetBytes(buf);
|
||||
var nonce = Convert.ToBase64String(buf);
|
||||
|
||||
var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||
sha256.TransformBlock(buf, 0, buf.Length, buf, 0);
|
||||
buf = Convert.FromBase64String(password);
|
||||
sha256.TransformFinalBlock(buf, 0, buf.Length);
|
||||
var pwd = Convert.ToBase64String(sha256.Hash);
|
||||
|
||||
m_activeNonces.AddOrUpdate(nonce, key => new Tuple<DateTime, string>(expires, pwd), (key, existingValue) =>
|
||||
{
|
||||
// Simulate the original behavior => if the nonce, against all odds, is already used
|
||||
// we throw an ArgumentException
|
||||
throw new ArgumentException("An element with the same key already exists in the dictionary.");
|
||||
});
|
||||
|
||||
response.Cookies.Add(new HttpServer.ResponseCookie(NONCE_COOKIE_NAME, nonce, expires));
|
||||
using(var bw = new BodyWriter(response, request))
|
||||
{
|
||||
bw.OutputOK(new {
|
||||
Status = "OK",
|
||||
Nonce = nonce,
|
||||
Salt = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPasswordSalt
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (input["password"] != null && !string.IsNullOrWhiteSpace(input["password"].Value))
|
||||
{
|
||||
var nonce_el = request.Cookies[NONCE_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(NONCE_COOKIE_NAME)];
|
||||
var nonce = nonce_el == null || string.IsNullOrWhiteSpace(nonce_el.Value) ? "" : nonce_el.Value;
|
||||
var urldecoded = nonce == null ? "" : Duplicati.Library.Utility.Uri.UrlDecode(nonce);
|
||||
if (m_activeNonces.ContainsKey(urldecoded))
|
||||
nonce = urldecoded;
|
||||
|
||||
if (!m_activeNonces.ContainsKey(nonce))
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.Unauthorized;
|
||||
response.Reason = "Unauthorized";
|
||||
response.ContentType = "application/json";
|
||||
return true;
|
||||
}
|
||||
|
||||
var pwd = m_activeNonces[nonce].Item2;
|
||||
|
||||
// Remove the nonce
|
||||
m_activeNonces.TryRemove(nonce, out _);
|
||||
|
||||
if (pwd != input["password"].Value)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.Unauthorized;
|
||||
response.Reason = "Unauthorized";
|
||||
response.ContentType = "application/json";
|
||||
return true;
|
||||
}
|
||||
|
||||
var buf = new byte[32];
|
||||
var expires = DateTime.UtcNow.AddHours(1);
|
||||
m_prng.GetBytes(buf);
|
||||
var token = Duplicati.Library.Utility.Utility.Base64UrlEncode(buf);
|
||||
while (token.Length > 0 && token.EndsWith("=", StringComparison.Ordinal))
|
||||
token = token.Substring(0, token.Length - 1);
|
||||
|
||||
m_activeTokens.AddOrUpdate(token, key => expires, (key, existingValue) =>
|
||||
{
|
||||
// Simulate the original behavior => if the token, against all odds, is already used
|
||||
// we throw an ArgumentException
|
||||
throw new ArgumentException("An element with the same key already exists in the dictionary.");
|
||||
});
|
||||
|
||||
response.Cookies.Add(new HttpServer.ResponseCookie(AUTH_COOKIE_NAME, token, expires));
|
||||
|
||||
using(var bw = new BodyWriter(response, request))
|
||||
bw.OutputOK();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var limitedAccess =
|
||||
request.Uri.AbsolutePath.StartsWith(RESTHandler.API_URI_PATH, StringComparison.OrdinalIgnoreCase)
|
||||
;
|
||||
|
||||
// Override to allow the CAPTCHA call to go through
|
||||
if (request.Uri.AbsolutePath.StartsWith(CAPTCHA_IMAGE_URI, StringComparison.OrdinalIgnoreCase) && request.Method == "GET")
|
||||
limitedAccess = false;
|
||||
|
||||
if (limitedAccess)
|
||||
{
|
||||
if (xsrf_token != null && m_activexsrf.ContainsKey(xsrf_token))
|
||||
{
|
||||
var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
m_activexsrf[xsrf_token] = expires;
|
||||
response.Cookies.Add(new ResponseCookie(XSRF_COOKIE_NAME, xsrf_token, expires));
|
||||
}
|
||||
else
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.BadRequest;
|
||||
response.Reason = "Missing XSRF Token. Please reload the page";
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPassword))
|
||||
return false;
|
||||
|
||||
foreach(var k in (from n in m_activeTokens where DateTime.UtcNow > n.Value select n.Key))
|
||||
m_activeTokens.TryRemove(k, out _);
|
||||
|
||||
|
||||
// If we have a valid token, proceed
|
||||
if (!string.IsNullOrWhiteSpace(auth_token))
|
||||
{
|
||||
DateTime expires;
|
||||
var found = m_activeTokens.TryGetValue(auth_token, out expires);
|
||||
if (!found)
|
||||
{
|
||||
auth_token = Duplicati.Library.Utility.Uri.UrlDecode(auth_token);
|
||||
found = m_activeTokens.TryGetValue(auth_token, out expires);
|
||||
}
|
||||
|
||||
if (found && DateTime.UtcNow < expires)
|
||||
{
|
||||
expires = DateTime.UtcNow.AddHours(1);
|
||||
|
||||
m_activeTokens[auth_token] = expires;
|
||||
response.Cookies.Add(new ResponseCookie(AUTH_COOKIE_NAME, auth_token, expires));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ("/".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase) || "/index.html".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
response.Redirect("/login.html");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (limitedAccess)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.Unauthorized;
|
||||
response.Reason = "Not logged in";
|
||||
response.AddHeader("Location", "login.html");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+124
-101
@@ -1,101 +1,124 @@
|
||||
// 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 Duplicati.Server.Serialization;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
public class BodyWriter : System.IO.StreamWriter, IDisposable
|
||||
{
|
||||
private readonly HttpServer.IHttpResponse m_resp;
|
||||
private readonly string m_jsonp;
|
||||
private static readonly object SUCCESS_RESPONSE = new { Status = "OK" };
|
||||
|
||||
// We override the format provider so all JSON output uses US format
|
||||
public override IFormatProvider FormatProvider
|
||||
{
|
||||
get { return System.Globalization.CultureInfo.InvariantCulture; }
|
||||
}
|
||||
|
||||
public BodyWriter(HttpServer.IHttpResponse resp, HttpServer.IHttpRequest request)
|
||||
: this(resp, request.QueryString["jsonp"].Value)
|
||||
{
|
||||
}
|
||||
|
||||
public BodyWriter(HttpServer.IHttpResponse resp, string jsonp)
|
||||
: base(resp.Body, resp.Encoding)
|
||||
{
|
||||
m_resp = resp;
|
||||
m_jsonp = jsonp;
|
||||
if (!m_resp.HeadersSent)
|
||||
m_resp.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
|
||||
}
|
||||
|
||||
protected override void Dispose (bool disposing)
|
||||
{
|
||||
if (!m_resp.HeadersSent)
|
||||
{
|
||||
base.Flush();
|
||||
m_resp.ContentLength = base.BaseStream.Length;
|
||||
m_resp.Send();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
public void SetOK()
|
||||
{
|
||||
m_resp.Reason = "OK";
|
||||
m_resp.Status = System.Net.HttpStatusCode.OK;
|
||||
}
|
||||
|
||||
public void OutputOK(object result = null)
|
||||
{
|
||||
SetOK();
|
||||
WriteJsonObject(result ?? SUCCESS_RESPONSE);
|
||||
}
|
||||
|
||||
public void WriteJsonObject(object o)
|
||||
{
|
||||
if (!m_resp.HeadersSent)
|
||||
m_resp.ContentType = "application/json";
|
||||
|
||||
using(this)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(m_jsonp))
|
||||
{
|
||||
this.Write(m_jsonp);
|
||||
this.Write('(');
|
||||
}
|
||||
|
||||
Serializer.SerializeJson(this, o, true);
|
||||
|
||||
if (!string.IsNullOrEmpty(m_jsonp))
|
||||
{
|
||||
this.Write(')');
|
||||
this.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Copyright (C) 2024, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Server.Serialization;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
public class BodyWriter : IDisposable, IAsyncDisposable
|
||||
{
|
||||
private readonly HttpServer.IHttpResponse m_resp;
|
||||
private readonly string m_jsonp;
|
||||
private static readonly object SUCCESS_RESPONSE = new { Status = "OK" };
|
||||
private readonly System.IO.StreamWriter m_bodyStreamWriter;
|
||||
|
||||
public BodyWriter(HttpServer.IHttpResponse resp, HttpServer.IHttpRequest request)
|
||||
: this(resp, request.QueryString["jsonp"].Value)
|
||||
{
|
||||
}
|
||||
|
||||
public BodyWriter(HttpServer.IHttpResponse resp, string jsonp)
|
||||
{
|
||||
m_bodyStreamWriter = new System.IO.StreamWriter(resp.Body, resp.Encoding);
|
||||
m_resp = resp;
|
||||
m_jsonp = jsonp;
|
||||
if (!m_resp.HeadersSent)
|
||||
m_resp.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
|
||||
}
|
||||
public void SetOK()
|
||||
{
|
||||
m_resp.Reason = "OK";
|
||||
m_resp.Status = System.Net.HttpStatusCode.OK;
|
||||
}
|
||||
|
||||
public void OutputOK(object result = null)
|
||||
{
|
||||
SetOK();
|
||||
WriteJsonObject(result ?? SUCCESS_RESPONSE);
|
||||
}
|
||||
|
||||
public void WriteJsonObject(object o)
|
||||
{
|
||||
if (!m_resp.HeadersSent)
|
||||
m_resp.ContentType = "application/json";
|
||||
|
||||
Task.Run(async () => {
|
||||
|
||||
if (!string.IsNullOrEmpty(m_jsonp))
|
||||
{
|
||||
await m_bodyStreamWriter.WriteAsync(m_jsonp);
|
||||
await m_bodyStreamWriter.WriteAsync('(');
|
||||
}
|
||||
|
||||
var oldCulture = CultureInfo.CurrentCulture;
|
||||
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
await Serializer.SerializeJsonAsync(m_bodyStreamWriter, o, true);
|
||||
CultureInfo.CurrentCulture = oldCulture;
|
||||
|
||||
if (!string.IsNullOrEmpty(m_jsonp))
|
||||
{
|
||||
await m_bodyStreamWriter.WriteAsync(')');
|
||||
await m_bodyStreamWriter.FlushAsync();
|
||||
}
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Task.Run(async () => {
|
||||
await DisposeAsync();
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private bool disposed = false;
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if(disposed) return;
|
||||
disposed = true;
|
||||
|
||||
if (!m_resp.HeadersSent)
|
||||
{
|
||||
await m_bodyStreamWriter.FlushAsync();
|
||||
m_resp.ContentLength = m_bodyStreamWriter.BaseStream.Length;
|
||||
m_resp.Send();
|
||||
}
|
||||
await m_bodyStreamWriter.DisposeAsync();
|
||||
}
|
||||
|
||||
internal void Flush()
|
||||
{
|
||||
Task.Run(async () => {
|
||||
await m_bodyStreamWriter.FlushAsync();
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
internal void Write(string v)
|
||||
{
|
||||
Task.Run(async () => {
|
||||
await m_bodyStreamWriter.WriteAsync(v);
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-1
@@ -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.Drawing;
|
||||
-1
@@ -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 HttpServer;
|
||||
+4
-4
@@ -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.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
@@ -26,6 +25,7 @@ using System.Linq;
|
||||
using HttpServer.HttpModules;
|
||||
|
||||
using Duplicati.Server.WebServer.RESTMethods;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
@@ -44,7 +44,7 @@ namespace Duplicati.Server.WebServer
|
||||
static RESTHandler()
|
||||
{
|
||||
var lst =
|
||||
from n in typeof(RESTHandler).Assembly.GetTypes()
|
||||
from n in typeof(IRESTMethod).Assembly.GetTypes()
|
||||
where
|
||||
n.Namespace == typeof(IRESTMethod).Namespace
|
||||
&&
|
||||
@@ -197,7 +197,7 @@ namespace Duplicati.Server.WebServer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.DataConnection.LogError("", string.Format("Request for {0} gave error", info.Request.Uri), ex);
|
||||
FIXMEGlobal.DataConnection.LogError("", string.Format("Request for {0} gave error", info.Request.Uri), ex);
|
||||
Console.WriteLine(ex);
|
||||
|
||||
try
|
||||
@@ -225,7 +225,7 @@ namespace Duplicati.Server.WebServer
|
||||
}
|
||||
catch (Exception flex)
|
||||
{
|
||||
Program.DataConnection.LogError("", "Reporting error gave error", flex);
|
||||
FIXMEGlobal.DataConnection.LogError("", "Reporting error gave error", flex);
|
||||
}
|
||||
}
|
||||
}
|
||||
+449
-474
@@ -1,474 +1,449 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using HttpServer.HttpModules;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Duplicati.Library.Common.IO;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
public class Server
|
||||
{
|
||||
/// <summary>
|
||||
/// The tag used for logging
|
||||
/// </summary>
|
||||
private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType<Server>();
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webroot folder
|
||||
/// </summary>
|
||||
public const string OPTION_WEBROOT = "webservice-webroot";
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webservice listen port
|
||||
/// </summary>
|
||||
public const string OPTION_PORT = "webservice-port";
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webservice listen interface
|
||||
/// </summary>
|
||||
public const string OPTION_INTERFACE = "webservice-interface";
|
||||
|
||||
/// <summary>
|
||||
/// The default path to the web root
|
||||
/// </summary>
|
||||
public const string DEFAULT_OPTION_WEBROOT = "webroot";
|
||||
|
||||
/// <summary>
|
||||
/// The default listening port
|
||||
/// </summary>
|
||||
public const int DEFAULT_OPTION_PORT = 8200;
|
||||
|
||||
/// <summary>
|
||||
/// Option for setting the webservice SSL certificate
|
||||
/// </summary>
|
||||
public const string OPTION_SSLCERTIFICATEFILE = "webservice-sslcertificatefile";
|
||||
|
||||
/// <summary>
|
||||
/// Option for setting the webservice SSL certificate key
|
||||
/// </summary>
|
||||
public const string OPTION_SSLCERTIFICATEFILEPASSWORD = "webservice-sslcertificatepassword";
|
||||
|
||||
/// <summary>
|
||||
/// The default listening interface
|
||||
/// </summary>
|
||||
public const string DEFAULT_OPTION_INTERFACE = "loopback";
|
||||
|
||||
/// <summary>
|
||||
/// The single webserver instance
|
||||
/// </summary>
|
||||
private readonly HttpServer.HttpServer m_server;
|
||||
|
||||
/// <summary>
|
||||
/// The webserver listening port
|
||||
/// </summary>
|
||||
public readonly int Port;
|
||||
|
||||
/// <summary>
|
||||
/// A string that is sent out instead of password values
|
||||
/// </summary>
|
||||
public const string PASSWORD_PLACEHOLDER = "**********";
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the webserver and starts it
|
||||
/// </summary>
|
||||
/// <param name="options">A set of options</param>
|
||||
public Server(IDictionary<string, string> options)
|
||||
{
|
||||
string portstring;
|
||||
IEnumerable<int> ports = null;
|
||||
options.TryGetValue(OPTION_PORT, out portstring);
|
||||
if (!string.IsNullOrEmpty(portstring))
|
||||
ports =
|
||||
from n in portstring.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
where int.TryParse(n, out _)
|
||||
select int.Parse(n);
|
||||
|
||||
if (ports == null || !ports.Any())
|
||||
ports = new int[] { DEFAULT_OPTION_PORT };
|
||||
|
||||
string interfacestring;
|
||||
System.Net.IPAddress listenInterface;
|
||||
options.TryGetValue(OPTION_INTERFACE, out interfacestring);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(interfacestring))
|
||||
interfacestring = Program.DataConnection.ApplicationSettings.ServerListenInterface;
|
||||
if (string.IsNullOrWhiteSpace(interfacestring))
|
||||
interfacestring = DEFAULT_OPTION_INTERFACE;
|
||||
|
||||
if (interfacestring.Trim() == "*" || interfacestring.Trim().Equals("any", StringComparison.OrdinalIgnoreCase) || interfacestring.Trim().Equals("all", StringComparison.OrdinalIgnoreCase))
|
||||
listenInterface = System.Net.IPAddress.Any;
|
||||
else if (interfacestring.Trim() == "loopback")
|
||||
listenInterface = System.Net.IPAddress.Loopback;
|
||||
else
|
||||
listenInterface = System.Net.IPAddress.Parse(interfacestring);
|
||||
|
||||
string certificateFile;
|
||||
options.TryGetValue(OPTION_SSLCERTIFICATEFILE, out certificateFile);
|
||||
|
||||
string certificateFilePassword;
|
||||
options.TryGetValue(OPTION_SSLCERTIFICATEFILEPASSWORD, out certificateFilePassword);
|
||||
|
||||
X509Certificate2 cert = null;
|
||||
bool certValid = false;
|
||||
|
||||
if (certificateFile == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
cert = Program.DataConnection.ApplicationSettings.ServerSSLCertificate;
|
||||
|
||||
if (cert != null)
|
||||
certValid = cert.HasPrivateKey;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Duplicati.Library.Logging.Log.WriteWarningMessage(LOGTAG, "DefectStoredSSLCert", ex, Strings.Server.DefectSSLCertInDatabase);
|
||||
}
|
||||
}
|
||||
else if (certificateFile.Length == 0)
|
||||
{
|
||||
Program.DataConnection.ApplicationSettings.ServerSSLCertificate = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(certificateFilePassword))
|
||||
cert = new X509Certificate2(certificateFile, "", X509KeyStorageFlags.Exportable);
|
||||
else
|
||||
cert = new X509Certificate2(certificateFile, certificateFilePassword, X509KeyStorageFlags.Exportable);
|
||||
|
||||
certValid = cert.HasPrivateKey;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Strings.Server.SSLCertificateFailure(ex.Message), ex);
|
||||
}
|
||||
}
|
||||
|
||||
// If we are in hosted mode with no specified port,
|
||||
// then try different ports
|
||||
foreach (var p in ports)
|
||||
try
|
||||
{
|
||||
// Due to the way the server is initialized,
|
||||
// we cannot try to start it again on another port,
|
||||
// so we create a new server for each attempt
|
||||
|
||||
var server = CreateServer(options);
|
||||
|
||||
if (!certValid)
|
||||
server.Start(listenInterface, p);
|
||||
else
|
||||
{
|
||||
var secProtocols = System.Security.Authentication.SslProtocols.Tls12;
|
||||
|
||||
try
|
||||
{
|
||||
//try TLS 1.3 (type not available on .NET < 4.8)
|
||||
secProtocols = System.Security.Authentication.SslProtocols.Tls12 | (System.Security.Authentication.SslProtocols)12288;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
}
|
||||
server.Start(listenInterface, p, cert, secProtocols, null, false);
|
||||
}
|
||||
|
||||
m_server = server;
|
||||
m_server.ServerName = string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
|
||||
this.Port = p;
|
||||
|
||||
if (interfacestring != Program.DataConnection.ApplicationSettings.ServerListenInterface)
|
||||
Program.DataConnection.ApplicationSettings.ServerListenInterface = interfacestring;
|
||||
|
||||
if (certValid && !cert.Equals(Program.DataConnection.ApplicationSettings.ServerSSLCertificate))
|
||||
Program.DataConnection.ApplicationSettings.ServerSSLCertificate = cert;
|
||||
|
||||
Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "ServerListening", Strings.Server.StartedServer(listenInterface.ToString(), p));
|
||||
|
||||
return;
|
||||
}
|
||||
catch (System.Net.Sockets.SocketException)
|
||||
{
|
||||
}
|
||||
|
||||
throw new Exception(Strings.Server.ServerStartFailure(ports));
|
||||
}
|
||||
|
||||
private static void AddMimeTypes(FileModule fm)
|
||||
{
|
||||
fm.AddDefaultMimeTypes();
|
||||
fm.MimeTypes["htc"] = "text/x-component";
|
||||
fm.MimeTypes["json"] = "application/json";
|
||||
fm.MimeTypes["map"] = "application/json";
|
||||
fm.MimeTypes["htm"] = "text/html; charset=utf-8";
|
||||
fm.MimeTypes["html"] = "text/html; charset=utf-8";
|
||||
fm.MimeTypes["hbs"] = "application/x-handlebars-template";
|
||||
fm.MimeTypes["woff"] = "application/font-woff";
|
||||
fm.MimeTypes["woff2"] = "application/font-woff";
|
||||
}
|
||||
|
||||
private static HttpServer.HttpServer CreateServer(IDictionary<string, string> options)
|
||||
{
|
||||
HttpServer.HttpServer server = new HttpServer.HttpServer();
|
||||
|
||||
server.Add(new HostHeaderChecker());
|
||||
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SYNO_DSM_AUTH") ?? string.Empty, "1"))
|
||||
server.Add(new SynologyAuthenticationHandler());
|
||||
|
||||
server.Add(new AuthenticationHandler());
|
||||
|
||||
server.Add(new RESTHandler());
|
||||
|
||||
string webroot = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||
string install_webroot = System.IO.Path.Combine(Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "webroot");
|
||||
|
||||
#if DEBUG
|
||||
// Easy test for extensions while debugging
|
||||
install_webroot = Library.AutoUpdater.UpdaterManager.InstalledBaseDir;
|
||||
|
||||
if (!System.IO.Directory.Exists(System.IO.Path.Combine(webroot, "webroot")))
|
||||
{
|
||||
//For debug we go "../../../.." to get out of "GUI/Duplicati.GUI.TrayIcon/bin/debug"
|
||||
string tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..", "..", ".."));
|
||||
tmpwebroot = System.IO.Path.Combine(tmpwebroot, "Server");
|
||||
if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot")))
|
||||
webroot = tmpwebroot;
|
||||
else
|
||||
{
|
||||
//If we are running the server standalone, we only need to exit "bin/Debug"
|
||||
tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", ".."));
|
||||
if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot")))
|
||||
webroot = tmpwebroot;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
webroot = System.IO.Path.Combine(webroot, "webroot");
|
||||
|
||||
if (options.ContainsKey(OPTION_WEBROOT))
|
||||
{
|
||||
string userroot = options[OPTION_WEBROOT];
|
||||
#if DEBUG
|
||||
//In debug mode we do not care where the path points
|
||||
#else
|
||||
//In release mode we check that the user supplied path is located
|
||||
// in the same folders as the running application, to avoid users
|
||||
// that inadvertently expose top level folders
|
||||
if (!string.IsNullOrWhiteSpace(userroot)
|
||||
&&
|
||||
(
|
||||
userroot.StartsWith(Util.AppendDirSeparator(System.Reflection.Assembly.GetExecutingAssembly().Location), Library.Utility.Utility.ClientFilenameStringComparison)
|
||||
||
|
||||
userroot.StartsWith(Util.AppendDirSeparator(Program.StartupPath), Library.Utility.Utility.ClientFilenameStringComparison)
|
||||
)
|
||||
)
|
||||
#endif
|
||||
{
|
||||
webroot = userroot;
|
||||
install_webroot = webroot;
|
||||
}
|
||||
}
|
||||
|
||||
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "customized")))
|
||||
{
|
||||
var customized_files = new CacheControlFileHandler("/customized/", System.IO.Path.Combine(install_webroot, "customized"));
|
||||
AddMimeTypes(customized_files);
|
||||
server.Add(customized_files);
|
||||
}
|
||||
|
||||
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "oem")))
|
||||
{
|
||||
var oem_files = new CacheControlFileHandler("/oem/", System.IO.Path.Combine(install_webroot, "oem"));
|
||||
AddMimeTypes(oem_files);
|
||||
server.Add(oem_files);
|
||||
}
|
||||
|
||||
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "package")))
|
||||
{
|
||||
var proxy_files = new CacheControlFileHandler("/proxy/", System.IO.Path.Combine(install_webroot, "package"));
|
||||
AddMimeTypes(proxy_files);
|
||||
server.Add(proxy_files);
|
||||
}
|
||||
|
||||
var fh = new CacheControlFileHandler("/", webroot, true);
|
||||
AddMimeTypes(fh);
|
||||
server.Add(fh);
|
||||
|
||||
server.Add(new IndexHtmlHandler(webroot));
|
||||
#if DEBUG
|
||||
//For debugging, it is nice to know when we get a 404
|
||||
server.Add(new DebugReportHandler());
|
||||
#endif
|
||||
return server;
|
||||
}
|
||||
|
||||
private class DebugReportHandler : HttpModule
|
||||
{
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
System.Diagnostics.Trace.WriteLine(string.Format("Rejecting request for {0}", request.Uri));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class CacheControlFileHandler : FileModule
|
||||
{
|
||||
public CacheControlFileHandler(string baseUri, string basePath, bool useLastModifiedHeader = false)
|
||||
: base(baseUri, basePath, useLastModifiedHeader)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
if (!this.CanHandle(request.Uri))
|
||||
return false;
|
||||
|
||||
if (request.Uri.AbsolutePath.EndsWith("index.html", StringComparison.Ordinal) || request.Uri.AbsolutePath.EndsWith("index.htm", StringComparison.Ordinal))
|
||||
response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
|
||||
else
|
||||
response.AddHeader("Cache-Control", "max-age=" + (60 * 60 * 24));
|
||||
return base.Process(request, response, session);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Module for injecting host header verification
|
||||
/// </summary>
|
||||
private class HostHeaderChecker : HttpModule
|
||||
{
|
||||
/// <summary>
|
||||
/// The hostnames that we allow
|
||||
/// </summary>
|
||||
private string[] m_lastSplitNames;
|
||||
|
||||
/// <summary>
|
||||
/// The string used to generate m_lastSplitNames;
|
||||
/// </summary>
|
||||
private string m_lastAllowed;
|
||||
|
||||
/// <summary>
|
||||
/// A regex to detect potential IPv4 addresses.
|
||||
/// Note that this also detects things that are not valid IPv4.
|
||||
/// </summary>
|
||||
private static readonly System.Text.RegularExpressions.Regex IPV4 = new System.Text.RegularExpressions.Regex(@"((\d){1,3}\.){3}(\d){1,3}");
|
||||
/// <summary>
|
||||
/// A regex to detect potential IPv6 addresses.
|
||||
/// Note that this also detects things that are not valid IPv6.
|
||||
/// </summary>
|
||||
private static readonly System.Text.RegularExpressions.Regex IPV6 = new System.Text.RegularExpressions.Regex(@"(\:)?(\:?[A-Fa-f0-9]{1,4}\:?){1,8}(\:)?");
|
||||
|
||||
/// <summary>
|
||||
/// The hostnames that are always allowed
|
||||
/// </summary>
|
||||
private static readonly string[] DEFAULT_ALLOWED = new string[] { "localhost", "127.0.0.1", "::1", "localhost.localdomain" };
|
||||
|
||||
/// <summary>
|
||||
/// Process the received request
|
||||
/// </summary>
|
||||
/// <returns>A flag indicating if the request is handled.</returns>
|
||||
/// <param name="request">The received request.</param>
|
||||
/// <param name="response">The response object.</param>
|
||||
/// <param name="session">The session state.</param>
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
string[] h = null;
|
||||
var hstring = Program.DataConnection.ApplicationSettings.AllowedHostnames;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(hstring))
|
||||
{
|
||||
h = m_lastSplitNames;
|
||||
if (hstring != m_lastAllowed)
|
||||
{
|
||||
m_lastAllowed = hstring;
|
||||
h = m_lastSplitNames = (hstring ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
if (h == null || h.Length == 0)
|
||||
h = null;
|
||||
}
|
||||
|
||||
// For some reason, the web server strips out the host header
|
||||
var host = request.Headers["Host"];
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
host = request.Uri.Host;
|
||||
|
||||
// This should not happen
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
response.Reason = "Invalid request, missing host header";
|
||||
response.Status = System.Net.HttpStatusCode.Forbidden;
|
||||
var msg = System.Text.Encoding.ASCII.GetBytes(response.Reason);
|
||||
response.ContentType = "text/plain";
|
||||
response.ContentLength = msg.Length;
|
||||
response.Body.Write(msg, 0, msg.Length);
|
||||
response.Send();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check the hostnames we always allow
|
||||
if (DEFAULT_ALLOWED.Contains(host, StringComparer.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
// Then the user specified ones
|
||||
if (h != null && h.Contains(host, StringComparer.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
// Disable checks if we have an asterisk
|
||||
if (h != null && Array.IndexOf(h, "*") >= 0)
|
||||
return false;
|
||||
|
||||
// Finally, check if we have a potential IP address
|
||||
var v4 = IPV4.Match(host);
|
||||
var v6 = IPV6.Match(host);
|
||||
|
||||
if ((v4.Success && v4.Length == host.Length) || (v6.Success && v6.Length == host.Length))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Verify that the hostname is indeed a valid IP address
|
||||
System.Net.IPAddress.Parse(host);
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
|
||||
// Failed to find a valid header
|
||||
response.Reason = $"The host header sent by the client is not allowed";
|
||||
response.Status = System.Net.HttpStatusCode.Forbidden;
|
||||
var txt = System.Text.Encoding.ASCII.GetBytes(response.Reason);
|
||||
response.ContentType = "text/plain";
|
||||
response.ContentLength = txt.Length;
|
||||
response.Body.Write(txt, 0, txt.Length);
|
||||
response.Send();
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using HttpServer.HttpModules;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
public class Server
|
||||
{
|
||||
/// <summary>
|
||||
/// The tag used for logging
|
||||
/// </summary>
|
||||
private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType<Server>();
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webroot folder
|
||||
/// </summary>
|
||||
public const string OPTION_WEBROOT = "webservice-webroot";
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webservice listen port
|
||||
/// </summary>
|
||||
public const string OPTION_PORT = "webservice-port";
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webservice listen interface
|
||||
/// </summary>
|
||||
public const string OPTION_INTERFACE = "webservice-interface";
|
||||
|
||||
/// <summary>
|
||||
/// The default path to the web root
|
||||
/// </summary>
|
||||
public const string DEFAULT_OPTION_WEBROOT = "webroot";
|
||||
|
||||
/// <summary>
|
||||
/// The default listening port
|
||||
/// </summary>
|
||||
public const int DEFAULT_OPTION_PORT = 8200;
|
||||
|
||||
/// <summary>
|
||||
/// Option for setting the webservice SSL certificate
|
||||
/// </summary>
|
||||
public const string OPTION_SSLCERTIFICATEFILE = "webservice-sslcertificatefile";
|
||||
|
||||
/// <summary>
|
||||
/// Option for setting the webservice SSL certificate key
|
||||
/// </summary>
|
||||
public const string OPTION_SSLCERTIFICATEFILEPASSWORD = "webservice-sslcertificatepassword";
|
||||
|
||||
/// <summary>
|
||||
/// The default listening interface
|
||||
/// </summary>
|
||||
public const string DEFAULT_OPTION_INTERFACE = "loopback";
|
||||
|
||||
/// <summary>
|
||||
/// The single webserver instance
|
||||
/// </summary>
|
||||
private readonly HttpServer.HttpServer m_server;
|
||||
|
||||
/// <summary>
|
||||
/// The webserver listening port
|
||||
/// </summary>
|
||||
public readonly int Port;
|
||||
|
||||
/// <summary>
|
||||
/// A string that is sent out instead of password values
|
||||
/// </summary>
|
||||
public const string PASSWORD_PLACEHOLDER = "**********";
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the webserver and starts it
|
||||
/// </summary>
|
||||
/// <param name="options">A set of options</param>
|
||||
public Server(IDictionary<string, string> options)
|
||||
{
|
||||
string portstring;
|
||||
IEnumerable<int> ports = null;
|
||||
options.TryGetValue(OPTION_PORT, out portstring);
|
||||
if (!string.IsNullOrEmpty(portstring))
|
||||
ports =
|
||||
from n in portstring.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
where int.TryParse(n, out _)
|
||||
select int.Parse(n);
|
||||
|
||||
if (ports == null || !ports.Any())
|
||||
ports = new int[] { DEFAULT_OPTION_PORT };
|
||||
|
||||
string interfacestring;
|
||||
System.Net.IPAddress listenInterface;
|
||||
options.TryGetValue(OPTION_INTERFACE, out interfacestring);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(interfacestring))
|
||||
interfacestring = FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface;
|
||||
if (string.IsNullOrWhiteSpace(interfacestring))
|
||||
interfacestring = DEFAULT_OPTION_INTERFACE;
|
||||
|
||||
if (interfacestring.Trim() == "*" || interfacestring.Trim().Equals("any", StringComparison.OrdinalIgnoreCase) || interfacestring.Trim().Equals("all", StringComparison.OrdinalIgnoreCase))
|
||||
listenInterface = System.Net.IPAddress.Any;
|
||||
else if (interfacestring.Trim() == "loopback")
|
||||
listenInterface = System.Net.IPAddress.Loopback;
|
||||
else
|
||||
listenInterface = System.Net.IPAddress.Parse(interfacestring);
|
||||
|
||||
string certificateFile;
|
||||
options.TryGetValue(OPTION_SSLCERTIFICATEFILE, out certificateFile);
|
||||
|
||||
string certificateFilePassword;
|
||||
options.TryGetValue(OPTION_SSLCERTIFICATEFILEPASSWORD, out certificateFilePassword);
|
||||
|
||||
X509Certificate2 cert = null;
|
||||
bool certValid = false;
|
||||
|
||||
if (certificateFile == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
cert = FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate;
|
||||
|
||||
if (cert != null)
|
||||
certValid = cert.HasPrivateKey;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Duplicati.Library.Logging.Log.WriteWarningMessage(LOGTAG, "DefectStoredSSLCert", ex, Strings.Server.DefectSSLCertInDatabase);
|
||||
}
|
||||
}
|
||||
else if (certificateFile.Length == 0)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(certificateFilePassword))
|
||||
cert = new X509Certificate2(certificateFile, "", X509KeyStorageFlags.Exportable);
|
||||
else
|
||||
cert = new X509Certificate2(certificateFile, certificateFilePassword, X509KeyStorageFlags.Exportable);
|
||||
|
||||
certValid = cert.HasPrivateKey;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Strings.Server.SSLCertificateFailure(ex.Message), ex);
|
||||
}
|
||||
}
|
||||
|
||||
// If we are in hosted mode with no specified port,
|
||||
// then try different ports
|
||||
foreach (var p in ports)
|
||||
try
|
||||
{
|
||||
// Due to the way the server is initialized,
|
||||
// we cannot try to start it again on another port,
|
||||
// so we create a new server for each attempt
|
||||
|
||||
var server = CreateServer(options);
|
||||
|
||||
if (!certValid)
|
||||
server.Start(listenInterface, p);
|
||||
else
|
||||
{
|
||||
var secProtocols = System.Security.Authentication.SslProtocols.Tls12;
|
||||
|
||||
try
|
||||
{
|
||||
//try TLS 1.3 (type not available on .NET < 4.8)
|
||||
secProtocols = System.Security.Authentication.SslProtocols.Tls12 | (System.Security.Authentication.SslProtocols)12288;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
}
|
||||
server.Start(listenInterface, p, cert, secProtocols, null, false);
|
||||
}
|
||||
|
||||
m_server = server;
|
||||
m_server.ServerName = string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
|
||||
this.Port = p;
|
||||
|
||||
if (interfacestring != FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface)
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface = interfacestring;
|
||||
|
||||
if (certValid && !cert.Equals(FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate))
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate = cert;
|
||||
|
||||
Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "ServerListening", Strings.Server.StartedServer(listenInterface.ToString(), p));
|
||||
|
||||
return;
|
||||
}
|
||||
catch (System.Net.Sockets.SocketException)
|
||||
{
|
||||
}
|
||||
|
||||
throw new Exception(Strings.Server.ServerStartFailure(ports));
|
||||
}
|
||||
|
||||
private static void AddMimeTypes(FileModule fm)
|
||||
{
|
||||
fm.AddDefaultMimeTypes();
|
||||
fm.MimeTypes["htc"] = "text/x-component";
|
||||
fm.MimeTypes["json"] = "application/json";
|
||||
fm.MimeTypes["map"] = "application/json";
|
||||
fm.MimeTypes["htm"] = "text/html; charset=utf-8";
|
||||
fm.MimeTypes["html"] = "text/html; charset=utf-8";
|
||||
fm.MimeTypes["hbs"] = "application/x-handlebars-template";
|
||||
fm.MimeTypes["woff"] = "application/font-woff";
|
||||
fm.MimeTypes["woff2"] = "application/font-woff";
|
||||
}
|
||||
|
||||
private static HttpServer.HttpServer CreateServer(IDictionary<string, string> options)
|
||||
{
|
||||
HttpServer.HttpServer server = new HttpServer.HttpServer();
|
||||
|
||||
server.Add(new HostHeaderChecker());
|
||||
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SYNO_DSM_AUTH") ?? string.Empty, "1"))
|
||||
server.Add(new SynologyAuthenticationHandler());
|
||||
|
||||
server.Add(new AuthenticationHandler());
|
||||
|
||||
server.Add(new RESTHandler());
|
||||
|
||||
string webroot = Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR;
|
||||
string install_webroot = System.IO.Path.Combine(webroot, "webroot");
|
||||
|
||||
#if DEBUG
|
||||
// Easy test for extensions while debugging
|
||||
install_webroot = Library.AutoUpdater.UpdaterManager.INSTALLATIONDIR;
|
||||
|
||||
if (!System.IO.Directory.Exists(System.IO.Path.Combine(webroot, "webroot")))
|
||||
{
|
||||
//For debug we go "../../../.." to get out of "GUI/Duplicati.GUI.TrayIcon/bin/debug"
|
||||
string tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..", "..", ".."));
|
||||
tmpwebroot = System.IO.Path.Combine(tmpwebroot, "Server");
|
||||
if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot")))
|
||||
webroot = tmpwebroot;
|
||||
else
|
||||
{
|
||||
//If we are running the server standalone, we only need to exit "bin/Debug"
|
||||
tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", ".."));
|
||||
if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot")))
|
||||
webroot = tmpwebroot;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
webroot = System.IO.Path.Combine(webroot, "webroot");
|
||||
|
||||
if (options.ContainsKey(OPTION_WEBROOT))
|
||||
{
|
||||
string userroot = options[OPTION_WEBROOT];
|
||||
#if DEBUG
|
||||
//In debug mode we do not care where the path points
|
||||
#else
|
||||
//In release mode we check that the user supplied path is located
|
||||
// in the same folders as the running application, to avoid users
|
||||
// that inadvertently expose top level folders
|
||||
if (!string.IsNullOrWhiteSpace(userroot)
|
||||
&& userroot.StartsWith(Util.AppendDirSeparator(Duplicati.Library.Utility.Utility.getEntryAssembly().Location), Library.Utility.Utility.ClientFilenameStringComparison))
|
||||
#endif
|
||||
{
|
||||
webroot = userroot;
|
||||
install_webroot = webroot;
|
||||
}
|
||||
}
|
||||
|
||||
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "customized")))
|
||||
{
|
||||
var customized_files = new CacheControlFileHandler("/customized/", System.IO.Path.Combine(install_webroot, "customized"));
|
||||
AddMimeTypes(customized_files);
|
||||
server.Add(customized_files);
|
||||
}
|
||||
|
||||
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "oem")))
|
||||
{
|
||||
var oem_files = new CacheControlFileHandler("/oem/", System.IO.Path.Combine(install_webroot, "oem"));
|
||||
AddMimeTypes(oem_files);
|
||||
server.Add(oem_files);
|
||||
}
|
||||
|
||||
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "package")))
|
||||
{
|
||||
var proxy_files = new CacheControlFileHandler("/proxy/", System.IO.Path.Combine(install_webroot, "package"));
|
||||
AddMimeTypes(proxy_files);
|
||||
server.Add(proxy_files);
|
||||
}
|
||||
|
||||
var fh = new CacheControlFileHandler("/", webroot, true);
|
||||
AddMimeTypes(fh);
|
||||
server.Add(fh);
|
||||
|
||||
server.Add(new IndexHtmlHandler(webroot));
|
||||
#if DEBUG
|
||||
//For debugging, it is nice to know when we get a 404
|
||||
server.Add(new DebugReportHandler());
|
||||
#endif
|
||||
return server;
|
||||
}
|
||||
|
||||
private class DebugReportHandler : HttpModule
|
||||
{
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
System.Diagnostics.Trace.WriteLine(string.Format("Rejecting request for {0}", request.Uri));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class CacheControlFileHandler : FileModule
|
||||
{
|
||||
public CacheControlFileHandler(string baseUri, string basePath, bool useLastModifiedHeader = false)
|
||||
: base(baseUri, basePath, useLastModifiedHeader)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
if (!this.CanHandle(request.Uri))
|
||||
return false;
|
||||
|
||||
if (request.Uri.AbsolutePath.EndsWith("index.html", StringComparison.Ordinal) || request.Uri.AbsolutePath.EndsWith("index.htm", StringComparison.Ordinal))
|
||||
response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
|
||||
else
|
||||
response.AddHeader("Cache-Control", "max-age=" + (60 * 60 * 24));
|
||||
return base.Process(request, response, session);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Module for injecting host header verification
|
||||
/// </summary>
|
||||
private class HostHeaderChecker : HttpModule
|
||||
{
|
||||
/// <summary>
|
||||
/// The hostnames that we allow
|
||||
/// </summary>
|
||||
private string[] m_lastSplitNames;
|
||||
|
||||
/// <summary>
|
||||
/// The string used to generate m_lastSplitNames;
|
||||
/// </summary>
|
||||
private string m_lastAllowed;
|
||||
|
||||
/// <summary>
|
||||
/// A regex to detect potential IPv4 addresses.
|
||||
/// Note that this also detects things that are not valid IPv4.
|
||||
/// </summary>
|
||||
private static readonly System.Text.RegularExpressions.Regex IPV4 = new System.Text.RegularExpressions.Regex(@"((\d){1,3}\.){3}(\d){1,3}");
|
||||
/// <summary>
|
||||
/// A regex to detect potential IPv6 addresses.
|
||||
/// Note that this also detects things that are not valid IPv6.
|
||||
/// </summary>
|
||||
private static readonly System.Text.RegularExpressions.Regex IPV6 = new System.Text.RegularExpressions.Regex(@"(\:)?(\:?[A-Fa-f0-9]{1,4}\:?){1,8}(\:)?");
|
||||
|
||||
/// <summary>
|
||||
/// The hostnames that are always allowed
|
||||
/// </summary>
|
||||
private static readonly string[] DEFAULT_ALLOWED = new string[] { "localhost", "127.0.0.1", "::1", "localhost.localdomain" };
|
||||
|
||||
/// <summary>
|
||||
/// Process the received request
|
||||
/// </summary>
|
||||
/// <returns>A flag indicating if the request is handled.</returns>
|
||||
/// <param name="request">The received request.</param>
|
||||
/// <param name="response">The response object.</param>
|
||||
/// <param name="session">The session state.</param>
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
string[] h = null;
|
||||
var hstring = FIXMEGlobal.DataConnection.ApplicationSettings.AllowedHostnames;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(hstring))
|
||||
{
|
||||
h = m_lastSplitNames;
|
||||
if (hstring != m_lastAllowed)
|
||||
{
|
||||
m_lastAllowed = hstring;
|
||||
h = m_lastSplitNames = (hstring ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
if (h == null || h.Length == 0)
|
||||
h = null;
|
||||
}
|
||||
|
||||
// For some reason, the web server strips out the host header
|
||||
var host = request.Headers["Host"];
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
host = request.Uri.Host;
|
||||
|
||||
// This should not happen
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
response.Reason = "Invalid request, missing host header";
|
||||
response.Status = System.Net.HttpStatusCode.Forbidden;
|
||||
var msg = System.Text.Encoding.ASCII.GetBytes(response.Reason);
|
||||
response.ContentType = "text/plain";
|
||||
response.ContentLength = msg.Length;
|
||||
response.Body.Write(msg, 0, msg.Length);
|
||||
response.Send();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check the hostnames we always allow
|
||||
if (DEFAULT_ALLOWED.Contains(host, StringComparer.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
// Then the user specified ones
|
||||
if (h != null && h.Contains(host, StringComparer.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
// Disable checks if we have an asterisk
|
||||
if (h != null && Array.IndexOf(h, "*") >= 0)
|
||||
return false;
|
||||
|
||||
// Finally, check if we have a potential IP address
|
||||
var v4 = IPV4.Match(host);
|
||||
var v6 = IPV6.Match(host);
|
||||
|
||||
if ((v4.Success && v4.Length == host.Length) || (v6.Success && v6.Length == host.Length))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Verify that the hostname is indeed a valid IP address
|
||||
System.Net.IPAddress.Parse(host);
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
|
||||
// Failed to find a valid header
|
||||
response.Reason = $"The host header sent by the client is not allowed";
|
||||
response.Status = System.Net.HttpStatusCode.Forbidden;
|
||||
var txt = System.Text.Encoding.ASCII.GetBytes(response.Reason);
|
||||
response.ContentType = "text/plain";
|
||||
response.ContentLength = txt.Length;
|
||||
response.Body.Write(txt, 0, txt.Length);
|
||||
response.Send();
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
@@ -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.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
Backup: {
|
||||
Settings: [
|
||||
{Name: "encryption-module", Value: "aes"},
|
||||
{Name: "compression-module", Value: "zip"},
|
||||
{Name: "dblock-size", Value: "50mb"},
|
||||
{Name: "keep-time", Value: ""}
|
||||
]
|
||||
},
|
||||
Schedule: {
|
||||
Repeat: "1D",
|
||||
Time: "13:00:00",
|
||||
AllowedDays: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
||||
}
|
||||
{
|
||||
Backup: {
|
||||
Settings: [
|
||||
{Name: "encryption-module", Value: "aes"},
|
||||
{Name: "compression-module", Value: "zip"},
|
||||
{Name: "dblock-size", Value: "50mb"},
|
||||
{Name: "keep-time", Value: ""}
|
||||
]
|
||||
},
|
||||
Schedule: {
|
||||
Repeat: "1D",
|
||||
Time: "13:00:00",
|
||||
AllowedDays: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
||||
}
|
||||
}
|
||||
+162
-23
@@ -1,7 +1,7 @@
|
||||
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31919.166
|
||||
VisualStudioVersion = 17.1.32210.238
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Utility", "Duplicati\Library\Utility\Duplicati.Library.Utility.csproj", "{DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65}"
|
||||
EndProject
|
||||
@@ -17,7 +17,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Logging",
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.S3", "Duplicati\Library\Backend\S3\Duplicati.Library.Backend.S3.csproj", "{C03F6DFD-805A-4BE0-9338-64870ADDB4A2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine", "Duplicati\CommandLine\Duplicati.CommandLine.csproj", "{81765A64-3661-4E3E-B850-2F6F87A51F74}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine", "Duplicati\CommandLine\CLI\Duplicati.CommandLine.csproj", "{81765A64-3661-4E3E-B850-2F6F87A51F74}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.WEBDAV", "Duplicati\Library\Backend\WEBDAV\Duplicati.Library.Backend.WEBDAV.csproj", "{BAE27510-8B5D-44B2-B33E-372A98908041}"
|
||||
EndProject
|
||||
@@ -79,8 +79,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.UsageRepo
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.SharePoint", "Duplicati\Library\Backend\SharePoint\Duplicati.Library.Backend.SharePoint.csproj", "{59C8BBC5-6E42-46FB-AB3E-6C183A82459A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsService", "Duplicati\WindowsService\WindowsService.csproj", "{BEF7AF9A-3978-4F90-8592-198BF6EA6C6B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.AlternativeFTP", "Duplicati\Library\Backend\AlternativeFTP\Duplicati.Library.Backend.AlternativeFTP.csproj", "{3F47CB0C-CEE1-447F-9C26-7F8E419E3E3F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Dropbox", "Duplicati\Library\Backend\Dropbox\Duplicati.Library.Backend.Dropbox.csproj", "{B20A7CEE-9C5B-47B9-8B76-BC85ADFE8493}"
|
||||
@@ -99,13 +97,56 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.Confi
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Encryption", "Duplicati\Library\Encryption\Duplicati.Library.Encryption.csproj", "{2CF2D90E-C25B-47AD-91E0-98451BAB8058}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Storj", "Duplicati\Library\Backend\Storj\Duplicati.Library.Backend.Storj.csproj", "{AE035E01-C917-4F13-A35E-78F21C1A2F17}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.AutoUpdater", "Duplicati\CommandLine\AutoUpdater\Duplicati.CommandLine.AutoUpdater.csproj", "{4D1B198B-F773-44F2-872B-2B283053323C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Storj", "Duplicati\Library\Backend\Storj\Duplicati.Library.Backend.Storj.csproj", "{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.TencentCOS", "Duplicati\Library\Backend\TencentCOS\Duplicati.Library.Backend.TencentCOS.csproj", "{545DD6D4-9476-42D6-B51C-A28E000C489E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Tardigrade", "Duplicati\Library\Backend\Tardigrade\Duplicati.Library.Backend.Tardigrade.csproj", "{9A04CB37-DA72-4008-9703-3AC5191974E9}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Executables", "Executables", "{FA88A246-EF8E-46E3-90AF-539B8C0A6ADE}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "net8", "net8", "{6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.AutoUpdater", "Executables\net8\Duplicati.CommandLine.AutoUpdater\Duplicati.CommandLine.AutoUpdater.csproj", "{95B7DD83-2C5A-4F1E-8EA7-39654B2B236A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Service", "Executables\net8\Duplicati_Service\Duplicati.Service.csproj", "{34149709-F3ED-4FB5-A087-43EB195C948B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.BackendTester", "Executables\net8\Duplicati.CommandLine.BackendTester\Duplicati.CommandLine.BackendTester.csproj", "{2F1C0C8D-5C15-4BC0-811F-87F2C98D9790}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.BackendTool", "Executables\net8\Duplicati.CommandLine.BackendTool\Duplicati.CommandLine.BackendTool.csproj", "{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.ConfigurationImporter", "Executables\net8\Duplicati.CommandLine.ConfigurationImporter\Duplicati.CommandLine.ConfigurationImporter.csproj", "{156B9B77-C2B7-4169-8FE7-1171D221446C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.RecoveryTool", "Executables\net8\Duplicati.CommandLine.RecoveryTool\Duplicati.CommandLine.RecoveryTool.csproj", "{0FFC557E-1B84-46A2-B6E8-06064FA7EA58}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.GUI.TrayIcon", "Executables\net8\Duplicati.GUI.TrayIcon\Duplicati.GUI.TrayIcon.csproj", "{AF32C621-30DC-40F5-8CE6-DD69053068E9}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Server", "Executables\net8\Duplicati.Server\Duplicati.Server.csproj", "{55EEEBD2-CE45-45D6-9838-958F1C7354E4}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsService", "Duplicati\WindowsService\WindowsService.csproj", "{8A651E2D-A7C8-4EC7-B421-7A457654811D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.WindowsService", "Executables\net8\Duplicati.WindowsService\Duplicati.WindowsService.csproj", "{5D20B150-C445-47BE-8CE8-C9F74F19A4F2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine", "Executables\net8\Duplicati.CommandLine\Duplicati.CommandLine.csproj", "{0F5A1F4E-25FA-4D02-920D-CA2138498081}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Idrivee2", "Duplicati\Library\Backend\Idrivee2\Duplicati.Library.Backend.Idrivee2.csproj", "{6B594D23-B629-465C-B799-70EE9E56C218}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{C03F6DFD-805A-4BE0-9338-64870ADDB4A2} = {C03F6DFD-805A-4BE0-9338-64870ADDB4A2}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Backends", "Backends", "{E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Helper Libraries", "Helper Libraries", "{566EBBDA-19A4-4056-A615-D901D57D2439}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Implementation", "Implementation", "{D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backends", "Duplicati\Library\Backends\Duplicati.Library.Backends.csproj", "{5290E237-C2CD-48F2-99D2-817F9C2163C8}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.WebserverCore", "Duplicati\WebserverCore\Duplicati.WebserverCore.csproj", "{5A702CEE-DB36-4153-BD94-D8CF867E75A9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.RestAPI", "Duplicati.Library.RestAPI\Duplicati.Library.RestAPI.csproj", "{C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.AliyunOSS", "Duplicati\Library\Backend\AliyunOSS\Duplicati.Library.Backend.AliyunOSS.csproj", "{4EB3DABC-D412-4C12-8876-41A1427A389E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -265,10 +306,6 @@ Global
|
||||
{59C8BBC5-6E42-46FB-AB3E-6C183A82459A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{59C8BBC5-6E42-46FB-AB3E-6C183A82459A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{59C8BBC5-6E42-46FB-AB3E-6C183A82459A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BEF7AF9A-3978-4F90-8592-198BF6EA6C6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BEF7AF9A-3978-4F90-8592-198BF6EA6C6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BEF7AF9A-3978-4F90-8592-198BF6EA6C6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BEF7AF9A-3978-4F90-8592-198BF6EA6C6B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3F47CB0C-CEE1-447F-9C26-7F8E419E3E3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3F47CB0C-CEE1-447F-9C26-7F8E419E3E3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3F47CB0C-CEE1-447F-9C26-7F8E419E3E3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -305,26 +342,132 @@ Global
|
||||
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2CF2D90E-C25B-47AD-91E0-98451BAB8058}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AE035E01-C917-4F13-A35E-78F21C1A2F17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AE035E01-C917-4F13-A35E-78F21C1A2F17}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AE035E01-C917-4F13-A35E-78F21C1A2F17}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AE035E01-C917-4F13-A35E-78F21C1A2F17}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4D1B198B-F773-44F2-872B-2B283053323C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4D1B198B-F773-44F2-872B-2B283053323C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4D1B198B-F773-44F2-872B-2B283053323C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4D1B198B-F773-44F2-872B-2B283053323C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{545DD6D4-9476-42D6-B51C-A28E000C489E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{545DD6D4-9476-42D6-B51C-A28E000C489E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{545DD6D4-9476-42D6-B51C-A28E000C489E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{545DD6D4-9476-42D6-B51C-A28E000C489E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9A04CB37-DA72-4008-9703-3AC5191974E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9A04CB37-DA72-4008-9703-3AC5191974E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9A04CB37-DA72-4008-9703-3AC5191974E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9A04CB37-DA72-4008-9703-3AC5191974E9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{95B7DD83-2C5A-4F1E-8EA7-39654B2B236A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{95B7DD83-2C5A-4F1E-8EA7-39654B2B236A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{95B7DD83-2C5A-4F1E-8EA7-39654B2B236A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{95B7DD83-2C5A-4F1E-8EA7-39654B2B236A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{34149709-F3ED-4FB5-A087-43EB195C948B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{34149709-F3ED-4FB5-A087-43EB195C948B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{34149709-F3ED-4FB5-A087-43EB195C948B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{34149709-F3ED-4FB5-A087-43EB195C948B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2F1C0C8D-5C15-4BC0-811F-87F2C98D9790}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2F1C0C8D-5C15-4BC0-811F-87F2C98D9790}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2F1C0C8D-5C15-4BC0-811F-87F2C98D9790}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2F1C0C8D-5C15-4BC0-811F-87F2C98D9790}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{156B9B77-C2B7-4169-8FE7-1171D221446C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{156B9B77-C2B7-4169-8FE7-1171D221446C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{156B9B77-C2B7-4169-8FE7-1171D221446C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{156B9B77-C2B7-4169-8FE7-1171D221446C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0FFC557E-1B84-46A2-B6E8-06064FA7EA58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0FFC557E-1B84-46A2-B6E8-06064FA7EA58}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0FFC557E-1B84-46A2-B6E8-06064FA7EA58}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0FFC557E-1B84-46A2-B6E8-06064FA7EA58}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AF32C621-30DC-40F5-8CE6-DD69053068E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AF32C621-30DC-40F5-8CE6-DD69053068E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AF32C621-30DC-40F5-8CE6-DD69053068E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AF32C621-30DC-40F5-8CE6-DD69053068E9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{55EEEBD2-CE45-45D6-9838-958F1C7354E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{55EEEBD2-CE45-45D6-9838-958F1C7354E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{55EEEBD2-CE45-45D6-9838-958F1C7354E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{55EEEBD2-CE45-45D6-9838-958F1C7354E4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8A651E2D-A7C8-4EC7-B421-7A457654811D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8A651E2D-A7C8-4EC7-B421-7A457654811D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8A651E2D-A7C8-4EC7-B421-7A457654811D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8A651E2D-A7C8-4EC7-B421-7A457654811D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5D20B150-C445-47BE-8CE8-C9F74F19A4F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5D20B150-C445-47BE-8CE8-C9F74F19A4F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5D20B150-C445-47BE-8CE8-C9F74F19A4F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5D20B150-C445-47BE-8CE8-C9F74F19A4F2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0F5A1F4E-25FA-4D02-920D-CA2138498081}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0F5A1F4E-25FA-4D02-920D-CA2138498081}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0F5A1F4E-25FA-4D02-920D-CA2138498081}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0F5A1F4E-25FA-4D02-920D-CA2138498081}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6B594D23-B629-465C-B799-70EE9E56C218}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6B594D23-B629-465C-B799-70EE9E56C218}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6B594D23-B629-465C-B799-70EE9E56C218}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6B594D23-B629-465C-B799-70EE9E56C218}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5290E237-C2CD-48F2-99D2-817F9C2163C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5290E237-C2CD-48F2-99D2-817F9C2163C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5290E237-C2CD-48F2-99D2-817F9C2163C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5290E237-C2CD-48F2-99D2-817F9C2163C8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5A702CEE-DB36-4153-BD94-D8CF867E75A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5A702CEE-DB36-4153-BD94-D8CF867E75A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5A702CEE-DB36-4153-BD94-D8CF867E75A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5A702CEE-DB36-4153-BD94-D8CF867E75A9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C1D4D665-23A3-4216-9CD1-D67AE9AAAA4C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{FC9B7611-836F-4127-8B44-A7C31F506807} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{F61679A9-E5DE-468A-B5A4-05F92D0143D2} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{19ECCE09-B5EB-406C-8C57-BAC66997D469} = {566EBBDA-19A4-4056-A615-D901D57D2439}
|
||||
{C03F6DFD-805A-4BE0-9338-64870ADDB4A2} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{81765A64-3661-4E3E-B850-2F6F87A51F74} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
|
||||
{BAE27510-8B5D-44B2-B33E-372A98908041} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{1BFAE226-8364-4086-825C-BB83F6F3EE4C} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{E7280DCA-7776-4A73-B9B5-41FD77FC8799} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
|
||||
{0CA86ECF-5BEC-4909-B4F6-110A03B30B92} = {566EBBDA-19A4-4056-A615-D901D57D2439}
|
||||
{C0270709-2A40-43B5-8CF1-69581B9FA2A1} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{CCD76347-7DC7-4B42-B7E1-E500E624CAC3} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{FF2BF37C-E502-4C98-BEA0-701671DDFA08} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{19E661D2-C5DA-4F35-B3EE-7586E5734B5F} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
|
||||
{17566860-3D98-4604-AA5B-47661F75609F} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
|
||||
{2AF960C0-357D-4D44-A3D5-8B6E89DB0F11} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
|
||||
{2C838169-B187-4B09-8768-1C24C2521C8D} = {566EBBDA-19A4-4056-A615-D901D57D2439}
|
||||
{E93F3DE2-FF3A-4709-96A3-8190AA14FA25} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
|
||||
{8E4CECFB-0413-4B00-AB93-78D1C3902BD5} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{5489181D-950C-44AF-873C-45EB0A3B6BD2} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{D9E4E686-423C-48EC-A392-404E7C00860C} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{4A010589-76E6-4F05-A5C4-4598D5DF11F8} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
|
||||
{61C43D61-4368-4942-84A3-1EB623F4EF2A} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{6643A5AE-AB38-453F-ADCE-408E35A81A83} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{3FF7DD0B-5284-4BF9-97D9-1E4417FDABB2} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{59C8BBC5-6E42-46FB-AB3E-6C183A82459A} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{3F47CB0C-CEE1-447F-9C26-7F8E419E3E3F} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{B20A7CEE-9C5B-47B9-8B76-BC85ADFE8493} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{2CD5DBC3-3DA6-432D-BA97-F0B8D24501C2} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{32A74526-3E5F-413A-8CB4-1EFDAD4C8B91} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{851A1CB8-3CEB-41B4-956F-34D760D2A8E5} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{B93E3BF0-DAA7-49B7-B07D-0559C5816735} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
|
||||
{4D1B198B-F773-44F2-872B-2B283053323C} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
|
||||
{E9AB8491-BD4C-4E4F-84C3-0BD551CC7489} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{545DD6D4-9476-42D6-B51C-A28E000C489E} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3} = {FA88A246-EF8E-46E3-90AF-539B8C0A6ADE}
|
||||
{95B7DD83-2C5A-4F1E-8EA7-39654B2B236A} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
|
||||
{34149709-F3ED-4FB5-A087-43EB195C948B} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
|
||||
{2F1C0C8D-5C15-4BC0-811F-87F2C98D9790} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
|
||||
{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
|
||||
{156B9B77-C2B7-4169-8FE7-1171D221446C} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
|
||||
{0FFC557E-1B84-46A2-B6E8-06064FA7EA58} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
|
||||
{AF32C621-30DC-40F5-8CE6-DD69053068E9} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
|
||||
{55EEEBD2-CE45-45D6-9838-958F1C7354E4} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
|
||||
{8A651E2D-A7C8-4EC7-B421-7A457654811D} = {566EBBDA-19A4-4056-A615-D901D57D2439}
|
||||
{5D20B150-C445-47BE-8CE8-C9F74F19A4F2} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
|
||||
{0F5A1F4E-25FA-4D02-920D-CA2138498081} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
|
||||
{6B594D23-B629-465C-B799-70EE9E56C218} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
|
||||
{D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B} = {FA88A246-EF8E-46E3-90AF-539B8C0A6ADE}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {8B40BAFE-D862-4397-9495-8F5EAF5CE80C}
|
||||
EndGlobalSection
|
||||
@@ -340,8 +483,4 @@ Global
|
||||
$5.EolMarker = Windows
|
||||
$5.scope = text/plain
|
||||
EndGlobalSection
|
||||
GlobalSection(SubversionScc) = preSolution
|
||||
Svn-Managed = True
|
||||
Manager = AnkhSVN - Subversion Support for Visual Studio
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AssemblyName>Duplicati.CommandLine.AutoUpdater.Implementation</AssemblyName>
|
||||
<RootNamespace>Duplicati.CommandLine.AutoUpdater</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Common\Duplicati.Library.Common.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,44 +1,44 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Description>A backend debugging tool for Duplicati</Description>
|
||||
<AssemblyName>Duplicati.CommandLine.BackendTester.Implementation</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Library\Backend\AzureBlob\Duplicati.Library.Backend.AzureBlob.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Dropbox\Duplicati.Library.Backend.Dropbox.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Idrivee2\Duplicati.Library.Backend.Idrivee2.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Jottacloud\Duplicati.Library.Backend.Jottacloud.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Rclone\Duplicati.Library.Backend.Rclone.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\SharePoint\Duplicati.Library.Backend.SharePoint.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Sia\Duplicati.Library.Backend.Sia.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Storj\Duplicati.Library.Backend.Storj.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Tardigrade\Duplicati.Library.Backend.Tardigrade.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\TencentCOS\Duplicati.Library.Backend.TencentCOS.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj" />
|
||||
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Interface\Duplicati.Library.Interface.csproj" />
|
||||
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj" />
|
||||
<ProjectReference Include="..\..\License\Duplicati.License.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\OneDrive\Duplicati.Library.Backend.OneDrive.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\TahoeLAFS\Duplicati.Library.Backend.TahoeLAFS.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\GoogleServices\Duplicati.Library.Backend.GoogleServices.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\OpenStack\Duplicati.Library.Backend.OpenStack.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\CloudFiles\Duplicati.Library.Backend.CloudFiles.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\File\Duplicati.Library.Backend.File.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\FTP\Duplicati.Library.Backend.FTP.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\S3\Duplicati.Library.Backend.S3.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\SSHv2\Duplicati.Library.Backend.SSHv2.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\WEBDAV\Duplicati.Library.Backend.WEBDAV.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Modules\Builtin\Duplicati.Library.Modules.Builtin.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\OAuthHelper\Duplicati.Library.OAuthHelper.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Localization\Duplicati.Library.Localization.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Logging\Duplicati.Library.Logging.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Backblaze\Duplicati.Library.Backend.Backblaze.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Mega\Duplicati.Library.Backend.Mega.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Box\Duplicati.Library.Backend.Box.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\AlternativeFTP\Duplicati.Library.Backend.AlternativeFTP.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Common\Duplicati.Library.Common.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Compression\Duplicati.Library.Compression.csproj" />
|
||||
<ProjectReference Include="..\..\Library\SQLiteHelper\Duplicati.Library.SQLiteHelper.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backends\Duplicati.Library.Backends.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Interface\Duplicati.Library.Interface.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\License\Duplicati.License.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Modules\Builtin\Duplicati.Library.Modules.Builtin.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Backend\OAuthHelper\Duplicati.Library.OAuthHelper.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Localization\Duplicati.Library.Localization.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Logging\Duplicati.Library.Logging.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Common\Duplicati.Library.Common.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Compression\Duplicati.Library.Compression.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\SQLiteHelper\Duplicati.Library.SQLiteHelper.csproj">
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
// 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) 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;
|
||||
@@ -29,9 +29,8 @@ using System.Threading;
|
||||
|
||||
namespace Duplicati.CommandLine.BackendTester
|
||||
{
|
||||
class Program
|
||||
public class Program
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Used to maintain a reference to initialized system settings.
|
||||
/// </summary>
|
||||
@@ -63,13 +62,7 @@ namespace Duplicati.CommandLine.BackendTester
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
Duplicati.Library.AutoUpdater.UpdaterManager.IgnoreWebrootFolder = true;
|
||||
return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args);
|
||||
}
|
||||
|
||||
public static void RealMain(string[] _args)
|
||||
public static int Main(string[] _args)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -107,7 +100,7 @@ namespace Duplicati.CommandLine.BackendTester
|
||||
foreach (string s in lines)
|
||||
Console.WriteLine(s);
|
||||
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (options.ContainsKey("tempdir") && !string.IsNullOrEmpty(options["tempdir"]))
|
||||
@@ -129,14 +122,17 @@ namespace Duplicati.CommandLine.BackendTester
|
||||
{
|
||||
Console.WriteLine("Starting run no {0}", i);
|
||||
if (!Run(args, options, i == 0))
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
Console.WriteLine("Unittest complete!");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Unittest failed: " + ex);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static bool Run(List<string> args, Dictionary<string, string> options, bool first)
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AssemblyName>Duplicati.CommandLine.BackendTool.Implementation</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Library\Backend\CloudFiles\Duplicati.Library.Backend.CloudFiles.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\AzureBlob\Duplicati.Library.Backend.AzureBlob.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\File\Duplicati.Library.Backend.File.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\FTP\Duplicati.Library.Backend.FTP.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Idrivee2\Duplicati.Library.Backend.Idrivee2.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Jottacloud\Duplicati.Library.Backend.Jottacloud.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Rclone\Duplicati.Library.Backend.Rclone.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\S3\Duplicati.Library.Backend.S3.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\SharePoint\Duplicati.Library.Backend.SharePoint.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Sia\Duplicati.Library.Backend.Sia.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\SSHv2\Duplicati.Library.Backend.SSHv2.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\TahoeLAFS\Duplicati.Library.Backend.TahoeLAFS.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Storj\Duplicati.Library.Backend.Storj.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Tardigrade\Duplicati.Library.Backend.Tardigrade.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\TencentCOS\Duplicati.Library.Backend.TencentCOS.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\WEBDAV\Duplicati.Library.Backend.WEBDAV.csproj" />
|
||||
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Interface\Duplicati.Library.Interface.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj" />
|
||||
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj" />
|
||||
<ProjectReference Include="..\..\License\Duplicati.License.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\OneDrive\Duplicati.Library.Backend.OneDrive.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\GoogleServices\Duplicati.Library.Backend.GoogleServices.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\OpenStack\Duplicati.Library.Backend.OpenStack.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Backblaze\Duplicati.Library.Backend.Backblaze.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Mega\Duplicati.Library.Backend.Mega.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Box\Duplicati.Library.Backend.Box.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\AlternativeFTP\Duplicati.Library.Backend.AlternativeFTP.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backend\Dropbox\Duplicati.Library.Backend.Dropbox.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Backends\Duplicati.Library.Backends.csproj" />
|
||||
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Interface\Duplicati.Library.Interface.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\License\Duplicati.License.csproj">
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
// 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) 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.Interface;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Duplicati.CommandLine.BackendTool
|
||||
@@ -33,13 +34,7 @@ namespace Duplicati.CommandLine.BackendTool
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
Duplicati.Library.AutoUpdater.UpdaterManager.IgnoreWebrootFolder = true;
|
||||
return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args);
|
||||
}
|
||||
|
||||
public static int RealMain(string[] _args)
|
||||
public static int Main(string[] _args)
|
||||
{
|
||||
bool debugoutput = false;
|
||||
try
|
||||
@@ -92,6 +87,26 @@ namespace Duplicati.CommandLine.BackendTool
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
var modules = (from n in Library.DynamicLoader.GenericLoader.Modules
|
||||
where n is Library.Interface.IConnectionModule
|
||||
select n).ToArray();
|
||||
|
||||
var uri = new Library.Utility.Uri(args[1]);
|
||||
var qp = uri.QueryParameters;
|
||||
|
||||
var backendOpts = new Dictionary<string, string>();
|
||||
foreach (var k in qp.Keys.Cast<string>())
|
||||
backendOpts[k] = qp[k];
|
||||
|
||||
foreach (var k in backendOpts.Keys) {
|
||||
options.Remove(k);
|
||||
}
|
||||
|
||||
foreach (var n in modules) {
|
||||
n.Configure(options);
|
||||
n.Configure(backendOpts);
|
||||
}
|
||||
|
||||
using(var backend = Library.DynamicLoader.BackendLoader.GetBackend(args[1], options))
|
||||
{
|
||||
@@ -154,7 +169,6 @@ namespace Duplicati.CommandLine.BackendTool
|
||||
|
||||
throw new Exception("Internal error");
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -24,6 +24,8 @@ using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.Utility;
|
||||
|
||||
@@ -38,8 +40,9 @@ namespace Duplicati.CommandLine
|
||||
private readonly System.Threading.ManualResetEvent m_readyEvent;
|
||||
private readonly System.Threading.ManualResetEvent m_finishEvent;
|
||||
private readonly ConsoleOutput m_output;
|
||||
private System.Threading.Thread m_thread;
|
||||
private readonly TimeSpan m_updateFrequency;
|
||||
private Task m_task;
|
||||
private readonly CancellationTokenSource m_cancellationTokenSource;
|
||||
|
||||
public PeriodicOutput(ConsoleOutput messageSink, TimeSpan updateFrequency)
|
||||
{
|
||||
@@ -47,10 +50,8 @@ namespace Duplicati.CommandLine
|
||||
m_readyEvent = new System.Threading.ManualResetEvent(false);
|
||||
m_finishEvent = new System.Threading.ManualResetEvent(false);
|
||||
m_updateFrequency = updateFrequency;
|
||||
m_thread = new System.Threading.Thread(this.ThreadMain);
|
||||
m_thread.IsBackground = true;
|
||||
m_thread.Name = "Periodic console writer";
|
||||
m_thread.Start();
|
||||
m_cancellationTokenSource = new CancellationTokenSource();
|
||||
m_task = Task.Run(() => this.ThreadMain(m_cancellationTokenSource.Token), m_cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
public void SetReady() { m_readyEvent.Set(); }
|
||||
@@ -58,13 +59,13 @@ namespace Duplicati.CommandLine
|
||||
|
||||
public bool Join(TimeSpan wait)
|
||||
{
|
||||
if (m_thread != null)
|
||||
return m_thread.Join(wait);
|
||||
if (m_task != null)
|
||||
return this.m_task.Wait(wait);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ThreadMain()
|
||||
private void ThreadMain(CancellationToken cancellationToken)
|
||||
{
|
||||
m_readyEvent.WaitOne();
|
||||
if (m_finishEvent.WaitOne(TimeSpan.FromMilliseconds(10), true))
|
||||
@@ -73,7 +74,7 @@ namespace Duplicati.CommandLine
|
||||
var last_count = -1L;
|
||||
var finished = false;
|
||||
|
||||
while (true)
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
float progress;
|
||||
long filesprocessed;
|
||||
@@ -110,22 +111,22 @@ namespace Duplicati.CommandLine
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (m_thread != null)
|
||||
if (m_task != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_finishEvent.Set();
|
||||
m_readyEvent.Set();
|
||||
|
||||
if (m_thread != null && m_thread.IsAlive)
|
||||
if (m_task != null)
|
||||
{
|
||||
m_thread.Abort();
|
||||
m_thread.Join(500);
|
||||
m_cancellationTokenSource.Cancel();
|
||||
m_task.Wait(500);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
m_thread = null;
|
||||
m_task = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -579,15 +580,6 @@ namespace Duplicati.CommandLine
|
||||
output.MessageEvent(string.Format("Restored {0} ({1}) files to {2}", res.RestoredFiles, Library.Utility.Utility.FormatSizeString(res.SizeOfRestoredFiles), string.IsNullOrEmpty(restorePath) ? "original path" : restorePath));
|
||||
output.MessageEvent(string.Format("Duration of restore: {0:hh\\:mm\\:ss}", res.Duration));
|
||||
|
||||
if (res.RestoredFiles > 0 && !Library.Main.Utility.SuppressDonationMessages)
|
||||
{
|
||||
output.MessageEvent("***********************************************");
|
||||
output.MessageEvent("Did we help save your files? If so, please support Duplicati with a donation. We suggest 10€ for private use and 100€ for commercial use.");
|
||||
output.MessageEvent("");
|
||||
output.MessageEvent("https://www.duplicati.com/donate/");
|
||||
output.MessageEvent("***********************************************");
|
||||
}
|
||||
|
||||
if (output.FullResults)
|
||||
Library.Utility.Utility.PrintSerializeObject(res, outwriter);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Description>A commandline version of Duplicati</Description>
|
||||
<AssemblyName>Duplicati.CommandLine.Implementation</AssemblyName>
|
||||
<ApplicationIcon>TrayWarning.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Library\Backends\Duplicati.Library.Backends.csproj" />
|
||||
<ProjectReference Include="..\..\Library\Compression\Duplicati.Library.Compression.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Utility\Duplicati.Library.Utility.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Common\Duplicati.Library.Common.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Encryption\Duplicati.Library.Encryption.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Interface\Duplicati.Library.Interface.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Logging\Duplicati.Library.Logging.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Main\Duplicati.Library.Main.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Modules\Builtin\Duplicati.Library.Modules.Builtin.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\License\Duplicati.License.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\BackendTool\Duplicati.CommandLine.BackendTool.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\BackendTester\Duplicati.CommandLine.BackendTester.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\AutoUpdater\Duplicati.Library.AutoUpdater.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\Localization\Duplicati.Library.Localization.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\RecoveryTool\Duplicati.CommandLine.RecoveryTool.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Library\UsageReporter\Duplicati.Library.UsageReporter.csproj">
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Tools\Duplicati.Tools.csproj">
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="help.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -24,6 +24,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Duplicati.Library.AutoUpdater;
|
||||
using Duplicati.Library.Common;
|
||||
using FilterGroup = Duplicati.Library.Utility.FilterGroup;
|
||||
|
||||
@@ -117,8 +118,7 @@ namespace Duplicati.CommandLine
|
||||
|
||||
tp = tp.Replace("%VERSION%", License.VersionNumbers.Version);
|
||||
tp = tp.Replace("%BACKENDS%", string.Join(", ", Library.DynamicLoader.BackendLoader.Keys));
|
||||
tp = tp.Replace("%MONO%", Library.Utility.Utility.IsMono ? "mono " : "");
|
||||
tp = tp.Replace("%APP_PATH%", System.IO.Path.GetFileName(System.Reflection.Assembly.GetExecutingAssembly().Location));
|
||||
tp = tp.Replace("%APP_PATH%", Path.Combine(UpdaterManager.INSTALLATIONDIR, PackageHelper.GetExecutableName(PackageHelper.NamedExecutable.CommandLine)));
|
||||
tp = tp.Replace("%PATH_SEPARATOR%", System.IO.Path.PathSeparator.ToString());
|
||||
tp = tp.Replace("%EXAMPLE_SOURCE_PATH%", Platform.IsClientPosix ? "/source" : @"D:\source");
|
||||
tp = tp.Replace("%EXAMPLE_SOURCE_FILE%", Platform.IsClientPosix ? "/source/myfile.txt" : @"D:\source\file.txt");
|
||||
@@ -1,23 +1,23 @@
|
||||
// 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) 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;
|
||||
@@ -25,7 +25,7 @@ using System.Linq;
|
||||
using Duplicati.Library.Localization.Short;
|
||||
using System.IO;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Library.Logging;
|
||||
using Duplicati.Library.AutoUpdater;
|
||||
|
||||
namespace Duplicati.CommandLine
|
||||
{
|
||||
@@ -38,14 +38,7 @@ namespace Duplicati.CommandLine
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
Duplicati.Library.AutoUpdater.UpdaterManager.IgnoreWebrootFolder = true;
|
||||
return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args);
|
||||
}
|
||||
|
||||
public static int RealMain(string[] args)
|
||||
{
|
||||
Library.UsageReporter.Reporter.Initialize();
|
||||
FROM_COMMANDLINE = true;
|
||||
@@ -101,7 +94,7 @@ namespace Duplicati.CommandLine
|
||||
|
||||
private static int ShowChangeLog(TextWriter outwriter)
|
||||
{
|
||||
var path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "changelog.txt");
|
||||
var path = System.IO.Path.Combine(UpdaterManager.INSTALLATIONDIR, "changelog.txt");
|
||||
outwriter.WriteLine(System.IO.File.ReadAllText(path));
|
||||
return 0;
|
||||
}
|
||||
@@ -114,18 +107,28 @@ namespace Duplicati.CommandLine
|
||||
|
||||
if (update != null && update.Version != Library.AutoUpdater.UpdaterManager.SelfVersion.Version)
|
||||
{
|
||||
outwriter.WriteLine("Found update \"{0}\", downloading ...", update.Displayname);
|
||||
long lastpg = 0;
|
||||
Library.AutoUpdater.UpdaterManager.DownloadAndUnpackUpdate(update, f =>
|
||||
var package = update.FindPackage();
|
||||
if (package == null)
|
||||
{
|
||||
var npg = (long)(f * 100);
|
||||
if (Math.Abs(npg - lastpg) >= 5 || (npg == 100 && lastpg != 100))
|
||||
outwriter.WriteLine($"Failed to locate a matching package for this machine, please visit this link and select the correct package: {update.GetGenericUpdatePageUrl()}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var filename = Path.GetFullPath(package.GetFilename());
|
||||
outwriter.WriteLine("Downloading update \"{0}\" to {1} ...", update.Displayname, filename);
|
||||
|
||||
long lastpg = 0;
|
||||
Library.AutoUpdater.UpdaterManager.DownloadUpdate(update, package, filename, f =>
|
||||
{
|
||||
lastpg = npg;
|
||||
outwriter.WriteLine("Downloading {0}% ...", npg);
|
||||
}
|
||||
});
|
||||
outwriter.WriteLine("Update \"{0}\" ({1}) installed, using on next launch", update.Displayname, update.Version);
|
||||
var npg = (long)(f * 100);
|
||||
if (Math.Abs(npg - lastpg) >= 5 || (npg == 100 && lastpg != 100))
|
||||
{
|
||||
lastpg = npg;
|
||||
outwriter.WriteLine("Downloading {0}% ...", npg);
|
||||
}
|
||||
});
|
||||
outwriter.WriteLine("Update \"{0}\" ({1}) downloaded", update.Displayname, update.Version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user