diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index e1f6a2ba4..000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,66 +0,0 @@ -version: 1.0.{build} -image: Visual Studio 2019 -configuration: Debug -platform: Any CPU -environment: - AUTOUPDATER_Duplicati_SKIP_UPDATE: 1 - COVERALLS_REPO_TOKEN: - secure: szQsrkP5rvra8L60SomD73/dFvRwot0UuyA566zqzI2qmLOr+MhLN0AyC8BTbhYY -before_build: - - ps: $env:UNITTEST_BASEFOLDER = "$Env:APPVEYOR_BUILD_FOLDER" + "\testdata" - - cmd: nuget restore Duplicati.sln -build: - project: Duplicati.sln - verbosity: minimal -test_script: - - ps: | - nuget install OpenCover -Version 4.7.922 -OutputDirectory packages - nuget install coveralls.net -Version 0.7.0 -OutputDirectory packages - nuget install NUnit.Runners -Version 3.10.0 -OutputDirectory packages - - $testDir = ".\Duplicati\UnitTest\bin\Debug" - $tests = @("$testDir\Duplicati.UnitTest.dll") - $testsPassed = $true - - foreach ($elem in $tests) { - $testResultsFile = "$elem" + ".xml" - - .\packages\OpenCover.4.7.922\tools\OpenCover.Console.exe ` - -register:user ` - -target:.\packages\NUnit.ConsoleRunner.3.10.0\tools\nunit3-console.exe ` - "-targetargs:""$elem"" /result:""$testResultsFile""" ` - "-filter:+[Duplicati.Library.Main]* -[UnitTest]*" ` - -output:opencover.xml ` - -returntargetcode ` - - # Keep track of test failures so that we can throw an exception after - # uploading coverage and test results. - $testsPassed = $testsPassed -and $? - - # Upload test results to AppVeyor. - $wc = New-Object "System.Net.WebClient" - $wc.UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\""$testResultsFile"")) - } - - if ($Env:COVERALLS_REPO_TOKEN) { - $revision = git rev-parse HEAD - $branch = git rev-parse --abbrev-ref HEAD - $commitAuthor = git show --quiet --format="%aN" $revision - $commitEmail = git show --quiet --format="%aE" $revision - $commitMessage = git show --quiet --format="%s" $revision - - .\packages\coveralls.net.0.7.0\tools\csmacnz.Coveralls.exe ` - --opencover -i opencover.xml ` - --repoToken "$Env:COVERALLS_REPO_TOKEN" ` - --commitId "$revision" ` - --commitBranch "$branch" ` - --commitAuthor "$commitAuthor" ` - --commitEmail "$commitEmail" ` - --commitMessage "$commitMessage" ` - --useRelativePaths ` - --basePath .\Duplicati\UnitTest\bin\Debug - } - - if (!$testsPassed) { - throw "Tests failed." - } diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..33fcf131b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +*.csproj text +*.sh text eol=lf +#The #! in rules won't work if CRLF +/Installer/debian/docker/debian/* text eol=lf +/Installer/fedora/*.sh text eol=lf diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml new file mode 100644 index 000000000..cdfce5be6 --- /dev/null +++ b/.github/workflows/artifacts.yml @@ -0,0 +1,105 @@ +name: Artifacts + +on: + push: + branches: "**net5**" + pull_request: + branches: "**net5**" + +jobs: + zip: + runs-on: ${{ matrix.cfg.os }} + strategy: + fail-fast: false + matrix: + cfg: + - { os: ubuntu-latest, runtime: linux-x64 } + - { os: windows-latest, runtime: win-x64 } + - { os: macos-latest, runtime: osx-x64 } + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + - name: Restore dependencies + run: dotnet restore --runtime=${{ matrix.cfg.runtime }} Duplicati.sln + - name: Package + #TODO: investigate #if __WindowsGTK__ || ENABLE_GTK via -p:DefineConstants=ENABLE_GTK Maybe needed for GUI? + run: dotnet publish -c Release --runtime=${{ matrix.cfg.runtime }} -o publish Duplicati.sln + - name: Save Artifacts + uses: actions/upload-artifact@v2 + with: + name: duplicati-${{ matrix.cfg.runtime }} + path: publish + deb: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Package + run: Installer/debian/build.sh + - name: Save Artifacts + uses: actions/upload-artifact@v2 + with: + name: deb package + path: duplicati*.deb + rpm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Package + run: Installer/fedora/build.sh + - name: Save Artifacts + uses: actions/upload-artifact@v2 + with: + name: rpm package + path: duplicati*.rpm + msi: + runs-on: windows-latest + needs: zip + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Download zip + uses: actions/download-artifact@v2 + with: + name: duplicati-win-x64 + path: bin + - name: Install go-msi + run: choco install -y "go-msi" + - name: Prepare PATH + shell: bash + run: | + echo "$WIX\\bin" >> $GITHUB_PATH + echo "C:\\Program Files\\go-msi" >> $GITHUB_PATH + - name: Build MSI + id: buildmsi + shell: bash + run: | + Installer/Windows/MSI/build.sh + - name: Save Artifacts + uses: actions/upload-artifact@v2 + with: + name: duplicati-win-x64-msi + path: duplicati-win-x64.msi + dmg: + runs-on: macos-latest + needs: zip + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Download zip + uses: actions/download-artifact@v2 + with: + name: duplicati-osx-x64 + path: bin + - name: Build DMG + shell: bash + run: | + Installer/OSX/dmg/build.sh + - name: Save Artifacts + uses: actions/upload-artifact@v2 + with: + name: Duplicati.dmg + path: Duplicati.dmg diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml new file mode 100644 index 000000000..977a0a9ea --- /dev/null +++ b/.github/workflows/freebsd.yml @@ -0,0 +1,143 @@ +name: Freebsd +on: + push: + branches: "**freebsd**" + pull_request: + branches: "**freebsd**" + +jobs: + zip: + runs-on: macos-12 + steps: + - uses: actions/checkout@v2 + - name: Download dotnet + # See also https://github.com/sec/dotnet-core-freebsd-source-build + # See also https://github.com/Thefrank/dotnet-freebsd-crossbuild + run: curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/v6.0.401-11/dotnet-sdk-6.0.401-freebsd-x64.tar.gz -o dn.tgz + - name: Build + id: test + uses: vmactions/freebsd-vm@v0 + with: + usesh: true + # sync: sshfs + prepare: pkg install -y curl libinotify libunwind git cmake ninja bash wget icu + run: | + #!/bin/bash + set -x + set -e + pwd + ls -lah + whoami + env + freebsd-version + df -h + dmesg | grep memory + swapinfo + dd if=/dev/zero of=/root/swap1 bs=1m count=20480 + chmod 0600 /root/swap1 + mdconfig -a -t vnode -f /root/swap1 -u 0 + swapon /dev/md0 + df -h + swapinfo + + export DOTNET_CLI_TELEMETRY_OPTOUT=1 + BUILD_DIR=`pwd` + cd /var/tmp + mkdir dn + cd dn + echo Unpacking .NET + tar -xzf ${BUILD_DIR}/dn.tgz + cd ${BUILD_DIR} + mkdir /var/tmp/nuget + export RVERSION=v6.0.401-11 + export VERSION=6.0.9 + curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.AspNetCore.App.Runtime.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.AspNetCore.App.Runtime.freebsd-x64.${VERSION}.nupkg + curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.NETCore.App.Host.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.NETCore.App.Host.freebsd-x64.${VERSION}.nupkg + curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.NETCore.App.Runtime.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.NETCore.App.Runtime.freebsd-x64.${VERSION}.nupkg + curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.NETCore.App.Crossgen2.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.NETCore.App.Crossgen2.freebsd-x64.${VERSION}.nupkg + /var/tmp/dn/dotnet nuget add source /var/tmp/nuget/ + + cp -R `pwd` /var/tmp/build + cd /var/tmp/build + + echo Restore + /var/tmp/dn/dotnet restore --runtime=freebsd-x64 Duplicati.sln + echo Build + /var/tmp/dn/dotnet build Duplicati.sln + echo Publish + /var/tmp/dn/dotnet publish -c Release --runtime=freebsd-x64 -o publish Duplicati.sln + + cp -R ./publish ${BUILD_DIR}/publish + - name: Save Artifacts + uses: actions/upload-artifact@v2 + with: + name: duplicati-freebsd-x64 + path: publish + tests: + runs-on: macos-12 + steps: + - uses: actions/checkout@v2 + - name: Download dotnet + # See also https://github.com/sec/dotnet-core-freebsd-source-build + # See also https://github.com/Thefrank/dotnet-freebsd-crossbuild + run: curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/v6.0.401-11/dotnet-sdk-6.0.401-freebsd-x64.tar.gz -o dn.tgz + - name: Run test + id: test + uses: vmactions/freebsd-vm@v0 + with: + usesh: true + # sync: sshfs + prepare: pkg install -y curl libinotify libunwind git cmake ninja bash wget icu + run: | + #!/bin/bash + set -x + set -e + pwd + ls -lah + whoami + env + freebsd-version + df -h + dmesg | grep memory + swapinfo + dd if=/dev/zero of=/root/swap1 bs=1m count=20480 + chmod 0600 /root/swap1 + mdconfig -a -t vnode -f /root/swap1 -u 0 + swapon /dev/md0 + df -h + swapinfo + + export DOTNET_CLI_TELEMETRY_OPTOUT=1 + BUILD_DIR=`pwd` + cd /var/tmp + mkdir dn + cd dn + echo Unpacking .NET + tar -xzf ${BUILD_DIR}/dn.tgz + cd ${BUILD_DIR} + mkdir /var/tmp/nuget + export RVERSION=v6.0.401-11 + export VERSION=6.0.9 + curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.AspNetCore.App.Runtime.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.AspNetCore.App.Runtime.freebsd-x64.${VERSION}.nupkg + curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.NETCore.App.Host.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.NETCore.App.Host.freebsd-x64.${VERSION}.nupkg + curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.NETCore.App.Runtime.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.NETCore.App.Runtime.freebsd-x64.${VERSION}.nupkg + curl -L https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download/${RVERSION}/Microsoft.NETCore.App.Crossgen2.freebsd-x64.${VERSION}.nupkg -o /var/tmp/nuget/Microsoft.NETCore.App.Crossgen2.freebsd-x64.${VERSION}.nupkg + /var/tmp/dn/dotnet nuget add source /var/tmp/nuget/ + + cp -R `pwd` /var/tmp/build + cd /var/tmp/build + + echo Restore + /var/tmp/dn/dotnet restore --runtime=freebsd-x64 Duplicati.sln + echo Testing + mkdir -p ${BUILD_DIR}/duplicati_testdata/logs + mkdir -p /var/tmp/duplicati_testdata + ln -s ${BUILD_DIR}/duplicati_testdata/logs /var/tmp/duplicati_testdata/logs + export UNITTEST_BASEFOLDER=/var/tmp/duplicati_testdata + /var/tmp/dn/dotnet test --runtime=freebsd-x64 --verbosity minimal Duplicati.sln + - name: Archive Test Logs + if: always() + uses: actions/upload-artifact@v2 + with: + name: test-logs + path: duplicati_testdata/logs/* diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9e5d0c0b1..77ce60a93 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,9 +1,10 @@ name: Tests -on: [push, pull_request] - -env: - build_directory: "Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release" +on: + push: + branches: "**net5**" + pull_request: + branches: "**net5**" jobs: unit_tests: @@ -15,81 +16,27 @@ jobs: 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: 6.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 Packages - run: nuget restore Duplicati.sln - - - name: Install Additional Dependencies - if: matrix.os == 'ubuntu-latest' - run: sudo apt-get install gtk-sharp2 - - - 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 - 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: Restore NuGet dependencies + run: dotnet restore Duplicati.sln - name: Build Duplicati - run: msbuild -p:Configuration=Release -p:DefineConstants=ENABLE_GTK Duplicati.sln + run: dotnet build --no-restore Duplicati.sln - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.x + - name: Run unit tests + run: dotnet test --no-build --verbosity minimal Duplicati.sln - - 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 + selenium: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Selenium + run: pipeline/selenium/test.sh + diff --git a/.gitignore b/.gitignore index 7ba91052e..359f8d818 100644 --- a/.gitignore +++ b/.gitignore @@ -23,5 +23,8 @@ changelog-news.txt binfiles.wxs .idea/ .vs/ +/publish/ Installer/debian/*.buildinfo +/*.deb +/*.rpm \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 225209504..000000000 --- a/.travis.yml +++ /dev/null @@ -1,83 +0,0 @@ -language: minimal -services: docker -before_install: - - sudo apt-get install moreutils - - set -o pipefail -addons: - coverity_scan: - project: - name: "duplicati/duplicati" - description: "Duplicati Build submitted via Travis CI" - notification_email: kenneth@duplicati.com - build_command_prepend: "msbuild /t:Clean" - build_command: "msbuild /t:Rebuild" - branch_pattern: coverity_scan -env: - global: - # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created - # via the "travis encrypt" command using the project repo's public key - - secure: "ijaQDcVcMo/utuP514FiDZVGR5LJbATWmYDmgRIIaI0YG2Rk6uuJp9XXDXr8sx66JWttPGgs7Htqfy50VKHdr7nsfyCQCRNGmqF+OJeUgAQMj5spxn9A7PZY9+GVhnXoJ06wvYjCv5MPeLBMDK/KpIM5Ny5IG+TKzQy4U7Dyg0riMUT61aqzxSjTGfHZPmaqBICJTzsRmeGHglemFUz/1B4NAN+IeAtXoH9YIfA2LjJfYmMLckPgFQLKN/tQRaGPWXh77y0qVgA++K8UVu5W3yGo3JXBPDdabGgKs9BPlfN5/Ixnkcy6ekAubDA/Aspur9x7/szj4/AnGMY6frNB33nQcnJQZ/51GYKr6CXCmjoJQuWc8ZKdEMqDn3w/kyEzvluSjAsJNRVboeWPE4VfpAeUBsjQ+TuEQi8WXhN8VJgZV7yCkVlZTVbhtOxMPsBiFb74lpWC2iQuWLJv9ED6vyXZVzZhNtfjXNXNZzIwYxbcd7I0ZQ8fmDstFnvBJveJby2ZRpECXTbc03eIUq1SAaVJJY2r47RD1PwVqM3xvUTljIqtKdyIz915mEOjqG1BnCDaiGHYJ4byuDDeIUZO9X70s60Gh4hupZA2Ruup0v7SfYgkto6aNcStd0NYd716Vd7hqXNTp6nu01L5YYJDY4h5sHyBXVXbxCKlUb01kUw=" - - USAGEREPORTER_Duplicati_LEVEL=none - - AUTOUPDATER_Duplicati_SKIP_UPDATE=1 - - BUILD_DIR=${TRAVIS_BUILD_DIR}/../.build - - TEST_DIR=${TRAVIS_BUILD_DIR}/../.test - - ROOT_DIR="$TRAVIS_BUILD_DIR" - -# matrix expansion within jobs would be good: -# https://github.com/travis-ci/travis-ci/issues/8295 - -# There is one cache per branch, so concurrent builds on different branches possible! -# DO NOT SET ENVIRONMENT VARIABLES WITHIN JOBS, OR CACHES WILL NOT BE SHARED AMONGST JOBS! - - -jobs: - include: - - stage: build - cache: - directories: - - $BUILD_DIR - script: - - ${ROOT_DIR}/pipeline/jobs/build_job.sh - - - - stage: tests - cache: - directories: - - $BUILD_DIR - script: - - ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories BulkNormal - - stage: tests - cache: - directories: - - $BUILD_DIR - script: - - ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories BulkNoSize - - stage: tests - cache: - directories: - - $BUILD_DIR - script: - - ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories SVNDataLong,SVNData,RecoveryTool,ProblematicPath,SymLink - - stage: tests - cache: - directories: - - $BUILD_DIR - script: - - travis_wait ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Border - - stage: tests - cache: - directories: - - $BUILD_DIR - script: - - ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Filter,Targeted,Purge,Serialization,WebApi,Utility,UriUtility,IO,ImportExport,Disruption,RestoreHandler,RepairHandler,DeleteHandler,Controller - - - stage: tests - cache: - directories: - - $BUILD_DIR - script: - - ${ROOT_DIR}/pipeline/jobs/selenium_job.sh - addons: - sauce_connect: - username: $SAUCE_USERNAME - access_key: $SAUCE_ACCESS_KEY diff --git a/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.csproj b/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.csproj index 5768330ff..86143c38b 100644 --- a/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.csproj +++ b/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.csproj @@ -1,4 +1,5 @@  + net8.0 Exe @@ -15,4 +16,12 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/BuildTools/DependencyFinder/DependencyFinder.csproj b/BuildTools/DependencyFinder/DependencyFinder.csproj index efad950d8..ef79e3a56 100644 --- a/BuildTools/DependencyFinder/DependencyFinder.csproj +++ b/BuildTools/DependencyFinder/DependencyFinder.csproj @@ -1,9 +1,19 @@  + net8.0 Exe + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/BuildTools/DependencyFinder/DependencyFinder.sln b/BuildTools/DependencyFinder/DependencyFinder.sln index cc68d58a9..85448817a 100644 --- a/BuildTools/DependencyFinder/DependencyFinder.sln +++ b/BuildTools/DependencyFinder/DependencyFinder.sln @@ -1,17 +1,17 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2012 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyFinder", "DependencyFinder.csproj", "{3E891818-579A-4C06-8066-4C702BFA5921}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x86 = Debug|x86 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {3E891818-579A-4C06-8066-4C702BFA5921}.Debug|x86.ActiveCfg = Debug|x86 - {3E891818-579A-4C06-8066-4C702BFA5921}.Debug|x86.Build.0 = Debug|x86 - {3E891818-579A-4C06-8066-4C702BFA5921}.Release|x86.ActiveCfg = Release|x86 - {3E891818-579A-4C06-8066-4C702BFA5921}.Release|x86.Build.0 = Release|x86 - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyFinder", "DependencyFinder.csproj", "{3E891818-579A-4C06-8066-4C702BFA5921}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3E891818-579A-4C06-8066-4C702BFA5921}.Debug|x86.ActiveCfg = Debug|x86 + {3E891818-579A-4C06-8066-4C702BFA5921}.Debug|x86.Build.0 = Debug|x86 + {3E891818-579A-4C06-8066-4C702BFA5921}.Release|x86.ActiveCfg = Release|x86 + {3E891818-579A-4C06-8066-4C702BFA5921}.Release|x86.Build.0 = Release|x86 + EndGlobalSection +EndGlobal diff --git a/BuildTools/DependencyFinder/Program.cs b/BuildTools/DependencyFinder/Program.cs index a007f71db..a11daf0bd 100644 --- a/BuildTools/DependencyFinder/Program.cs +++ b/BuildTools/DependencyFinder/Program.cs @@ -60,7 +60,7 @@ namespace DependencyFinder //Console.WriteLine("Probing for {0}", dllname); try { return AssemblyDefinition.ReadAssembly(dllname); } catch { } - try { return resolver.Resolve(asm.FullName); } + try { return resolver.Resolve(asm); } catch { } return null; diff --git a/BuildTools/GnupgSigningTool/GnupgSigningTool.csproj b/BuildTools/GnupgSigningTool/GnupgSigningTool.csproj index 6a24a8c18..0e7751830 100644 --- a/BuildTools/GnupgSigningTool/GnupgSigningTool.csproj +++ b/BuildTools/GnupgSigningTool/GnupgSigningTool.csproj @@ -1,11 +1,21 @@  + net8.0 Exe + + - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj b/BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj index b70477ad5..e7bd3a791 100644 --- a/BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj +++ b/BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj @@ -1,10 +1,19 @@  + net8.0 Exe + - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Server/Database/Backup.cs b/Duplicati.Library.RestAPI/Database/Backup.cs similarity index 77% rename from Duplicati/Server/Database/Backup.cs rename to Duplicati.Library.RestAPI/Database/Backup.cs index 33b2671f3..117253942 100644 --- a/Duplicati/Server/Database/Backup.cs +++ b/Duplicati.Library.RestAPI/Database/Backup.cs @@ -1,24 +1,20 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using Duplicati.Server.Serialization.Interface; using System.Collections.Generic; diff --git a/Duplicati/Server/Database/Connection.cs b/Duplicati.Library.RestAPI/Database/Connection.cs similarity index 92% rename from Duplicati/Server/Database/Connection.cs rename to Duplicati.Library.RestAPI/Database/Connection.cs index ff9971ee7..4fd7b1f90 100644 --- a/Duplicati/Server/Database/Connection.cs +++ b/Duplicati.Library.RestAPI/Database/Connection.cs @@ -1,1265 +1,1262 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Linq; -using Duplicati.Server.Serialization.Interface; -using System.Text; - -namespace Duplicati.Server.Database -{ - public class Connection : IDisposable - { - private readonly System.Data.IDbConnection m_connection; - private System.Data.IDbCommand m_errorcmd; - public readonly object m_lock = new object(); - public const int ANY_BACKUP_ID = -1; - public const int SERVER_SETTINGS_ID = -2; - private readonly Dictionary m_temporaryBackups = new Dictionary(); - - public Connection(System.Data.IDbConnection connection) - { - m_connection = connection; - m_errorcmd = m_connection.CreateCommand(); - m_errorcmd.CommandText = @"INSERT INTO ""ErrorLog"" (""BackupID"", ""Message"", ""Exception"", ""Timestamp"") VALUES (?,?,?,?)"; - for(var i = 0; i < 4; i++) - m_errorcmd.Parameters.Add(m_errorcmd.CreateParameter()); - - this.ApplicationSettings = new ServerSettings(this); - } - - internal void LogError(string backupid, string message, Exception ex) - { - lock(m_lock) - { - if (!long.TryParse(backupid, out long id)) - id = -1; - ((System.Data.IDbDataParameter)m_errorcmd.Parameters[0]).Value = id; - ((System.Data.IDbDataParameter)m_errorcmd.Parameters[1]).Value = message; - ((System.Data.IDbDataParameter)m_errorcmd.Parameters[2]).Value = ex?.ToString(); - ((System.Data.IDbDataParameter)m_errorcmd.Parameters[3]).Value = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(DateTime.UtcNow); - m_errorcmd.ExecuteNonQuery(); - } - } - - internal void ExecuteWithCommand(Action f) - { - lock(m_lock) - using(var cmd = m_connection.CreateCommand()) - f(cmd); - } - - internal Serializable.ImportExportStructure PrepareBackupForExport(IBackup backup) - { - var scheduleId = GetScheduleIDsFromTags(new string[] { "ID=" + backup.ID }); - return new Serializable.ImportExportStructure() { - CreatedByVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), - Backup = (Database.Backup)backup, - Schedule = (Database.Schedule)(scheduleId.Any() ? GetSchedule(scheduleId.First()) : null), - DisplayNames = SpecialFolders.GetSourceNames(backup) - }; - } - - public string RegisterTemporaryBackup(IBackup backup) - { - lock(m_lock) - { - if (backup == null) - throw new ArgumentNullException(nameof(backup)); - if (backup.ID != null) - throw new ArgumentException("Backup is already active, cannot make temporary"); - - backup.ID = Guid.NewGuid().ToString("D"); - m_temporaryBackups.Add(backup.ID, (Backup)backup); - return backup.ID; - } - } - - public void UnregisterTemporaryBackup(IBackup backup) - { - lock(m_lock) - m_temporaryBackups.Remove(backup.ID); - } - - public void UpdateTemporaryBackup(IBackup backup) - { - lock(m_lock) - if (m_temporaryBackups.Remove(backup.ID)) - m_temporaryBackups.Add(backup.ID, (Backup)backup); - } - - public IBackup GetTemporaryBackup(string id) - { - if (string.IsNullOrEmpty(id)) - return null; - - lock(m_lock) - { - Backup b; - m_temporaryBackups.TryGetValue(id, out b); - return b; - } - } - - public ServerSettings ApplicationSettings { get; private set; } - - internal IDictionary GetMetadata(long id) - { - lock(m_lock) - return ReadFromDb( - (rd) => new KeyValuePair( - ConvertToString(rd, 0), - ConvertToString(rd, 1) - ), - @"SELECT ""Name"", ""Value"" FROM ""Metadata"" WHERE ""BackupID"" = ? ", id) - .ToDictionary((k) => k.Key, (k) => k.Value); - } - - internal void SetMetadata(IDictionary values, long id, System.Data.IDbTransaction transaction) - { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) - { - OverwriteAndUpdateDb( - tr, - @"DELETE FROM ""Metadata"" WHERE ""BackupID"" = ?", new object[] { id }, - values ?? new Dictionary(), - @"INSERT INTO ""Metadata"" (""BackupID"", ""Name"", ""Value"") VALUES (?, ?, ?)", - (f) => new object[] { id, f.Key, f.Value } - ); - - if (tr != null) - tr.Commit(); - } - } - - internal IFilter[] GetFilters(long id) - { - lock(m_lock) - return ReadFromDb( - (rd) => (IFilter)new Filter() { - Order = ConvertToInt64(rd, 0), - Include = ConvertToBoolean(rd, 1), - Expression = ConvertToString(rd, 2) ?? "" - }, - @"SELECT ""Order"", ""Include"", ""Expression"" FROM ""Filter"" WHERE ""BackupID"" = ? ORDER BY ""Order"" ", id) - .ToArray(); - } - - internal void SetFilters(IEnumerable values, long id, System.Data.IDbTransaction transaction = null) - { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) - { - OverwriteAndUpdateDb( - tr, - @"DELETE FROM ""Filter"" WHERE ""BackupID"" = ?", new object[] { id }, - values, - @"INSERT INTO ""Filter"" (""BackupID"", ""Order"", ""Include"", ""Expression"") VALUES (?, ?, ?, ?)", - (f) => new object[] { id, f.Order, f.Include, f.Expression } - ); - - if (tr != null) - tr.Commit(); - } - } - - public ISetting[] GetSettings(long id) - { - lock(m_lock) - return ReadFromDb( - (rd) => (ISetting)new Setting() { - Filter = ConvertToString(rd, 0) ?? "", - Name = ConvertToString(rd, 1) ?? "", - Value = ConvertToString(rd, 2) ?? "" - //TODO: Attach the argument information - }, - @"SELECT ""Filter"", ""Name"", ""Value"" FROM ""Option"" WHERE ""BackupID"" = ?", id) - .ToArray(); - } - - internal void SetSettings(IEnumerable values, long id, System.Data.IDbTransaction transaction = null) - { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) - { - OverwriteAndUpdateDb( - tr, - @"DELETE FROM ""Option"" WHERE ""BackupID"" = ?", new object[] { id }, - values, - @"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (?, ?, ?, ?)", - (f) => { - if (Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER.Equals(f.Value)) - throw new Exception("Attempted to save a property with the placeholder password"); - return new object[] { id, f.Filter ?? "", f.Name, f.Value ?? "" }; - } - ); - - if (tr != null) - tr.Commit(); - } - } - - internal string[] GetSources(long id) - { - lock(m_lock) - return ReadFromDb( - (rd) => ConvertToString(rd, 0), - @"SELECT ""Path"" FROM ""Source"" WHERE ""BackupID"" = ?", id) - .ToArray(); - } - - internal void SetSources(IEnumerable values, long id, System.Data.IDbTransaction transaction) - { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) - { - OverwriteAndUpdateDb( - tr, - @"DELETE FROM ""Source"" WHERE ""BackupID"" = ?", new object[] { id }, - values, - @"INSERT INTO ""Source"" (""BackupID"", ""Path"") VALUES (?, ?)", - (f) => new object[] { id, f } - ); - - if (tr != null) - tr.Commit(); - } - } - - internal long[] GetBackupIDsForTags(string[] tags) - { - if (tags == null || tags.Length == 0) - return new long[0]; - - if (tags.Length == 1 && tags[0].StartsWith("ID=", StringComparison.Ordinal)) - return new long[] { long.Parse(tags[0].Substring("ID=".Length)) }; - - lock(m_lock) - using(var cmd = m_connection.CreateCommand()) - { - var sb = new StringBuilder(); - - foreach(var t in tags) - { - if (sb.Length != 0) - sb.Append(" OR "); - sb.Append(@" ("","" || ""Tags"" || "","" LIKE ""%,"" || ? || "",%"") "); - - var p = cmd.CreateParameter(); - p.Value = t; - cmd.Parameters.Add(p); - } - - cmd.CommandText = @"SELECT ""ID"" FROM ""Backup"" WHERE " + sb; - - return Read(cmd, (rd) => ConvertToInt64(rd, 0)).ToArray(); - } - } - - internal IBackup GetBackup(string id) - { - if (string.IsNullOrWhiteSpace(id)) - throw new ArgumentNullException(nameof(id)); - - return long.TryParse(id, out long lid) ? GetBackup(lid) : GetTemporaryBackup(id); - } - - internal IBackup GetBackup(long id) - { - lock(m_lock) - { - var bk = ReadFromDb( - (rd) => new Backup { - ID = ConvertToInt64(rd, 0).ToString(), - Name = ConvertToString(rd, 1), - Description = ConvertToString(rd, 2), - Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), - TargetURL = ConvertToString(rd, 4), - DBPath = ConvertToString(rd, 5), - }, - @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" WHERE ID = ?", id) - .FirstOrDefault(); - - if (bk != null) - bk.LoadChildren(this); - - return bk; - } - } - - internal ISchedule GetSchedule(long id) - { - lock(m_lock) - { - var bk = ReadFromDb( - (rd) => new Schedule { - ID = ConvertToInt64(rd, 0), - Tags = (ConvertToString(rd, 1) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), - Time = ConvertToDateTime(rd, 2), - Repeat = ConvertToString(rd, 3), - LastRun = ConvertToDateTime(rd, 4), - Rule = ConvertToString(rd, 5), - }, - @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" WHERE ID = ?", id) - .FirstOrDefault(); - - return bk; - } - } - - internal Boolean IsUnencryptedOrPassphraseStored(long id) - { - lock (m_lock) - { - var usesEncryption = ReadFromDb( - (rd) => ConvertToBoolean(rd, 0), - @"SELECT VALUE != """" FROM ""Option"" WHERE BackupID = ? AND NAME='encryption-module'", id) - .FirstOrDefault(); - - if (!usesEncryption) - { - return true; - } - - return ReadFromDb( - (rd) => ConvertToBoolean(rd, 0), - @"SELECT VALUE != """" FROM ""Option"" WHERE BackupID = ? AND NAME='passphrase'", id) - .FirstOrDefault(); - } - } - - internal long[] GetScheduleIDsFromTags(string[] tags) - { - if (tags == null || tags.Length == 0) - return new long[0]; - - lock(m_lock) - using(var cmd = m_connection.CreateCommand()) - { - var sb = new StringBuilder(); - - foreach(var t in tags) - { - if (sb.Length != 0) - sb.Append(" OR "); - sb.Append(@" ("","" || ""Tags"" || "","" LIKE ""%,"" || ? || "",%"") "); - - var p = cmd.CreateParameter(); - p.Value = t; - cmd.Parameters.Add(p); - } - - cmd.CommandText = @"SELECT ""ID"" FROM ""Schedule"" WHERE " + sb; - - return Read(cmd, (rd) => ConvertToInt64(rd, 0)).ToArray(); - } - } - - public void AddOrUpdateBackupAndSchedule(IBackup item, ISchedule schedule) - { - AddOrUpdateBackup(item, true, schedule); - } - - public string ValidateBackup(IBackup item, ISchedule schedule) - { - if (string.IsNullOrWhiteSpace(item.Name)) - return "Missing a name"; - - if (string.IsNullOrWhiteSpace(item.TargetURL)) - return "Missing a target"; - - if (item.Sources == null || item.Sources.Any(x => string.IsNullOrWhiteSpace(x)) || item.Sources.Length == 0) - return "Invalid source list"; - - var disabled_encryption = false; - var passphrase = string.Empty; - var gpgAsymmetricEncryption = false; - if (item.Settings != null) - { - foreach (var s in item.Settings) - - if (string.Equals(s.Name, "--no-encryption", StringComparison.OrdinalIgnoreCase)) - disabled_encryption = string.IsNullOrWhiteSpace(s.Value) || Library.Utility.Utility.ParseBool(s.Value, false); - else if (string.Equals(s.Name, "passphrase", StringComparison.OrdinalIgnoreCase)) - passphrase = s.Value; - else if (string.Equals(s.Name, "keep-versions", StringComparison.OrdinalIgnoreCase)) - { - int i; - if (!int.TryParse(s.Value, out i) || i <= 0) - return "Retention value must be a positive integer"; - } - else if (string.Equals(s.Name, "keep-time", StringComparison.OrdinalIgnoreCase)) - { - try - { - var ts = Library.Utility.Timeparser.ParseTimeSpan(s.Value); - if (ts <= TimeSpan.FromMinutes(5)) - return "Retention value must be more than 5 minutes"; - } - catch - { - return "Retention value must be a valid timespan"; - } - } - else if (string.Equals(s.Name, "dblock-size", StringComparison.OrdinalIgnoreCase)) - { - try - { - var ds = Library.Utility.Sizeparser.ParseSize(s.Value); - if (ds < 1024 * 1024) - return "DBlock size must be at least 1MB"; - } - catch - { - return "DBlock value must be a valid size string"; - } - } - else if (string.Equals(s.Name, "--blocksize", StringComparison.OrdinalIgnoreCase)) - { - try - { - var ds = Library.Utility.Sizeparser.ParseSize(s.Value); - if (ds < 1024 || ds > int.MaxValue) - return "The blocksize must be at least 1KB"; - } - catch - { - return "The blocksize value must be a valid size string"; - } - } - else if (string.Equals(s.Name, "--prefix", StringComparison.OrdinalIgnoreCase)) - { - if (!string.IsNullOrWhiteSpace(s.Value) && s.Value.Contains("-")) - return "The prefix cannot contain hyphens (-)"; - } - else if (string.Equals(s.Name, "--gpg-encryption-command", StringComparison.OrdinalIgnoreCase)) { - gpgAsymmetricEncryption = string.Equals(s.Value, "--encrypt", StringComparison.OrdinalIgnoreCase); - } - } - - if (!disabled_encryption && !gpgAsymmetricEncryption && string.IsNullOrWhiteSpace(passphrase)) - return "Missing passphrase"; - - if (schedule != null) - { - try - { - var ts = Library.Utility.Timeparser.ParseTimeSpan(schedule.Repeat); - if (ts <= TimeSpan.FromMinutes(5)) - return "Schedule repetition time must be more than 5 minutes"; - } - catch - { - return "Schedule repetition value must be a valid timespan"; - } - - } - - return null; - } - - internal void UpdateBackupDBPath(IBackup item, string path) - { - lock (m_lock) - { - using (var tr = m_connection.BeginTransaction()) - { - using (var cmd = m_connection.CreateCommand()) - { - cmd.Transaction = tr; - cmd.Parameters.Add(cmd.CreateParameter()); - ((System.Data.IDbDataParameter) cmd.Parameters[0]).Value = path; - cmd.Parameters.Add(cmd.CreateParameter()); - ((System.Data.IDbDataParameter) cmd.Parameters[1]).Value = item.ID; - - cmd.CommandText = @"UPDATE ""Backup"" SET ""DBPath""=? WHERE ""ID""=?"; - cmd.ExecuteNonQuery(); - tr.Commit(); - } - } - } - - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - } - - private void AddOrUpdateBackup(IBackup item, bool updateSchedule, ISchedule schedule) - { - lock(m_lock) - { - bool update = item.ID != null; - if (!update && item.DBPath == null) - { - var folder = Program.DataFolder; - if (!System.IO.Directory.Exists(folder)) - System.IO.Directory.CreateDirectory(folder); - - for(var i = 0; i < 100; i++) - { - var guess = System.IO.Path.Combine(folder, System.IO.Path.ChangeExtension(Duplicati.Library.Main.DatabaseLocator.GenerateRandomName(), ".sqlite")); - if (!System.IO.File.Exists(guess)) - { - ((Backup)item).DBPath = guess; - break; - } - } - - if (item.DBPath == null) - throw new Exception("Unable to generate a unique database file name"); - } - - using(var tr = m_connection.BeginTransaction()) - { - OverwriteAndUpdateDb( - tr, - null, - new object[] { long.Parse(item.ID ?? "-1") }, - new IBackup[] { item }, - update ? - @"UPDATE ""Backup"" SET ""Name""=?, ""Description""=?, ""Tags""=?, ""TargetURL""=? WHERE ""ID""=?" : - @"INSERT INTO ""Backup"" (""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"") VALUES (?,?,?,?,?)", - (n) => { - - if (n.TargetURL.IndexOf(Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER, StringComparison.Ordinal) >= 0) - throw new Exception("Attempted to save a backup with the password placeholder"); - if (update && long.Parse(n.ID) <= 0) - throw new Exception("Invalid update, cannot update application settings through update method"); - - return new object[] { - n.Name, - n.Description ?? "" , // Description is optional but the column is set to NOT NULL, an additional check is welcome - string.Join(",", n.Tags ?? new string[0]), - n.TargetURL, - update ? item.ID : n.DBPath - }; - }); - - if (!update) - using(var cmd = m_connection.CreateCommand()) - { - cmd.Transaction = tr; - cmd.CommandText = @"SELECT last_insert_rowid();"; - item.ID = ExecuteScalarInt64(cmd).ToString(); - } - - var id = long.Parse(item.ID); - - if (long.Parse(item.ID) <= 0) - throw new Exception("Invalid addition, cannot update application settings through update method"); - - SetSources(item.Sources, id, tr); - SetSettings(item.Settings, id, tr); - SetFilters(item.Filters, id, tr); - SetMetadata(item.Metadata, id, tr); - - if (updateSchedule) - { - var tags = new string[] { "ID=" + item.ID }; - var existing = GetScheduleIDsFromTags(tags); - if (schedule == null && existing.Any()) - DeleteFromDb("Schedule", existing.First(), tr); - else if (schedule != null) - { - if (existing.Any()) - { - var cur = GetSchedule(existing.First()); - cur.AllowedDays = schedule.AllowedDays; - cur.Repeat = schedule.Repeat; - cur.Tags = schedule.Tags; - cur.Time = schedule.Time; - - schedule = cur; - } - else - { - schedule.ID = -1; - } - - schedule.Tags = tags; - AddOrUpdateSchedule(schedule, tr); - } - } - - tr.Commit(); - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - } - } - } - - internal void AddOrUpdateSchedule(ISchedule item) - { - lock(m_lock) - using(var tr = m_connection.BeginTransaction()) - { - AddOrUpdateSchedule(item, tr); - tr.Commit(); - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - } - } - - private void AddOrUpdateSchedule(ISchedule item, System.Data.IDbTransaction tr) - { - lock(m_lock) - { - bool update = item.ID >= 0; - OverwriteAndUpdateDb( - tr, - null, - new object[] { item.ID }, - new ISchedule[] { item }, - update ? - @"UPDATE ""Schedule"" SET ""Tags""=?, ""Time""=?, ""Repeat""=?, ""LastRun""=?, ""Rule""=? WHERE ""ID""=?" : - @"INSERT INTO ""Schedule"" (""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"") VALUES (?,?,?,?,?)", - (n) => new object[] { - string.Join(",", n.Tags), - Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(n.Time), - n.Repeat, - Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(n.LastRun), - n.Rule ?? "", - update ? (object)item.ID : null - }); - - if (!update) - using(var cmd = m_connection.CreateCommand()) - { - cmd.Transaction = tr; - cmd.CommandText = @"SELECT last_insert_rowid();"; - item.ID = ExecuteScalarInt64(cmd); - } - } - } - - public void DeleteBackup(long ID) - { - if (ID < 0) - return; - - lock(m_lock) - { - using(var tr = m_connection.BeginTransaction()) - { - var existing = GetScheduleIDsFromTags(new string[] { "ID=" + ID.ToString() }); - if (existing.Any()) - DeleteFromDb("Schedule", existing.First(), tr); - - DeleteFromDb("ErrorLog", ID, "BackupID", tr); - DeleteFromDb("Filter", ID, "BackupID", tr); - DeleteFromDb("Log", ID, "BackupID", tr); - DeleteFromDb("Metadata", ID, "BackupID", tr); - DeleteFromDb("Option", ID, "BackupID", tr); - DeleteFromDb("Source", ID, "BackupID", tr); - - DeleteFromDb("Backup", ID, tr); - - tr.Commit(); - } - } - - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - } - - public void DeleteBackup(IBackup backup) - { - if (backup.IsTemporary) - UnregisterTemporaryBackup(backup); - else - DeleteBackup(long.Parse(backup.ID)); - } - - public void DeleteSchedule(long ID) - { - if (ID < 0) - return; - - lock(m_lock) - DeleteFromDb("Schedule", ID); - - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - } - - public void DeleteSchedule(ISchedule schedule) - { - DeleteSchedule(schedule.ID); - } - - public IBackup[] Backups - { - get - { - lock(m_lock) - { - var lst = ReadFromDb( - (rd) => (IBackup)new Backup() { - ID = ConvertToInt64(rd, 0).ToString(), - Name = ConvertToString(rd, 1), - Description = ConvertToString(rd, 2), - Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), - TargetURL = ConvertToString(rd, 4), - DBPath = ConvertToString(rd, 5), - }, - @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" ") - .ToArray(); - - foreach(var n in lst) - n.Metadata = GetMetadata(long.Parse(n.ID)); - - - return lst; - } - } - } - - public ISchedule[] Schedules - { - get - { - lock(m_lock) - return ReadFromDb( - (rd) => (ISchedule)new Schedule() { - ID = ConvertToInt64(rd, 0), - Tags = (ConvertToString(rd, 1) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), - Time = ConvertToDateTime(rd, 2), - Repeat = ConvertToString(rd, 3), - LastRun = ConvertToDateTime(rd, 4), - Rule = ConvertToString(rd, 5), - }, - @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" ") - .ToArray(); - } - } - - - public IFilter[] Filters - { - get { return GetFilters(ANY_BACKUP_ID); } - set { SetFilters(value, ANY_BACKUP_ID); } - } - - public ISetting[] Settings - { - get { return GetSettings(ANY_BACKUP_ID); } - set { SetSettings(value, ANY_BACKUP_ID); } - } - - public INotification[] GetNotifications() - { - lock(m_lock) - return ReadFromDb(null).Cast().ToArray(); - } - - public bool DismissNotification(long id) - { - lock(m_lock) - { - var notifications = GetNotifications(); - var cur = notifications.FirstOrDefault(x => x.ID == id); - if (cur == null) - return false; - - DeleteFromDb(typeof(Notification).Name, id); - Program.DataConnection.ApplicationSettings.UnackedError = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Error); - Program.DataConnection.ApplicationSettings.UnackedWarning = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Warning); - } - - System.Threading.Interlocked.Increment(ref Program.LastNotificationUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - - return true; - } - - public void RegisterNotification(Serialization.NotificationType type, string title, string message, Exception ex, string backupid, string action, string logid, string messageid, string logtag, Func conflicthandler) - { - lock(m_lock) - { - var notification = new Notification() - { - ID = -1, - Type = type, - Title = title, - Message = message, - Exception = ex == null ? "" : ex.ToString(), - BackupID = backupid, - Action = action ?? "", - Timestamp = DateTime.UtcNow, - LogEntryID = logid, - MessageID = messageid, - MessageLogTag = logtag - }; - - var conflictResult = conflicthandler(notification, GetNotifications()); - if (conflictResult == null) - return; - - if (conflictResult != notification) - DeleteFromDb(typeof(Notification).Name, conflictResult.ID); - - OverwriteAndUpdateDb(null, null, null, new Notification[] { notification }, false); - - if (type == Duplicati.Server.Serialization.NotificationType.Error) - Program.DataConnection.ApplicationSettings.UnackedError = true; - else if (type == Duplicati.Server.Serialization.NotificationType.Warning) - Program.DataConnection.ApplicationSettings.UnackedWarning = true; - } - - System.Threading.Interlocked.Increment(ref Program.LastNotificationUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - } - - //Workaround to clean up the database after invalid settings update - public void FixInvalidBackupId() - { - using(var cmd = m_connection.CreateCommand()) - using (var tr = m_connection.BeginTransaction()) - { - cmd.Transaction = tr; - cmd.Parameters.Add(cmd.CreateParameter()); - ((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = -1; - cmd.CommandText = @"DELETE FROM ""Option"" WHERE ""BackupID"" = ?"; - cmd.ExecuteNonQuery(); - cmd.CommandText = @"DELETE FROM ""Metadata"" WHERE ""BackupID"" = ?"; - cmd.ExecuteNonQuery(); - cmd.CommandText = @"DELETE FROM ""Filter"" WHERE ""BackupID"" = ?"; - cmd.ExecuteNonQuery(); - cmd.CommandText = @"DELETE FROM ""Source"" WHERE ""BackupID"" = ?"; - cmd.ExecuteNonQuery(); - - cmd.Parameters.Clear(); - cmd.Parameters.Add(cmd.CreateParameter()); - - ((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = "ID=-1"; - cmd.CommandText = @"DELETE FROM ""Schedule"" WHERE ""Tags"" = ?"; - cmd.ExecuteNonQuery(); - tr.Commit(); - } - - ApplicationSettings.FixedInvalidBackupId = true; - } - - public string[] GetUISettingsSchemes() - { - lock(m_lock) - return ReadFromDb( - (rd) => ConvertToString(rd, 0) ?? "", - @"SELECT DISTINCT ""Scheme"" FROM ""UIStorage""") - .ToArray(); - } - - public IDictionary GetUISettings(string scheme) - { - lock(m_lock) - return ReadFromDb( - (rd) => new KeyValuePair( - ConvertToString(rd, 0) ?? "", - ConvertToString(rd, 1) ?? "" - ), - @"SELECT ""Key"", ""Value"" FROM ""UIStorage"" WHERE ""Scheme"" = ?", - scheme) - .GroupBy(x => x.Key) - .ToDictionary(x => x.Key, x => x.Last().Value); - } - - public void SetUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null) - { - lock(m_lock) - using(var tr = transaction == null ? m_connection.BeginTransaction() : null) - { - OverwriteAndUpdateDb( - tr, - @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = ?", new object[] { scheme }, - values, - @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (?, ?, ?)", - (f) => { - return new object[] { scheme, f.Key ?? "", f.Value ?? "" }; - } - ); - - if (tr != null) - tr.Commit(); - } - } - - public void UpdateUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null) - { - lock (m_lock) - using (var tr = transaction == null ? m_connection.BeginTransaction() : null) - { - OverwriteAndUpdateDb( - tr, - @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = ? AND ""Key"" IN (?)", new object[] { scheme, values.Keys }, - values.Where(x => x.Value != null), - @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (?, ?, ?)", - (f) => - { - return new object[] { scheme, f.Key ?? "", f.Value ?? "" }; - } - ); - - if (tr != null) - tr.Commit(); - } - } - - public TempFile[] GetTempFiles() - { - lock(m_lock) - return ReadFromDb(null).ToArray(); - } - - public void DeleteTempFile(long id) - { - lock(m_lock) - DeleteFromDb(typeof(TempFile).Name, id); - } - - public long RegisterTempFile(string origin, string path, DateTime expires) - { - var tempfile = new TempFile() { - Timestamp = DateTime.Now, - Origin = origin, - Path = path, - Expires = expires - }; - - OverwriteAndUpdateDb(null, null, null, new TempFile[] { tempfile }, false); - - return tempfile.ID; - } - - public void PurgeLogData(DateTime purgeDate) - { - var t = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(purgeDate); - - using(var tr = m_connection.BeginTransaction()) - using(var cmd = m_connection.CreateCommand()) - { - cmd.Transaction = tr; - cmd.CommandText = @"DELETE FROM ""ErrorLog"" WHERE ""Timestamp"" < ?"; - cmd.Parameters.Add(cmd.CreateParameter()); - ((System.Data.IDataParameter)cmd.Parameters[0]).Value = t; - cmd.ExecuteNonQuery(); - - tr.Commit(); - } - } - - private static DateTime ConvertToDateTime(System.Data.IDataReader rd, int index) - { - var unixTime = ConvertToInt64(rd, index); - return unixTime == 0 ? new DateTime(0) : Library.Utility.Utility.EPOCH.AddSeconds(unixTime); - } - - private static bool ConvertToBoolean(System.Data.IDataReader rd, int index) - { - return ConvertToInt64(rd, index) == 1; - } - - private static string ConvertToString(System.Data.IDataReader rd, int index) - { - var r = rd.GetValue(index); - return r == null || r == DBNull.Value ? null : r.ToString(); - } - - private static long ConvertToInt64(System.Data.IDataReader rd, int index) - { - try - { - if (!rd.IsDBNull(index)) - return rd.GetInt64(index); - } - catch - { - } - - return -1; - } - - private static long ExecuteScalarInt64(System.Data.IDbCommand cmd, long defaultValue = -1) - { - using(var rd = cmd.ExecuteReader()) - return rd.Read() ? ConvertToInt64(rd, 0) : defaultValue; - } - - private static string ExecuteScalarString(System.Data.IDbCommand cmd) - { - using(var rd = cmd.ExecuteReader()) - return rd.Read() ? ConvertToString(rd, 0) : null; - - } - - private object ConvertToEnum(Type enumType, System.Data.IDataReader rd, int index, object @default) - { - try - { - return Enum.Parse(enumType, ConvertToString(rd, index)); - } - catch - { - } - - return @default; - } - - // Overloaded function for legacy functionality - private bool DeleteFromDb(string tablename, long id, System.Data.IDbTransaction transaction = null) - { - return DeleteFromDb(tablename, id, "ID", transaction); - } - - // New function that allows to delete rows from tables with arbitrary identifier values (e.g. ID or BackupID) - private bool DeleteFromDb(string tablename, long id, string identifier, System.Data.IDbTransaction transaction = null) - { - if (transaction == null) - { - using(var tr = m_connection.BeginTransaction()) - { - var r = DeleteFromDb(tablename, id, tr); - tr.Commit(); - return r; - } - } - else - { - using(var cmd = m_connection.CreateCommand()) - { - cmd.Transaction = transaction; - cmd.CommandText = string.Format(@"DELETE FROM ""{0}"" WHERE ""{1}""=?", tablename, identifier); - var p = cmd.CreateParameter(); - p.Value = id; - cmd.Parameters.Add(p); - - var r = cmd.ExecuteNonQuery(); - // Roll back the transaction if more than 1 ID was deleted. Multiple "BackupID" rows being deleted isn't a problem. - if (identifier == "ID" && r > 1) - throw new Exception(string.Format("Too many records attempted deleted from table {0} for id {1}: {2}", tablename, id, r)); - return r == 1; - } - } - } - - private static IEnumerable Read(System.Data.IDbCommand cmd, Func f) - { - using(var rd = cmd.ExecuteReader()) - while(rd.Read()) - yield return f(rd); - } - - private static IEnumerable Read(System.Data.IDataReader rd, Func f) - { - while(rd.Read()) - yield return f(); - } - - private System.Reflection.PropertyInfo[] GetORMFields() - { - var flags = - System.Reflection.BindingFlags.FlattenHierarchy | - System.Reflection.BindingFlags.Instance | - System.Reflection.BindingFlags.Public; - - var supportedPropertyTypes = new Type[] { - typeof(long), - typeof(string), - typeof(bool), - typeof(DateTime) - }; - - return - (from n in typeof(T).GetProperties(flags) - where supportedPropertyTypes.Contains(n.PropertyType) || n.PropertyType.IsEnum - select n).ToArray(); - } - - private IEnumerable ReadFromDb(string whereclause, params object[] args) - { - var properties = GetORMFields(); - - var sql = string.Format( - @"SELECT ""{0}"" FROM ""{1}"" {2} {3}", - string.Join(@""", """, properties.Select(x => x.Name)), - typeof(T).Name, - string.IsNullOrWhiteSpace(whereclause) ? "" : " WHERE ", - whereclause ?? "" - ); - - return ReadFromDb((rd) => { - var item = Activator.CreateInstance(); - for(var i = 0; i < properties.Length; i++) - { - var prop = properties[i]; - - if (prop.PropertyType.IsEnum) - prop.SetValue(item, ConvertToEnum(prop.PropertyType, rd, i, Enum.GetValues(prop.PropertyType).GetValue(0)), null); - else if (prop.PropertyType == typeof(string)) - prop.SetValue(item, ConvertToString(rd, i), null); - else if (prop.PropertyType == typeof(long)) - prop.SetValue(item, ConvertToInt64(rd, i), null); - else if (prop.PropertyType == typeof(bool)) - prop.SetValue(item, ConvertToBoolean(rd, i), null); - else if (prop.PropertyType == typeof(DateTime)) - prop.SetValue(item, ConvertToDateTime(rd, i), null); - } - - return item; - }, sql, args); - } - - private void OverwriteAndUpdateDb(System.Data.IDbTransaction transaction, string deleteSql, object[] deleteArgs, IEnumerable values, bool updateExisting) - { - var properties = GetORMFields(); - var idfield = properties.FirstOrDefault(x => x.Name == "ID"); - properties = properties.Where(x => x.Name != "ID").ToArray(); - - string sql; - - if (updateExisting) - { - sql = string.Format( - @"UPDATE ""{0}"" SET {1} WHERE ""ID""=?", - typeof(T).Name, - string.Join(@", ", properties.Select(x => string.Format(@"""{0}""=?", x.Name))) - ); - - properties = properties.Union(new System.Reflection.PropertyInfo[] { idfield }).ToArray(); - } - else - { - - sql = string.Format( - @"INSERT INTO ""{0}"" (""{1}"") VALUES ({2})", - typeof(T).Name, - string.Join(@""", """, properties.Select(x => x.Name)), - string.Join(@", ", properties.Select(x => "?")) - ); - } - - OverwriteAndUpdateDb(transaction, deleteSql, deleteArgs, values, sql, (item) => - { - return properties.Select((x) => - { - var val = x.GetValue(item, null); - - if (x.PropertyType.IsEnum) - val = val.ToString(); - else if (x.PropertyType == typeof(DateTime)) - val = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds((DateTime)val); - - return val; - }).ToArray(); - }); - - if (!updateExisting && values.Count() == 1 && idfield != null) - using(var cmd = m_connection.CreateCommand()) - { - cmd.Transaction = transaction; - cmd.CommandText = @"SELECT last_insert_rowid();"; - if (idfield.PropertyType == typeof(string)) - idfield.SetValue(values.First(), ExecuteScalarString(cmd), null); - else - idfield.SetValue(values.First(), ExecuteScalarInt64(cmd), null); - } - } - - private IEnumerable ReadFromDb(Func f, string sql, params object[] args) - { - using(var cmd = m_connection.CreateCommand()) - { - cmd.CommandText = sql; - if (args != null) - foreach(var a in args) - { - var p = cmd.CreateParameter(); - p.Value = a; - cmd.Parameters.Add(p); - } - - return Read(cmd, f).ToArray(); - } - } - - private void OverwriteAndUpdateDb(System.Data.IDbTransaction transaction, string deleteSql, object[] deleteArgs, IEnumerable values, string insertSql, Func f) - { - using(var cmd = m_connection.CreateCommand()) - { - cmd.Transaction = transaction; - - if (!string.IsNullOrEmpty(deleteSql)) - { - cmd.CommandText = deleteSql; - if (deleteArgs != null) - foreach(var a in deleteArgs) - { - var p = cmd.CreateParameter(); - p.Value = a; - cmd.Parameters.Add(p); - } - - cmd.ExecuteNonQuery(); - cmd.Parameters.Clear(); - } - - cmd.CommandText = insertSql; - - foreach(var n in values) - { - var r = f(n); - if (r == null) - continue; - - while (cmd.Parameters.Count < r.Length) - cmd.Parameters.Add(cmd.CreateParameter()); - - for(var i = 0; i < r.Length; i++) - ((System.Data.IDbDataParameter)cmd.Parameters[i]).Value = r[i]; - - cmd.ExecuteNonQuery(); - } - } - } - - #region IDisposable implementation - public void Dispose() - { - if (m_errorcmd != null) - try { if (m_errorcmd != null) m_errorcmd.Dispose(); } - catch { } - finally { m_errorcmd = null; } - - - try - { - if (m_connection != null) - m_connection.Dispose(); - } - catch - { - } - } - #endregion - } - -} - +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Generic; +using System.Linq; +using Duplicati.Server.Serialization.Interface; +using System.Text; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.Database +{ + public class Connection : IDisposable + { + private readonly System.Data.IDbConnection m_connection; + private System.Data.IDbCommand m_errorcmd; + public readonly object m_lock = new object(); + public const int ANY_BACKUP_ID = -1; + public const int SERVER_SETTINGS_ID = -2; + private readonly Dictionary m_temporaryBackups = new Dictionary(); + + public Connection(System.Data.IDbConnection connection) + { + m_connection = connection; + m_errorcmd = m_connection.CreateCommand(); + m_errorcmd.CommandText = @"INSERT INTO ""ErrorLog"" (""BackupID"", ""Message"", ""Exception"", ""Timestamp"") VALUES (?,?,?,?)"; + for(var i = 0; i < 4; i++) + m_errorcmd.Parameters.Add(m_errorcmd.CreateParameter()); + + this.ApplicationSettings = new ServerSettings(this); + } + + public void LogError(string backupid, string message, Exception ex) + { + lock(m_lock) + { + if (!long.TryParse(backupid, out long id)) + id = -1; + ((System.Data.IDbDataParameter)m_errorcmd.Parameters[0]).Value = id; + ((System.Data.IDbDataParameter)m_errorcmd.Parameters[1]).Value = message; + ((System.Data.IDbDataParameter)m_errorcmd.Parameters[2]).Value = ex?.ToString(); + ((System.Data.IDbDataParameter)m_errorcmd.Parameters[3]).Value = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(DateTime.UtcNow); + m_errorcmd.ExecuteNonQuery(); + } + } + + internal void ExecuteWithCommand(Action f) + { + lock(m_lock) + using(var cmd = m_connection.CreateCommand()) + f(cmd); + } + + internal Serializable.ImportExportStructure PrepareBackupForExport(IBackup backup) + { + var scheduleId = GetScheduleIDsFromTags(new string[] { "ID=" + backup.ID }); + return new Serializable.ImportExportStructure() { + CreatedByVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(), + Backup = (Database.Backup)backup, + Schedule = (Database.Schedule)(scheduleId.Any() ? GetSchedule(scheduleId.First()) : null), + DisplayNames = SpecialFolders.GetSourceNames(backup) + }; + } + + public string RegisterTemporaryBackup(IBackup backup) + { + lock(m_lock) + { + if (backup == null) + throw new ArgumentNullException(nameof(backup)); + if (backup.ID != null) + throw new ArgumentException("Backup is already active, cannot make temporary"); + + backup.ID = Guid.NewGuid().ToString("D"); + m_temporaryBackups.Add(backup.ID, (Backup)backup); + return backup.ID; + } + } + + public void UnregisterTemporaryBackup(IBackup backup) + { + lock(m_lock) + m_temporaryBackups.Remove(backup.ID); + } + + public void UpdateTemporaryBackup(IBackup backup) + { + lock(m_lock) + if (m_temporaryBackups.Remove(backup.ID)) + m_temporaryBackups.Add(backup.ID, (Backup)backup); + } + + public IBackup GetTemporaryBackup(string id) + { + if (string.IsNullOrEmpty(id)) + return null; + + lock(m_lock) + { + Backup b; + m_temporaryBackups.TryGetValue(id, out b); + return b; + } + } + + public ServerSettings ApplicationSettings { get; private set; } + + internal IDictionary GetMetadata(long id) + { + lock(m_lock) + return ReadFromDb( + (rd) => new KeyValuePair( + ConvertToString(rd, 0), + ConvertToString(rd, 1) + ), + @"SELECT ""Name"", ""Value"" FROM ""Metadata"" WHERE ""BackupID"" = ? ", id) + .ToDictionary((k) => k.Key, (k) => k.Value); + } + + internal void SetMetadata(IDictionary values, long id, System.Data.IDbTransaction transaction) + { + lock(m_lock) + using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + { + OverwriteAndUpdateDb( + tr, + @"DELETE FROM ""Metadata"" WHERE ""BackupID"" = ?", new object[] { id }, + values ?? new Dictionary(), + @"INSERT INTO ""Metadata"" (""BackupID"", ""Name"", ""Value"") VALUES (?, ?, ?)", + (f) => new object[] { id, f.Key, f.Value } + ); + + if (tr != null) + tr.Commit(); + } + } + + internal IFilter[] GetFilters(long id) + { + lock(m_lock) + return ReadFromDb( + (rd) => (IFilter)new Filter() { + Order = ConvertToInt64(rd, 0), + Include = ConvertToBoolean(rd, 1), + Expression = ConvertToString(rd, 2) ?? "" + }, + @"SELECT ""Order"", ""Include"", ""Expression"" FROM ""Filter"" WHERE ""BackupID"" = ? ORDER BY ""Order"" ", id) + .ToArray(); + } + + internal void SetFilters(IEnumerable values, long id, System.Data.IDbTransaction transaction = null) + { + lock(m_lock) + using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + { + OverwriteAndUpdateDb( + tr, + @"DELETE FROM ""Filter"" WHERE ""BackupID"" = ?", new object[] { id }, + values, + @"INSERT INTO ""Filter"" (""BackupID"", ""Order"", ""Include"", ""Expression"") VALUES (?, ?, ?, ?)", + (f) => new object[] { id, f.Order, f.Include, f.Expression } + ); + + if (tr != null) + tr.Commit(); + } + } + + public ISetting[] GetSettings(long id) + { + lock(m_lock) + return ReadFromDb( + (rd) => (ISetting)new Setting() { + Filter = ConvertToString(rd, 0) ?? "", + Name = ConvertToString(rd, 1) ?? "", + Value = ConvertToString(rd, 2) ?? "" + //TODO: Attach the argument information + }, + @"SELECT ""Filter"", ""Name"", ""Value"" FROM ""Option"" WHERE ""BackupID"" = ?", id) + .ToArray(); + } + + internal void SetSettings(IEnumerable values, long id, System.Data.IDbTransaction transaction = null) + { + lock(m_lock) + using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + { + OverwriteAndUpdateDb( + tr, + @"DELETE FROM ""Option"" WHERE ""BackupID"" = ?", new object[] { id }, + values, + @"INSERT INTO ""Option"" (""BackupID"", ""Filter"", ""Name"", ""Value"") VALUES (?, ?, ?, ?)", + (f) => { + if (Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER.Equals(f.Value)) + throw new Exception("Attempted to save a property with the placeholder password"); + return new object[] { id, f.Filter ?? "", f.Name, f.Value ?? "" }; + } + ); + + if (tr != null) + tr.Commit(); + } + } + + internal string[] GetSources(long id) + { + lock(m_lock) + return ReadFromDb( + (rd) => ConvertToString(rd, 0), + @"SELECT ""Path"" FROM ""Source"" WHERE ""BackupID"" = ?", id) + .ToArray(); + } + + internal void SetSources(IEnumerable values, long id, System.Data.IDbTransaction transaction) + { + lock(m_lock) + using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + { + OverwriteAndUpdateDb( + tr, + @"DELETE FROM ""Source"" WHERE ""BackupID"" = ?", new object[] { id }, + values, + @"INSERT INTO ""Source"" (""BackupID"", ""Path"") VALUES (?, ?)", + (f) => new object[] { id, f } + ); + + if (tr != null) + tr.Commit(); + } + } + + internal long[] GetBackupIDsForTags(string[] tags) + { + if (tags == null || tags.Length == 0) + return new long[0]; + + if (tags.Length == 1 && tags[0].StartsWith("ID=", StringComparison.Ordinal)) + return new long[] { long.Parse(tags[0].Substring("ID=".Length)) }; + + lock(m_lock) + using(var cmd = m_connection.CreateCommand()) + { + var sb = new StringBuilder(); + + foreach(var t in tags) + { + if (sb.Length != 0) + sb.Append(" OR "); + sb.Append(@" ("","" || ""Tags"" || "","" LIKE ""%,"" || ? || "",%"") "); + + var p = cmd.CreateParameter(); + p.Value = t; + cmd.Parameters.Add(p); + } + + cmd.CommandText = @"SELECT ""ID"" FROM ""Backup"" WHERE " + sb; + + return Read(cmd, (rd) => ConvertToInt64(rd, 0)).ToArray(); + } + } + + internal IBackup GetBackup(string id) + { + if (string.IsNullOrWhiteSpace(id)) + throw new ArgumentNullException(nameof(id)); + + return long.TryParse(id, out long lid) ? GetBackup(lid) : GetTemporaryBackup(id); + } + + internal IBackup GetBackup(long id) + { + lock(m_lock) + { + var bk = ReadFromDb( + (rd) => new Backup { + ID = ConvertToInt64(rd, 0).ToString(), + Name = ConvertToString(rd, 1), + Description = ConvertToString(rd, 2), + Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + TargetURL = ConvertToString(rd, 4), + DBPath = ConvertToString(rd, 5), + }, + @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" WHERE ID = ?", id) + .FirstOrDefault(); + + if (bk != null) + bk.LoadChildren(this); + + return bk; + } + } + + internal ISchedule GetSchedule(long id) + { + lock(m_lock) + { + var bk = ReadFromDb( + (rd) => new Schedule { + ID = ConvertToInt64(rd, 0), + Tags = (ConvertToString(rd, 1) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + Time = ConvertToDateTime(rd, 2), + Repeat = ConvertToString(rd, 3), + LastRun = ConvertToDateTime(rd, 4), + Rule = ConvertToString(rd, 5), + }, + @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" WHERE ID = ?", id) + .FirstOrDefault(); + + return bk; + } + } + + internal Boolean IsUnencryptedOrPassphraseStored(long id) + { + lock (m_lock) + { + var usesEncryption = ReadFromDb( + (rd) => ConvertToBoolean(rd, 0), + @"SELECT VALUE != """" FROM ""Option"" WHERE BackupID = ? AND NAME='encryption-module'", id) + .FirstOrDefault(); + + if (!usesEncryption) + { + return true; + } + + return ReadFromDb( + (rd) => ConvertToBoolean(rd, 0), + @"SELECT VALUE != """" FROM ""Option"" WHERE BackupID = ? AND NAME='passphrase'", id) + .FirstOrDefault(); + } + } + + internal long[] GetScheduleIDsFromTags(string[] tags) + { + if (tags == null || tags.Length == 0) + return new long[0]; + + lock(m_lock) + using(var cmd = m_connection.CreateCommand()) + { + var sb = new StringBuilder(); + + foreach(var t in tags) + { + if (sb.Length != 0) + sb.Append(" OR "); + sb.Append(@" ("","" || ""Tags"" || "","" LIKE ""%,"" || ? || "",%"") "); + + var p = cmd.CreateParameter(); + p.Value = t; + cmd.Parameters.Add(p); + } + + cmd.CommandText = @"SELECT ""ID"" FROM ""Schedule"" WHERE " + sb; + + return Read(cmd, (rd) => ConvertToInt64(rd, 0)).ToArray(); + } + } + + public void AddOrUpdateBackupAndSchedule(IBackup item, ISchedule schedule) + { + AddOrUpdateBackup(item, true, schedule); + } + + public string ValidateBackup(IBackup item, ISchedule schedule) + { + if (string.IsNullOrWhiteSpace(item.Name)) + return "Missing a name"; + + if (string.IsNullOrWhiteSpace(item.TargetURL)) + return "Missing a target"; + + if (item.Sources == null || item.Sources.Any(x => string.IsNullOrWhiteSpace(x)) || item.Sources.Length == 0) + return "Invalid source list"; + + var disabled_encryption = false; + var passphrase = string.Empty; + var gpgAsymmetricEncryption = false; + if (item.Settings != null) + { + foreach (var s in item.Settings) + + if (string.Equals(s.Name, "--no-encryption", StringComparison.OrdinalIgnoreCase)) + disabled_encryption = string.IsNullOrWhiteSpace(s.Value) || Library.Utility.Utility.ParseBool(s.Value, false); + else if (string.Equals(s.Name, "passphrase", StringComparison.OrdinalIgnoreCase)) + passphrase = s.Value; + else if (string.Equals(s.Name, "keep-versions", StringComparison.OrdinalIgnoreCase)) + { + int i; + if (!int.TryParse(s.Value, out i) || i <= 0) + return "Retention value must be a positive integer"; + } + else if (string.Equals(s.Name, "keep-time", StringComparison.OrdinalIgnoreCase)) + { + try + { + var ts = Library.Utility.Timeparser.ParseTimeSpan(s.Value); + if (ts <= TimeSpan.FromMinutes(5)) + return "Retention value must be more than 5 minutes"; + } + catch + { + return "Retention value must be a valid timespan"; + } + } + else if (string.Equals(s.Name, "dblock-size", StringComparison.OrdinalIgnoreCase)) + { + try + { + var ds = Library.Utility.Sizeparser.ParseSize(s.Value); + if (ds < 1024 * 1024) + return "DBlock size must be at least 1MB"; + } + catch + { + return "DBlock value must be a valid size string"; + } + } + else if (string.Equals(s.Name, "--blocksize", StringComparison.OrdinalIgnoreCase)) + { + try + { + var ds = Library.Utility.Sizeparser.ParseSize(s.Value); + if (ds < 1024 || ds > int.MaxValue) + return "The blocksize must be at least 1KB"; + } + catch + { + return "The blocksize value must be a valid size string"; + } + } + else if (string.Equals(s.Name, "--prefix", StringComparison.OrdinalIgnoreCase)) + { + if (!string.IsNullOrWhiteSpace(s.Value) && s.Value.Contains("-")) + return "The prefix cannot contain hyphens (-)"; + } + else if (string.Equals(s.Name, "--gpg-encryption-command", StringComparison.OrdinalIgnoreCase)) { + gpgAsymmetricEncryption = string.Equals(s.Value, "--encrypt", StringComparison.OrdinalIgnoreCase); + } + } + + if (!disabled_encryption && !gpgAsymmetricEncryption && string.IsNullOrWhiteSpace(passphrase)) + return "Missing passphrase"; + + if (schedule != null) + { + try + { + var ts = Library.Utility.Timeparser.ParseTimeSpan(schedule.Repeat); + if (ts <= TimeSpan.FromMinutes(5)) + return "Schedule repetition time must be more than 5 minutes"; + } + catch + { + return "Schedule repetition value must be a valid timespan"; + } + + } + + return null; + } + + internal void UpdateBackupDBPath(IBackup item, string path) + { + lock (m_lock) + { + using (var tr = m_connection.BeginTransaction()) + { + using (var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = tr; + cmd.Parameters.Add(cmd.CreateParameter()); + ((System.Data.IDbDataParameter) cmd.Parameters[0]).Value = path; + cmd.Parameters.Add(cmd.CreateParameter()); + ((System.Data.IDbDataParameter) cmd.Parameters[1]).Value = item.ID; + + cmd.CommandText = @"UPDATE ""Backup"" SET ""DBPath""=? WHERE ""ID""=?"; + cmd.ExecuteNonQuery(); + tr.Commit(); + } + } + } + + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + private void AddOrUpdateBackup(IBackup item, bool updateSchedule, ISchedule schedule) + { + lock(m_lock) + { + bool update = item.ID != null; + if (!update && item.DBPath == null) + { + var folder = FIXMEGlobal.DataFolder; + if (!System.IO.Directory.Exists(folder)) + System.IO.Directory.CreateDirectory(folder); + + for(var i = 0; i < 100; i++) + { + var guess = System.IO.Path.Combine(folder, System.IO.Path.ChangeExtension(Duplicati.Library.Main.DatabaseLocator.GenerateRandomName(), ".sqlite")); + if (!System.IO.File.Exists(guess)) + { + ((Backup)item).DBPath = guess; + break; + } + } + + if (item.DBPath == null) + throw new Exception("Unable to generate a unique database file name"); + } + + using(var tr = m_connection.BeginTransaction()) + { + OverwriteAndUpdateDb( + tr, + null, + new object[] { long.Parse(item.ID ?? "-1") }, + new IBackup[] { item }, + update ? + @"UPDATE ""Backup"" SET ""Name""=?, ""Description""=?, ""Tags""=?, ""TargetURL""=? WHERE ""ID""=?" : + @"INSERT INTO ""Backup"" (""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"") VALUES (?,?,?,?,?)", + (n) => { + + if (n.TargetURL.IndexOf(Duplicati.Server.WebServer.Server.PASSWORD_PLACEHOLDER, StringComparison.Ordinal) >= 0) + throw new Exception("Attempted to save a backup with the password placeholder"); + if (update && long.Parse(n.ID) <= 0) + throw new Exception("Invalid update, cannot update application settings through update method"); + + return new object[] { + n.Name, + n.Description ?? "" , // Description is optional but the column is set to NOT NULL, an additional check is welcome + string.Join(",", n.Tags ?? new string[0]), + n.TargetURL, + update ? item.ID : n.DBPath + }; + }); + + if (!update) + using(var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = tr; + cmd.CommandText = @"SELECT last_insert_rowid();"; + item.ID = ExecuteScalarInt64(cmd).ToString(); + } + + var id = long.Parse(item.ID); + + if (long.Parse(item.ID) <= 0) + throw new Exception("Invalid addition, cannot update application settings through update method"); + + SetSources(item.Sources, id, tr); + SetSettings(item.Settings, id, tr); + SetFilters(item.Filters, id, tr); + SetMetadata(item.Metadata, id, tr); + + if (updateSchedule) + { + var tags = new string[] { "ID=" + item.ID }; + var existing = GetScheduleIDsFromTags(tags); + if (schedule == null && existing.Any()) + DeleteFromDb("Schedule", existing.First(), tr); + else if (schedule != null) + { + if (existing.Any()) + { + var cur = GetSchedule(existing.First()); + cur.AllowedDays = schedule.AllowedDays; + cur.Repeat = schedule.Repeat; + cur.Tags = schedule.Tags; + cur.Time = schedule.Time; + + schedule = cur; + } + else + { + schedule.ID = -1; + } + + schedule.Tags = tags; + AddOrUpdateSchedule(schedule, tr); + } + } + + tr.Commit(); + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + } + } + + internal void AddOrUpdateSchedule(ISchedule item) + { + lock(m_lock) + using(var tr = m_connection.BeginTransaction()) + { + AddOrUpdateSchedule(item, tr); + tr.Commit(); + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + } + + private void AddOrUpdateSchedule(ISchedule item, System.Data.IDbTransaction tr) + { + lock(m_lock) + { + bool update = item.ID >= 0; + OverwriteAndUpdateDb( + tr, + null, + new object[] { item.ID }, + new ISchedule[] { item }, + update ? + @"UPDATE ""Schedule"" SET ""Tags""=?, ""Time""=?, ""Repeat""=?, ""LastRun""=?, ""Rule""=? WHERE ""ID""=?" : + @"INSERT INTO ""Schedule"" (""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"") VALUES (?,?,?,?,?)", + (n) => new object[] { + string.Join(",", n.Tags), + Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(n.Time), + n.Repeat, + Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(n.LastRun), + n.Rule ?? "", + update ? (object)item.ID : null + }); + + if (!update) + using(var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = tr; + cmd.CommandText = @"SELECT last_insert_rowid();"; + item.ID = ExecuteScalarInt64(cmd); + } + } + } + + public void DeleteBackup(long ID) + { + if (ID < 0) + return; + + lock(m_lock) + { + using(var tr = m_connection.BeginTransaction()) + { + var existing = GetScheduleIDsFromTags(new string[] { "ID=" + ID.ToString() }); + if (existing.Any()) + DeleteFromDb("Schedule", existing.First(), tr); + + DeleteFromDb("ErrorLog", ID, "BackupID", tr); + DeleteFromDb("Filter", ID, "BackupID", tr); + DeleteFromDb("Log", ID, "BackupID", tr); + DeleteFromDb("Metadata", ID, "BackupID", tr); + DeleteFromDb("Option", ID, "BackupID", tr); + DeleteFromDb("Source", ID, "BackupID", tr); + + DeleteFromDb("Backup", ID, tr); + + tr.Commit(); + } + } + + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + public void DeleteBackup(IBackup backup) + { + if (backup.IsTemporary) + UnregisterTemporaryBackup(backup); + else + DeleteBackup(long.Parse(backup.ID)); + } + + public void DeleteSchedule(long ID) + { + if (ID < 0) + return; + + lock(m_lock) + DeleteFromDb("Schedule", ID); + + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + public void DeleteSchedule(ISchedule schedule) + { + DeleteSchedule(schedule.ID); + } + + public IBackup[] Backups + { + get + { + lock(m_lock) + { + var lst = ReadFromDb( + (rd) => (IBackup)new Backup() { + ID = ConvertToInt64(rd, 0).ToString(), + Name = ConvertToString(rd, 1), + Description = ConvertToString(rd, 2), + Tags = (ConvertToString(rd, 3) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + TargetURL = ConvertToString(rd, 4), + DBPath = ConvertToString(rd, 5), + }, + @"SELECT ""ID"", ""Name"", ""Description"", ""Tags"", ""TargetURL"", ""DBPath"" FROM ""Backup"" ") + .ToArray(); + + foreach(var n in lst) + n.Metadata = GetMetadata(long.Parse(n.ID)); + + + return lst; + } + } + } + + public ISchedule[] Schedules + { + get + { + lock(m_lock) + return ReadFromDb( + (rd) => (ISchedule)new Schedule() { + ID = ConvertToInt64(rd, 0), + Tags = (ConvertToString(rd, 1) ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + Time = ConvertToDateTime(rd, 2), + Repeat = ConvertToString(rd, 3), + LastRun = ConvertToDateTime(rd, 4), + Rule = ConvertToString(rd, 5), + }, + @"SELECT ""ID"", ""Tags"", ""Time"", ""Repeat"", ""LastRun"", ""Rule"" FROM ""Schedule"" ") + .ToArray(); + } + } + + + public IFilter[] Filters + { + get { return GetFilters(ANY_BACKUP_ID); } + set { SetFilters(value, ANY_BACKUP_ID); } + } + + public ISetting[] Settings + { + get { return GetSettings(ANY_BACKUP_ID); } + set { SetSettings(value, ANY_BACKUP_ID); } + } + + public INotification[] GetNotifications() + { + lock(m_lock) + return ReadFromDb(null).Cast().ToArray(); + } + + public bool DismissNotification(long id) + { + lock(m_lock) + { + var notifications = GetNotifications(); + var cur = notifications.FirstOrDefault(x => x.ID == id); + if (cur == null) + return false; + + DeleteFromDb(typeof(Notification).Name, id); + FIXMEGlobal.DataConnection.ApplicationSettings.UnackedError = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Error); + FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning = notifications.Any(x => x.ID != id && x.Type == Duplicati.Server.Serialization.NotificationType.Warning); + } + + FIXMEGlobal.IncrementLastNotificationUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + return true; + } + + public void RegisterNotification(Serialization.NotificationType type, string title, string message, Exception ex, string backupid, string action, string logid, string messageid, string logtag, Func conflicthandler) + { + lock(m_lock) + { + var notification = new Notification() + { + ID = -1, + Type = type, + Title = title, + Message = message, + Exception = ex == null ? "" : ex.ToString(), + BackupID = backupid, + Action = action ?? "", + Timestamp = DateTime.UtcNow, + LogEntryID = logid, + MessageID = messageid, + MessageLogTag = logtag + }; + + var conflictResult = conflicthandler(notification, GetNotifications()); + if (conflictResult == null) + return; + + if (conflictResult != notification) + DeleteFromDb(typeof(Notification).Name, conflictResult.ID); + + OverwriteAndUpdateDb(null, null, null, new Notification[] { notification }, false); + + if (type == Duplicati.Server.Serialization.NotificationType.Error) + FIXMEGlobal.DataConnection.ApplicationSettings.UnackedError = true; + else if (type == Duplicati.Server.Serialization.NotificationType.Warning) + FIXMEGlobal.DataConnection.ApplicationSettings.UnackedWarning = true; + } + + FIXMEGlobal.IncrementLastNotificationUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + //Workaround to clean up the database after invalid settings update + public void FixInvalidBackupId() + { + using(var cmd = m_connection.CreateCommand()) + using (var tr = m_connection.BeginTransaction()) + { + cmd.Transaction = tr; + cmd.Parameters.Add(cmd.CreateParameter()); + ((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = -1; + cmd.CommandText = @"DELETE FROM ""Option"" WHERE ""BackupID"" = ?"; + cmd.ExecuteNonQuery(); + cmd.CommandText = @"DELETE FROM ""Metadata"" WHERE ""BackupID"" = ?"; + cmd.ExecuteNonQuery(); + cmd.CommandText = @"DELETE FROM ""Filter"" WHERE ""BackupID"" = ?"; + cmd.ExecuteNonQuery(); + cmd.CommandText = @"DELETE FROM ""Source"" WHERE ""BackupID"" = ?"; + cmd.ExecuteNonQuery(); + + cmd.Parameters.Clear(); + cmd.Parameters.Add(cmd.CreateParameter()); + + ((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = "ID=-1"; + cmd.CommandText = @"DELETE FROM ""Schedule"" WHERE ""Tags"" = ?"; + cmd.ExecuteNonQuery(); + tr.Commit(); + } + + ApplicationSettings.FixedInvalidBackupId = true; + } + + public string[] GetUISettingsSchemes() + { + lock(m_lock) + return ReadFromDb( + (rd) => ConvertToString(rd, 0) ?? "", + @"SELECT DISTINCT ""Scheme"" FROM ""UIStorage""") + .ToArray(); + } + + public IDictionary GetUISettings(string scheme) + { + lock(m_lock) + return ReadFromDb( + (rd) => new KeyValuePair( + ConvertToString(rd, 0) ?? "", + ConvertToString(rd, 1) ?? "" + ), + @"SELECT ""Key"", ""Value"" FROM ""UIStorage"" WHERE ""Scheme"" = ?", + scheme) + .GroupBy(x => x.Key) + .ToDictionary(x => x.Key, x => x.Last().Value); + } + + public void SetUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null) + { + lock(m_lock) + using(var tr = transaction == null ? m_connection.BeginTransaction() : null) + { + OverwriteAndUpdateDb( + tr, + @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = ?", new object[] { scheme }, + values, + @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (?, ?, ?)", + (f) => { + return new object[] { scheme, f.Key ?? "", f.Value ?? "" }; + } + ); + + if (tr != null) + tr.Commit(); + } + } + + public void UpdateUISettings(string scheme, IDictionary values, System.Data.IDbTransaction transaction = null) + { + lock (m_lock) + using (var tr = transaction == null ? m_connection.BeginTransaction() : null) + { + OverwriteAndUpdateDb( + tr, + @"DELETE FROM ""UIStorage"" WHERE ""Scheme"" = ? AND ""Key"" IN (?)", new object[] { scheme, values.Keys }, + values.Where(x => x.Value != null), + @"INSERT INTO ""UIStorage"" (""Scheme"", ""Key"", ""Value"") VALUES (?, ?, ?)", + (f) => + { + return new object[] { scheme, f.Key ?? "", f.Value ?? "" }; + } + ); + + if (tr != null) + tr.Commit(); + } + } + + public TempFile[] GetTempFiles() + { + lock(m_lock) + return ReadFromDb(null).ToArray(); + } + + public void DeleteTempFile(long id) + { + lock(m_lock) + DeleteFromDb(typeof(TempFile).Name, id); + } + + public long RegisterTempFile(string origin, string path, DateTime expires) + { + var tempfile = new TempFile() { + Timestamp = DateTime.Now, + Origin = origin, + Path = path, + Expires = expires + }; + + OverwriteAndUpdateDb(null, null, null, new TempFile[] { tempfile }, false); + + return tempfile.ID; + } + + public void PurgeLogData(DateTime purgeDate) + { + var t = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(purgeDate); + + using(var tr = m_connection.BeginTransaction()) + using(var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = tr; + cmd.CommandText = @"DELETE FROM ""ErrorLog"" WHERE ""Timestamp"" < ?"; + cmd.Parameters.Add(cmd.CreateParameter()); + ((System.Data.IDataParameter)cmd.Parameters[0]).Value = t; + cmd.ExecuteNonQuery(); + + tr.Commit(); + } + } + + private static DateTime ConvertToDateTime(System.Data.IDataReader rd, int index) + { + var unixTime = ConvertToInt64(rd, index); + return unixTime == 0 ? new DateTime(0) : Library.Utility.Utility.EPOCH.AddSeconds(unixTime); + } + + private static bool ConvertToBoolean(System.Data.IDataReader rd, int index) + { + return ConvertToInt64(rd, index) == 1; + } + + private static string ConvertToString(System.Data.IDataReader rd, int index) + { + var r = rd.GetValue(index); + return r == null || r == DBNull.Value ? null : r.ToString(); + } + + private static long ConvertToInt64(System.Data.IDataReader rd, int index) + { + try + { + if (!rd.IsDBNull(index)) + return rd.GetInt64(index); + } + catch + { + } + + return -1; + } + + private static long ExecuteScalarInt64(System.Data.IDbCommand cmd, long defaultValue = -1) + { + using(var rd = cmd.ExecuteReader()) + return rd.Read() ? ConvertToInt64(rd, 0) : defaultValue; + } + + private static string ExecuteScalarString(System.Data.IDbCommand cmd) + { + using(var rd = cmd.ExecuteReader()) + return rd.Read() ? ConvertToString(rd, 0) : null; + + } + + private object ConvertToEnum(Type enumType, System.Data.IDataReader rd, int index, object @default) + { + try + { + return Enum.Parse(enumType, ConvertToString(rd, index)); + } + catch + { + } + + return @default; + } + + // Overloaded function for legacy functionality + private bool DeleteFromDb(string tablename, long id, System.Data.IDbTransaction transaction = null) + { + return DeleteFromDb(tablename, id, "ID", transaction); + } + + // New function that allows to delete rows from tables with arbitrary identifier values (e.g. ID or BackupID) + private bool DeleteFromDb(string tablename, long id, string identifier, System.Data.IDbTransaction transaction = null) + { + if (transaction == null) + { + using(var tr = m_connection.BeginTransaction()) + { + var r = DeleteFromDb(tablename, id, tr); + tr.Commit(); + return r; + } + } + else + { + using(var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = transaction; + cmd.CommandText = string.Format(@"DELETE FROM ""{0}"" WHERE ""{1}""=?", tablename, identifier); + var p = cmd.CreateParameter(); + p.Value = id; + cmd.Parameters.Add(p); + + var r = cmd.ExecuteNonQuery(); + // Roll back the transaction if more than 1 ID was deleted. Multiple "BackupID" rows being deleted isn't a problem. + if (identifier == "ID" && r > 1) + throw new Exception(string.Format("Too many records attempted deleted from table {0} for id {1}: {2}", tablename, id, r)); + return r == 1; + } + } + } + + private static IEnumerable Read(System.Data.IDbCommand cmd, Func f) + { + using(var rd = cmd.ExecuteReader()) + while(rd.Read()) + yield return f(rd); + } + + private static IEnumerable Read(System.Data.IDataReader rd, Func f) + { + while(rd.Read()) + yield return f(); + } + + private System.Reflection.PropertyInfo[] GetORMFields() + { + var flags = + System.Reflection.BindingFlags.FlattenHierarchy | + System.Reflection.BindingFlags.Instance | + System.Reflection.BindingFlags.Public; + + var supportedPropertyTypes = new Type[] { + typeof(long), + typeof(string), + typeof(bool), + typeof(DateTime) + }; + + return + (from n in typeof(T).GetProperties(flags) + where supportedPropertyTypes.Contains(n.PropertyType) || n.PropertyType.IsEnum + select n).ToArray(); + } + + private IEnumerable ReadFromDb(string whereclause, params object[] args) + { + var properties = GetORMFields(); + + var sql = string.Format( + @"SELECT ""{0}"" FROM ""{1}"" {2} {3}", + string.Join(@""", """, properties.Select(x => x.Name)), + typeof(T).Name, + string.IsNullOrWhiteSpace(whereclause) ? "" : " WHERE ", + whereclause ?? "" + ); + + return ReadFromDb((rd) => { + var item = Activator.CreateInstance(); + for(var i = 0; i < properties.Length; i++) + { + var prop = properties[i]; + + if (prop.PropertyType.IsEnum) + prop.SetValue(item, ConvertToEnum(prop.PropertyType, rd, i, Enum.GetValues(prop.PropertyType).GetValue(0)), null); + else if (prop.PropertyType == typeof(string)) + prop.SetValue(item, ConvertToString(rd, i), null); + else if (prop.PropertyType == typeof(long)) + prop.SetValue(item, ConvertToInt64(rd, i), null); + else if (prop.PropertyType == typeof(bool)) + prop.SetValue(item, ConvertToBoolean(rd, i), null); + else if (prop.PropertyType == typeof(DateTime)) + prop.SetValue(item, ConvertToDateTime(rd, i), null); + } + + return item; + }, sql, args); + } + + private void OverwriteAndUpdateDb(System.Data.IDbTransaction transaction, string deleteSql, object[] deleteArgs, IEnumerable values, bool updateExisting) + { + var properties = GetORMFields(); + var idfield = properties.FirstOrDefault(x => x.Name == "ID"); + properties = properties.Where(x => x.Name != "ID").ToArray(); + + string sql; + + if (updateExisting) + { + sql = string.Format( + @"UPDATE ""{0}"" SET {1} WHERE ""ID""=?", + typeof(T).Name, + string.Join(@", ", properties.Select(x => string.Format(@"""{0}""=?", x.Name))) + ); + + properties = properties.Union(new System.Reflection.PropertyInfo[] { idfield }).ToArray(); + } + else + { + + sql = string.Format( + @"INSERT INTO ""{0}"" (""{1}"") VALUES ({2})", + typeof(T).Name, + string.Join(@""", """, properties.Select(x => x.Name)), + string.Join(@", ", properties.Select(x => "?")) + ); + } + + OverwriteAndUpdateDb(transaction, deleteSql, deleteArgs, values, sql, (item) => + { + return properties.Select((x) => + { + var val = x.GetValue(item, null); + + if (x.PropertyType.IsEnum) + val = val.ToString(); + else if (x.PropertyType == typeof(DateTime)) + val = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds((DateTime)val); + + return val; + }).ToArray(); + }); + + if (!updateExisting && values.Count() == 1 && idfield != null) + using(var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = transaction; + cmd.CommandText = @"SELECT last_insert_rowid();"; + if (idfield.PropertyType == typeof(string)) + idfield.SetValue(values.First(), ExecuteScalarString(cmd), null); + else + idfield.SetValue(values.First(), ExecuteScalarInt64(cmd), null); + } + } + + private IEnumerable ReadFromDb(Func f, string sql, params object[] args) + { + using(var cmd = m_connection.CreateCommand()) + { + cmd.CommandText = sql; + if (args != null) + foreach(var a in args) + { + var p = cmd.CreateParameter(); + p.Value = a; + cmd.Parameters.Add(p); + } + + return Read(cmd, f).ToArray(); + } + } + + private void OverwriteAndUpdateDb(System.Data.IDbTransaction transaction, string deleteSql, object[] deleteArgs, IEnumerable values, string insertSql, Func f) + { + using(var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = transaction; + + if (!string.IsNullOrEmpty(deleteSql)) + { + cmd.CommandText = deleteSql; + if (deleteArgs != null) + foreach(var a in deleteArgs) + { + var p = cmd.CreateParameter(); + p.Value = a; + cmd.Parameters.Add(p); + } + + cmd.ExecuteNonQuery(); + cmd.Parameters.Clear(); + } + + cmd.CommandText = insertSql; + + foreach(var n in values) + { + var r = f(n); + if (r == null) + continue; + + while (cmd.Parameters.Count < r.Length) + cmd.Parameters.Add(cmd.CreateParameter()); + + for(var i = 0; i < r.Length; i++) + ((System.Data.IDbDataParameter)cmd.Parameters[i]).Value = r[i]; + + cmd.ExecuteNonQuery(); + } + } + } + + #region IDisposable implementation + public void Dispose() + { + if (m_errorcmd != null) + try { if (m_errorcmd != null) m_errorcmd.Dispose(); } + catch { } + finally { m_errorcmd = null; } + + + try + { + if (m_connection != null) + m_connection.Dispose(); + } + catch + { + } + } + #endregion + } + +} + diff --git a/Duplicati/Server/Database/Database schema/1. Add Notifications.sql b/Duplicati.Library.RestAPI/Database/Database schema/1. Add Notifications.sql similarity index 95% rename from Duplicati/Server/Database/Database schema/1. Add Notifications.sql rename to Duplicati.Library.RestAPI/Database/Database schema/1. Add Notifications.sql index 578ef8ee8..9dc3c8b75 100644 --- a/Duplicati/Server/Database/Database schema/1. Add Notifications.sql +++ b/Duplicati.Library.RestAPI/Database/Database schema/1. Add Notifications.sql @@ -1,14 +1,14 @@ -/* -Notifications not yet acknowledged by the user -*/ -CREATE TABLE "Notification" ( - "ID" INTEGER PRIMARY KEY, - "Type" TEXT NOT NULL, - "Title" TEXT NOT NULL, - "Message" TEXT NOT NULL, - "Exception" TEXT NOT NULL, - "BackupID" TEXT NULL, - "Action" TEXT NOT NULL, - "Timestamp" INTEGER NOT NULL -); - +/* +Notifications not yet acknowledged by the user +*/ +CREATE TABLE "Notification" ( + "ID" INTEGER PRIMARY KEY, + "Type" TEXT NOT NULL, + "Title" TEXT NOT NULL, + "Message" TEXT NOT NULL, + "Exception" TEXT NOT NULL, + "BackupID" TEXT NULL, + "Action" TEXT NOT NULL, + "Timestamp" INTEGER NOT NULL +); + diff --git a/Duplicati/Server/Database/Database schema/2. Add UIStorage.sql b/Duplicati.Library.RestAPI/Database/Database schema/2. Add UIStorage.sql similarity index 94% rename from Duplicati/Server/Database/Database schema/2. Add UIStorage.sql rename to Duplicati.Library.RestAPI/Database/Database schema/2. Add UIStorage.sql index 2c33babc2..b0339749a 100644 --- a/Duplicati/Server/Database/Database schema/2. Add UIStorage.sql +++ b/Duplicati.Library.RestAPI/Database/Database schema/2. Add UIStorage.sql @@ -1,9 +1,9 @@ -/* -Key/value storage for frontends -*/ -CREATE TABLE "UIStorage" ( - "Scheme" TEXT NOT NULL, - "Key" TEXT NOT NULL, - "Value" TEXT NOT NULL -); - +/* +Key/value storage for frontends +*/ +CREATE TABLE "UIStorage" ( + "Scheme" TEXT NOT NULL, + "Key" TEXT NOT NULL, + "Value" TEXT NOT NULL +); + diff --git a/Duplicati/Server/Database/Database schema/3. Add temp file storage.sql b/Duplicati.Library.RestAPI/Database/Database schema/3. Add temp file storage.sql similarity index 95% rename from Duplicati/Server/Database/Database schema/3. Add temp file storage.sql rename to Duplicati.Library.RestAPI/Database/Database schema/3. Add temp file storage.sql index 1f77cd181..67d71370a 100644 --- a/Duplicati/Server/Database/Database schema/3. Add temp file storage.sql +++ b/Duplicati.Library.RestAPI/Database/Database schema/3. Add temp file storage.sql @@ -1,11 +1,11 @@ -/* -Long-term temporary file records -*/ -CREATE TABLE "TempFile" ( - "ID" INTEGER PRIMARY KEY, - "Origin" TEXT NOT NULL, - "Path" TEXT NOT NULL, - "Timestamp" INTEGER NOT NULL, - "Expires" INTEGER NOT NULL -); - +/* +Long-term temporary file records +*/ +CREATE TABLE "TempFile" ( + "ID" INTEGER PRIMARY KEY, + "Origin" TEXT NOT NULL, + "Path" TEXT NOT NULL, + "Timestamp" INTEGER NOT NULL, + "Expires" INTEGER NOT NULL +); + diff --git a/Duplicati/Server/Database/Database schema/4. Add autoincrement to backup id.sql b/Duplicati.Library.RestAPI/Database/Database schema/4. Add autoincrement to backup id.sql similarity index 100% rename from Duplicati/Server/Database/Database schema/4. Add autoincrement to backup id.sql rename to Duplicati.Library.RestAPI/Database/Database schema/4. Add autoincrement to backup id.sql diff --git a/Duplicati/Server/Database/Database schema/5. Extend notification table.sql b/Duplicati.Library.RestAPI/Database/Database schema/5. Extend notification table.sql similarity index 100% rename from Duplicati/Server/Database/Database schema/5. Extend notification table.sql rename to Duplicati.Library.RestAPI/Database/Database schema/5. Extend notification table.sql diff --git a/Duplicati/Server/Database/Database schema/6. Add Description to Backup.sql b/Duplicati.Library.RestAPI/Database/Database schema/6. Add Description to Backup.sql similarity index 100% rename from Duplicati/Server/Database/Database schema/6. Add Description to Backup.sql rename to Duplicati.Library.RestAPI/Database/Database schema/6. Add Description to Backup.sql diff --git a/Duplicati/Server/Database/Database schema/Schema.sql b/Duplicati.Library.RestAPI/Database/Database schema/Schema.sql similarity index 96% rename from Duplicati/Server/Database/Database schema/Schema.sql rename to Duplicati.Library.RestAPI/Database/Database schema/Schema.sql index 97c237ae2..b05876d22 100644 --- a/Duplicati/Server/Database/Database schema/Schema.sql +++ b/Duplicati.Library.RestAPI/Database/Database schema/Schema.sql @@ -1,158 +1,158 @@ -/* - * The primary table that stores all backups. - * - * The name and tag are free form user strings. - * The tags are comma separated - * the TargetURL is the url to remote storage, - * and the DBPath is the path to the local database - */ -CREATE TABLE "Backup" ( - "ID" INTEGER PRIMARY KEY AUTOINCREMENT, - "Name" TEXT NOT NULL, - "Description" TEXT NOT NULL DEFAULT '', - "Tags" TEXT NOT NULL, - "TargetURL" TEXT NOT NULL, - "DBPath" TEXT NOT NULL -); - -/* - * The table that stores all schedules - * - * Tags is a comma separated parsed field that indicates - * which backups to run when activated. - * special tags are ID:1 which means backup with ID = 1 - * - * Time is the scheduled time, and lastRun is the last time the backup was executed - * - * Rule is a special parsed field - */ -CREATE TABLE "Schedule" ( - "ID" INTEGER PRIMARY KEY, - "Tags" TEXT NOT NULL, - "Time" INTEGER NOT NULL, - "Repeat" TEXT NOT NULL, - "LastRun" INTEGER NOT NULL, - "Rule" TEXT NOT NULL -); - -/* - * The source table is a list of source folders and files - */ -CREATE TABLE "Source" ( - "BackupID" INTEGER NOT NULL, - "Path" TEXT NOT NULL -); - -/* - * The filter table contains all filters associated with a backup. - * The special backupID -1 means "applied to all backups" - * The expression is the filter, if the filter is a regular - * expression, it is surrounded by hard brackets [ ] - */ -CREATE TABLE "Filter" ( - "BackupID" INTEGER NOT NULL, - "Order" INTEGER NOT NULL, - "Include" INTEGER NOT NULL, - "Expression" TEXT NOT NULL -); - -/* - * All options are stored in this table - * - * The special backupID -1 means "applied to all backups". - * - * The filter is used to indicate what the option applies to, - * for instance backend:s3 will only apply to backends of type S3 - * - * The name and value are the option name and value - */ -CREATE TABLE "Option" ( - "BackupID" INTEGER NOT NULL, - "Filter" TEXT NOT NULL, - "Name" TEXT NOT NULL, - "Value" TEXT NOT NULL -); - -/* - * Recorded metadata about a backup - * This table contains metadata, such as when the backup was last started, - * how long it took, how many files there were, how big the backup set was, - * how much data was uploaded, downloaded, how fast, how much space is left, - * and similar data. Programs can use this information to improve the display, - * but cannot count on these values being present - */ -CREATE TABLE "Metadata" ( - "BackupID" INTEGER NOT NULL, - "Name" TEXT NOT NULL, - "Value" TEXT NOT NULL -); - -/* - * The log of operations initiated by the scheduler/user - */ -CREATE TABLE "Log" ( - "BackupID" INTEGER NOT NULL, - "Description" TEXT NOT NULL, - "Start" INTEGER NOT NULL, - "Finish" INTEGER NOT NULL, - "Result" TEXT NOT NULL, - "SuggestedIcon" TEXT NOT NULL -); - -/* - * The log of errors - */ -CREATE TABLE "ErrorLog" ( - "BackupID" INTEGER, - "Message" TEXT NOT NULL, - "Exception" TEXT, - "Timestamp" INTEGER NOT NULL -); - -/* -Internal version tracking -*/ -CREATE TABLE "Version" ( - "ID" INTEGER PRIMARY KEY, - "Version" INTEGER NOT NULL -); - -/* -Notifications not yet acknowledged by the user -*/ -CREATE TABLE "Notification" ( - "ID" INTEGER PRIMARY KEY, - "Type" TEXT NOT NULL, - "Title" TEXT NOT NULL, - "Message" TEXT NOT NULL, - "Exception" TEXT NOT NULL, - "BackupID" TEXT NULL, - "Action" TEXT NOT NULL, - "Timestamp" INTEGER NOT NULL, - "LogEntryID" TEXT NULL, - "MessageID" TEXT NULL, - "MessageLogTag" TEXT NULL -); - -/* -Key/value storage for frontends -*/ -CREATE TABLE "UIStorage" ( - "Scheme" TEXT NOT NULL, - "Key" TEXT NOT NULL, - "Value" TEXT NOT NULL -); - -/* -Long-term temporary file records -*/ -CREATE TABLE "TempFile" ( - "ID" INTEGER PRIMARY KEY, - "Origin" TEXT NOT NULL, - "Path" TEXT NOT NULL, - "Timestamp" INTEGER NOT NULL, - "Expires" INTEGER NOT NULL -); - -INSERT INTO "Version" ("Version") VALUES (6); - +/* + * The primary table that stores all backups. + * + * The name and tag are free form user strings. + * The tags are comma separated + * the TargetURL is the url to remote storage, + * and the DBPath is the path to the local database + */ +CREATE TABLE "Backup" ( + "ID" INTEGER PRIMARY KEY AUTOINCREMENT, + "Name" TEXT NOT NULL, + "Description" TEXT NOT NULL DEFAULT '', + "Tags" TEXT NOT NULL, + "TargetURL" TEXT NOT NULL, + "DBPath" TEXT NOT NULL +); + +/* + * The table that stores all schedules + * + * Tags is a comma separated parsed field that indicates + * which backups to run when activated. + * special tags are ID:1 which means backup with ID = 1 + * + * Time is the scheduled time, and lastRun is the last time the backup was executed + * + * Rule is a special parsed field + */ +CREATE TABLE "Schedule" ( + "ID" INTEGER PRIMARY KEY, + "Tags" TEXT NOT NULL, + "Time" INTEGER NOT NULL, + "Repeat" TEXT NOT NULL, + "LastRun" INTEGER NOT NULL, + "Rule" TEXT NOT NULL +); + +/* + * The source table is a list of source folders and files + */ +CREATE TABLE "Source" ( + "BackupID" INTEGER NOT NULL, + "Path" TEXT NOT NULL +); + +/* + * The filter table contains all filters associated with a backup. + * The special backupID -1 means "applied to all backups" + * The expression is the filter, if the filter is a regular + * expression, it is surrounded by hard brackets [ ] + */ +CREATE TABLE "Filter" ( + "BackupID" INTEGER NOT NULL, + "Order" INTEGER NOT NULL, + "Include" INTEGER NOT NULL, + "Expression" TEXT NOT NULL +); + +/* + * All options are stored in this table + * + * The special backupID -1 means "applied to all backups". + * + * The filter is used to indicate what the option applies to, + * for instance backend:s3 will only apply to backends of type S3 + * + * The name and value are the option name and value + */ +CREATE TABLE "Option" ( + "BackupID" INTEGER NOT NULL, + "Filter" TEXT NOT NULL, + "Name" TEXT NOT NULL, + "Value" TEXT NOT NULL +); + +/* + * Recorded metadata about a backup + * This table contains metadata, such as when the backup was last started, + * how long it took, how many files there were, how big the backup set was, + * how much data was uploaded, downloaded, how fast, how much space is left, + * and similar data. Programs can use this information to improve the display, + * but cannot count on these values being present + */ +CREATE TABLE "Metadata" ( + "BackupID" INTEGER NOT NULL, + "Name" TEXT NOT NULL, + "Value" TEXT NOT NULL +); + +/* + * The log of operations initiated by the scheduler/user + */ +CREATE TABLE "Log" ( + "BackupID" INTEGER NOT NULL, + "Description" TEXT NOT NULL, + "Start" INTEGER NOT NULL, + "Finish" INTEGER NOT NULL, + "Result" TEXT NOT NULL, + "SuggestedIcon" TEXT NOT NULL +); + +/* + * The log of errors + */ +CREATE TABLE "ErrorLog" ( + "BackupID" INTEGER, + "Message" TEXT NOT NULL, + "Exception" TEXT, + "Timestamp" INTEGER NOT NULL +); + +/* +Internal version tracking +*/ +CREATE TABLE "Version" ( + "ID" INTEGER PRIMARY KEY, + "Version" INTEGER NOT NULL +); + +/* +Notifications not yet acknowledged by the user +*/ +CREATE TABLE "Notification" ( + "ID" INTEGER PRIMARY KEY, + "Type" TEXT NOT NULL, + "Title" TEXT NOT NULL, + "Message" TEXT NOT NULL, + "Exception" TEXT NOT NULL, + "BackupID" TEXT NULL, + "Action" TEXT NOT NULL, + "Timestamp" INTEGER NOT NULL, + "LogEntryID" TEXT NULL, + "MessageID" TEXT NULL, + "MessageLogTag" TEXT NULL +); + +/* +Key/value storage for frontends +*/ +CREATE TABLE "UIStorage" ( + "Scheme" TEXT NOT NULL, + "Key" TEXT NOT NULL, + "Value" TEXT NOT NULL +); + +/* +Long-term temporary file records +*/ +CREATE TABLE "TempFile" ( + "ID" INTEGER PRIMARY KEY, + "Origin" TEXT NOT NULL, + "Path" TEXT NOT NULL, + "Timestamp" INTEGER NOT NULL, + "Expires" INTEGER NOT NULL +); + +INSERT INTO "Version" ("Version") VALUES (6); + diff --git a/Duplicati.Library.RestAPI/Database/DatabaseConnectionSchemaMarker.cs b/Duplicati.Library.RestAPI/Database/DatabaseConnectionSchemaMarker.cs new file mode 100644 index 000000000..110f8477f --- /dev/null +++ b/Duplicati.Library.RestAPI/Database/DatabaseConnectionSchemaMarker.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Duplicati.Library.RestAPI.Database +{ + public static class DatabaseConnectionSchemaMarker + { + } +} diff --git a/Duplicati.Library.RestAPI/Database/Filter.cs b/Duplicati.Library.RestAPI/Database/Filter.cs new file mode 100644 index 000000000..7f14e2bda --- /dev/null +++ b/Duplicati.Library.RestAPI/Database/Filter.cs @@ -0,0 +1,31 @@ +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; + +namespace Duplicati.Server.Database +{ + public class Filter : Duplicati.Server.Serialization.Interface.IFilter + { + public long Order { get; set; } + + public bool Include { get; set; } + + public string Expression { get; set; } + } +} + diff --git a/Duplicati.Library.RestAPI/Database/Notification.cs b/Duplicati.Library.RestAPI/Database/Notification.cs new file mode 100644 index 000000000..22b507701 --- /dev/null +++ b/Duplicati.Library.RestAPI/Database/Notification.cs @@ -0,0 +1,39 @@ +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; + +namespace Duplicati.Server.Database +{ + public class Notification : Server.Serialization.Interface.INotification + { + #region INotification implementation + public long ID { get; set; } + public Duplicati.Server.Serialization.NotificationType Type { get; set; } + public string Title { get; set; } + public string Message { get; set; } + public string Exception { get; set; } + public string BackupID { get; set; } + public string Action { get; set; } + public DateTime Timestamp { get; set; } + public string LogEntryID { get; set; } + public string MessageID { get; set; } + public string MessageLogTag { get; set; } + #endregion + } +} + diff --git a/Duplicati/Server/Database/Schedule.cs b/Duplicati.Library.RestAPI/Database/Schedule.cs similarity index 65% rename from Duplicati/Server/Database/Schedule.cs rename to Duplicati.Library.RestAPI/Database/Schedule.cs index c0ebb77d8..7df817d40 100644 --- a/Duplicati/Server/Database/Schedule.cs +++ b/Duplicati.Library.RestAPI/Database/Schedule.cs @@ -1,24 +1,20 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using System.Linq; using Duplicati.Server.Serialization.Interface; diff --git a/Duplicati/Server/Database/ServerSettings.cs b/Duplicati.Library.RestAPI/Database/ServerSettings.cs similarity index 89% rename from Duplicati/Server/Database/ServerSettings.cs rename to Duplicati.Library.RestAPI/Database/ServerSettings.cs index f21f0ea3d..92b02bcfa 100644 --- a/Duplicati/Server/Database/ServerSettings.cs +++ b/Duplicati.Library.RestAPI/Database/ServerSettings.cs @@ -1,567 +1,564 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Linq; -using System.Collections.Generic; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using Duplicati.Library.Common; - -namespace Duplicati.Server.Database -{ - public class ServerSettings - { - private static class CONST - { - public const string STARTUP_DELAY = "startup-delay"; - public const string DOWNLOAD_SPEED_LIMIT = "max-download-speed"; - public const string UPLOAD_SPEED_LIMIT = "max-upload-speed"; - public const string THREAD_PRIORITY = "thread-priority"; - public const string LAST_WEBSERVER_PORT = "last-webserver-port"; - public const string IS_FIRST_RUN = "is-first-run"; - public const string SERVER_PORT_CHANGED = "server-port-changed"; - public const string SERVER_PASSPHRASE = "server-passphrase"; - public const string SERVER_PASSPHRASE_SALT = "server-passphrase-salt"; - public const string SERVER_PASSPHRASETRAYICON = "server-passphrase-trayicon"; - public const string SERVER_PASSPHRASETRAYICONHASH = "server-passphrase-trayicon-hash"; - public const string UPDATE_CHECK_LAST = "last-update-check"; - public const string UPDATE_CHECK_INTERVAL = "update-check-interval"; - public const string UPDATE_CHECK_NEW_VERSION = "update-check-latest"; - public const string UNACKED_ERROR = "unacked-error"; - public const string UNACKED_WARNING = "unacked-warning"; - public const string SERVER_LISTEN_INTERFACE = "server-listen-interface"; - public const string SERVER_SSL_CERTIFICATE = "server-ssl-certificate"; - public const string HAS_FIXED_INVALID_BACKUPID = "has-fixed-invalid-backup-id"; - public const string UPDATE_CHANNEL = "update-channel"; - public const string USAGE_REPORTER_LEVEL = "usage-reporter-level"; - public const string HAS_ASKED_FOR_PASSWORD_PROTECTION = "has-asked-for-password-protection"; - public const string DISABLE_TRAY_ICON_LOGIN = "disable-tray-icon-login"; - public const string SERVER_ALLOWED_HOSTNAMES = "allowed-hostnames"; - } - - private readonly Dictionary settings; - private readonly Connection databaseConnection; - private Library.AutoUpdater.UpdateInfo m_latestUpdate; - - internal ServerSettings(Connection con) - { - settings = new Dictionary(); - databaseConnection = con; - ReloadSettings(); - } - - public void ReloadSettings() - { - lock(databaseConnection.m_lock) - { - settings.Clear(); - foreach(var n in typeof(CONST).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Static).Select(x => (string)x.GetValue(null))) - settings[n] = null; - foreach(var n in databaseConnection.GetSettings(Connection.SERVER_SETTINGS_ID)) - settings[n.Name] = n.Value; - } - } - - public void UpdateSettings(Dictionary newsettings, bool clearExisting) - { - if (newsettings == null) - throw new ArgumentNullException(); - - lock(databaseConnection.m_lock) - { - m_latestUpdate = null; - if (clearExisting) - settings.Clear(); - - foreach(var k in newsettings) - if (!clearExisting && newsettings[k.Key] == null && k.Key.StartsWith("--", StringComparison.Ordinal)) - settings.Remove(k.Key); - else - settings[k.Key] = newsettings[k.Key]; - - } - - SaveSettings(); - - if (newsettings.Keys.Contains(CONST.SERVER_PASSPHRASE)) - GenerateWebserverPasswordTrayIcon(); - } - - private void SaveSettings() - { - databaseConnection.SetSettings( - from n in settings - select (Duplicati.Server.Serialization.Interface.ISetting)new Setting() { - Filter = "", - Name = n.Key, - Value = n.Value - }, Database.Connection.SERVER_SETTINGS_ID); - - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - - // In case the usage reporter is enabled or disabled, refresh now - Program.StartOrStopUsageReporter(); - // If throttle options were changed, update now - Program.UpdateThrottleSpeeds(); - } - - public string StartupDelayDuration - { - get - { - return settings[CONST.STARTUP_DELAY]; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.STARTUP_DELAY] = value; - SaveSettings(); - } - } - - public System.Threading.ThreadPriority? ThreadPriorityOverride - { - get - { - var tp = settings[CONST.THREAD_PRIORITY]; - if (string.IsNullOrEmpty(tp)) - return null; - - System.Threading.ThreadPriority r; - if (Enum.TryParse(tp, true, out r)) - return r; - - return null; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.THREAD_PRIORITY] = value.HasValue ? Enum.GetName(typeof(System.Threading.ThreadPriority), value.Value) : null; - } - } - - public string DownloadSpeedLimit - { - get - { - return settings[CONST.DOWNLOAD_SPEED_LIMIT]; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.DOWNLOAD_SPEED_LIMIT] = value; - SaveSettings(); - } - } - - public string UploadSpeedLimit - { - get - { - return settings[CONST.UPLOAD_SPEED_LIMIT]; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.UPLOAD_SPEED_LIMIT] = value; - SaveSettings(); - } - } - - public bool IsFirstRun - { - get - { - return Duplicati.Library.Utility.Utility.ParseBoolOption(settings, CONST.IS_FIRST_RUN); - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.IS_FIRST_RUN] = value.ToString(); - SaveSettings(); - } - } - - public bool HasAskedForPasswordProtection - { - get - { - return Duplicati.Library.Utility.Utility.ParseBoolOption(settings, CONST.HAS_ASKED_FOR_PASSWORD_PROTECTION); - } - set - { - lock (databaseConnection.m_lock) - settings[CONST.HAS_ASKED_FOR_PASSWORD_PROTECTION] = value.ToString(); - SaveSettings(); - } - } - - public bool UnackedError - { - get - { - return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.UNACKED_ERROR], false); - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.UNACKED_ERROR] = value.ToString(); - SaveSettings(); - } - } - - public bool UnackedWarning - { - get - { - return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.UNACKED_WARNING], false); - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.UNACKED_WARNING] = value.ToString(); - SaveSettings(); - } - } - - public bool ServerPortChanged - { - get - { - return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.SERVER_PORT_CHANGED], false); - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.SERVER_PORT_CHANGED] = value.ToString(); - SaveSettings(); - } - } - - public bool DisableTrayIconLogin - { - get - { - return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.DISABLE_TRAY_ICON_LOGIN], false); - } - set - { - lock (databaseConnection.m_lock) - settings[CONST.DISABLE_TRAY_ICON_LOGIN] = value.ToString(); - SaveSettings(); - } - } - - public int LastWebserverPort - { - get - { - var tp = settings[CONST.LAST_WEBSERVER_PORT]; - int p; - if (string.IsNullOrEmpty(tp) || !int.TryParse(tp, out p)) - return -1; - - return p; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.LAST_WEBSERVER_PORT] = value.ToString(); - SaveSettings(); - } - } - - public string WebserverPassword - { - get - { - return settings[CONST.SERVER_PASSPHRASE]; - } - } - - public string WebserverPasswordSalt - { - get - { - return settings[CONST.SERVER_PASSPHRASE_SALT]; - } - } - - public void SetWebserverPassword(string password) - { - if (string.IsNullOrWhiteSpace(password)) - { - lock(databaseConnection.m_lock) - { - settings[CONST.SERVER_PASSPHRASE] = ""; - settings[CONST.SERVER_PASSPHRASE_SALT] = ""; - } - } - else - { - var prng = RandomNumberGenerator.Create(); - var buf = new byte[32]; - prng.GetBytes(buf); - var salt = Convert.ToBase64String(buf); - - var sha256 = System.Security.Cryptography.SHA256.Create(); - var str = System.Text.Encoding.UTF8.GetBytes(password); - - sha256.TransformBlock(str, 0, str.Length, str, 0); - sha256.TransformFinalBlock(buf, 0, buf.Length); - var pwd = Convert.ToBase64String(sha256.Hash); - - lock(databaseConnection.m_lock) - { - settings[CONST.SERVER_PASSPHRASE] = pwd; - settings[CONST.SERVER_PASSPHRASE_SALT] = salt; - } - } - - SaveSettings(); - } - - public void SetAllowedHostnames(string allowedHostnames) - { - lock (databaseConnection.m_lock) - settings[CONST.SERVER_ALLOWED_HOSTNAMES] = allowedHostnames; - - SaveSettings(); - } - - public string WebserverPasswordTrayIcon => settings[CONST.SERVER_PASSPHRASETRAYICON]; - - public string WebserverPasswordTrayIconHash => settings[CONST.SERVER_PASSPHRASETRAYICONHASH]; - - public string AllowedHostnames => settings[CONST.SERVER_ALLOWED_HOSTNAMES]; - - public void GenerateWebserverPasswordTrayIcon() - { - var password = ""; - var pwd = ""; - - if (!string.IsNullOrEmpty(settings[CONST.SERVER_PASSPHRASE])) - { - password = Guid.NewGuid().ToString(); - var buf = Convert.FromBase64String(settings[CONST.SERVER_PASSPHRASE_SALT]); - - var sha256 = System.Security.Cryptography.SHA256.Create(); - var str = System.Text.Encoding.UTF8.GetBytes(password); - - sha256.TransformBlock(str, 0, str.Length, str, 0); - sha256.TransformFinalBlock(buf, 0, buf.Length); - pwd = Convert.ToBase64String(sha256.Hash); - } - - lock (databaseConnection.m_lock) - { - settings[CONST.SERVER_PASSPHRASETRAYICON] = password; - settings[CONST.SERVER_PASSPHRASETRAYICONHASH] = pwd; - } - - SaveSettings(); - } - - public DateTime LastUpdateCheck - { - get - { - long t; - if (long.TryParse(settings[CONST.UPDATE_CHECK_LAST], out t)) - return new DateTime(t, DateTimeKind.Utc); - else - return new DateTime(0, DateTimeKind.Utc); - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.UPDATE_CHECK_LAST] = value.ToUniversalTime().Ticks.ToString(); - SaveSettings(); - } - } - - public string UpdateCheckInterval - { - get - { - var tp = settings[CONST.UPDATE_CHECK_INTERVAL]; - if (string.IsNullOrWhiteSpace(tp)) - tp = "1W"; - - return tp; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.UPDATE_CHECK_INTERVAL] = value; - SaveSettings(); - Program.UpdatePoller.Reschedule(); - } - } - - public DateTime NextUpdateCheck - { - get - { - try - { - return Duplicati.Library.Utility.Timeparser.ParseTimeInterval(UpdateCheckInterval, LastUpdateCheck); - } - catch - { - return LastUpdateCheck.AddDays(7); - } - } - } - - public Library.AutoUpdater.UpdateInfo UpdatedVersion - { - get - { - if (string.IsNullOrWhiteSpace(settings[CONST.UPDATE_CHECK_NEW_VERSION])) - return null; - - try - { - if (m_latestUpdate != null) - return m_latestUpdate; - - using(var tr = new System.IO.StringReader(settings[CONST.UPDATE_CHECK_NEW_VERSION])) - return m_latestUpdate = Server.Serialization.Serializer.Deserialize(tr); - } - catch - { - } - - return null; - } - set - { - string result = null; - if (value != null) - { - var sb = new System.Text.StringBuilder(); - using(var tw = new System.IO.StringWriter(sb)) - Server.Serialization.Serializer.SerializeJson(tw, value); - - result = sb.ToString(); - } - - m_latestUpdate = value; - lock(databaseConnection.m_lock) - settings[CONST.UPDATE_CHECK_NEW_VERSION] = result; - - SaveSettings(); - } - } - - public string ServerListenInterface - { - get - { - return settings[CONST.SERVER_LISTEN_INTERFACE]; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.SERVER_LISTEN_INTERFACE] = value; - SaveSettings(); - } - } - - public X509Certificate2 ServerSSLCertificate - { - get - { - if (String.IsNullOrEmpty(settings[CONST.SERVER_SSL_CERTIFICATE])) - return null; - - if (Platform.IsClientWindows) - return new X509Certificate2(Convert.FromBase64String(settings[CONST.SERVER_SSL_CERTIFICATE])); - else - return new X509Certificate2(Convert.FromBase64String(settings[CONST.SERVER_SSL_CERTIFICATE]), ""); - } - set - { - if (value == null) - { - lock (databaseConnection.m_lock) - settings[CONST.SERVER_SSL_CERTIFICATE] = String.Empty; - } - else - { - if (Platform.IsClientWindows) - lock (databaseConnection.m_lock) - settings[CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(value.Export(X509ContentType.Pkcs12)); - else - lock (databaseConnection.m_lock) - settings[CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(value.Export(X509ContentType.Pkcs12, "")); - } - SaveSettings(); - } - } - - public bool FixedInvalidBackupId - { - get - { - return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.HAS_FIXED_INVALID_BACKUPID], false); - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.HAS_FIXED_INVALID_BACKUPID] = value.ToString(); - SaveSettings(); - } - } - - public string UpdateChannel - { - get - { - return settings[CONST.UPDATE_CHANNEL]; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.UPDATE_CHANNEL] = value; - SaveSettings(); - } - } - - public string UsageReporterLevel - { - get - { - return settings[CONST.USAGE_REPORTER_LEVEL]; - } - set - { - lock(databaseConnection.m_lock) - settings[CONST.USAGE_REPORTER_LEVEL] = value; - SaveSettings(); - } - } - } -} - +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Linq; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Duplicati.Library.Common; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.Database +{ + public class ServerSettings + { + private static class CONST + { + public const string STARTUP_DELAY = "startup-delay"; + public const string DOWNLOAD_SPEED_LIMIT = "max-download-speed"; + public const string UPLOAD_SPEED_LIMIT = "max-upload-speed"; + public const string THREAD_PRIORITY = "thread-priority"; + public const string LAST_WEBSERVER_PORT = "last-webserver-port"; + public const string IS_FIRST_RUN = "is-first-run"; + public const string SERVER_PORT_CHANGED = "server-port-changed"; + public const string SERVER_PASSPHRASE = "server-passphrase"; + public const string SERVER_PASSPHRASE_SALT = "server-passphrase-salt"; + public const string SERVER_PASSPHRASETRAYICON = "server-passphrase-trayicon"; + public const string SERVER_PASSPHRASETRAYICONHASH = "server-passphrase-trayicon-hash"; + public const string UPDATE_CHECK_LAST = "last-update-check"; + public const string UPDATE_CHECK_INTERVAL = "update-check-interval"; + public const string UPDATE_CHECK_NEW_VERSION = "update-check-latest"; + public const string UNACKED_ERROR = "unacked-error"; + public const string UNACKED_WARNING = "unacked-warning"; + public const string SERVER_LISTEN_INTERFACE = "server-listen-interface"; + public const string SERVER_SSL_CERTIFICATE = "server-ssl-certificate"; + public const string HAS_FIXED_INVALID_BACKUPID = "has-fixed-invalid-backup-id"; + public const string UPDATE_CHANNEL = "update-channel"; + public const string USAGE_REPORTER_LEVEL = "usage-reporter-level"; + public const string HAS_ASKED_FOR_PASSWORD_PROTECTION = "has-asked-for-password-protection"; + public const string DISABLE_TRAY_ICON_LOGIN = "disable-tray-icon-login"; + public const string SERVER_ALLOWED_HOSTNAMES = "allowed-hostnames"; + } + + private readonly Dictionary settings; + private readonly Connection databaseConnection; + private Library.AutoUpdater.UpdateInfo m_latestUpdate; + + internal ServerSettings(Connection con) + { + settings = new Dictionary(); + databaseConnection = con; + ReloadSettings(); + } + + public void ReloadSettings() + { + lock(databaseConnection.m_lock) + { + settings.Clear(); + foreach(var n in typeof(CONST).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.Static).Select(x => (string)x.GetValue(null))) + settings[n] = null; + foreach(var n in databaseConnection.GetSettings(Connection.SERVER_SETTINGS_ID)) + settings[n.Name] = n.Value; + } + } + + public void UpdateSettings(Dictionary newsettings, bool clearExisting) + { + if (newsettings == null) + throw new ArgumentNullException(); + + lock(databaseConnection.m_lock) + { + m_latestUpdate = null; + if (clearExisting) + settings.Clear(); + + foreach(var k in newsettings) + if (!clearExisting && newsettings[k.Key] == null && k.Key.StartsWith("--", StringComparison.Ordinal)) + settings.Remove(k.Key); + else + settings[k.Key] = newsettings[k.Key]; + + } + + SaveSettings(); + + if (newsettings.Keys.Contains(CONST.SERVER_PASSPHRASE)) + GenerateWebserverPasswordTrayIcon(); + } + + private void SaveSettings() + { + databaseConnection.SetSettings( + from n in settings + select (Duplicati.Server.Serialization.Interface.ISetting)new Setting() { + Filter = "", + Name = n.Key, + Value = n.Value + }, Database.Connection.SERVER_SETTINGS_ID); + + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + // In case the usage reporter is enabled or disabled, refresh now + FIXMEGlobal.StartOrStopUsageReporter(); + // If throttle options were changed, update now + FIXMEGlobal.UpdateThrottleSpeeds(); + } + + public string StartupDelayDuration + { + get + { + return settings[CONST.STARTUP_DELAY]; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.STARTUP_DELAY] = value; + SaveSettings(); + } + } + + public System.Threading.ThreadPriority? ThreadPriorityOverride + { + get + { + var tp = settings[CONST.THREAD_PRIORITY]; + if (string.IsNullOrEmpty(tp)) + return null; + + System.Threading.ThreadPriority r; + if (Enum.TryParse(tp, true, out r)) + return r; + + return null; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.THREAD_PRIORITY] = value.HasValue ? Enum.GetName(typeof(System.Threading.ThreadPriority), value.Value) : null; + } + } + + public string DownloadSpeedLimit + { + get + { + return settings[CONST.DOWNLOAD_SPEED_LIMIT]; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.DOWNLOAD_SPEED_LIMIT] = value; + SaveSettings(); + } + } + + public string UploadSpeedLimit + { + get + { + return settings[CONST.UPLOAD_SPEED_LIMIT]; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.UPLOAD_SPEED_LIMIT] = value; + SaveSettings(); + } + } + + public bool IsFirstRun + { + get + { + return Duplicati.Library.Utility.Utility.ParseBoolOption(settings, CONST.IS_FIRST_RUN); + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.IS_FIRST_RUN] = value.ToString(); + SaveSettings(); + } + } + + public bool HasAskedForPasswordProtection + { + get + { + return Duplicati.Library.Utility.Utility.ParseBoolOption(settings, CONST.HAS_ASKED_FOR_PASSWORD_PROTECTION); + } + set + { + lock (databaseConnection.m_lock) + settings[CONST.HAS_ASKED_FOR_PASSWORD_PROTECTION] = value.ToString(); + SaveSettings(); + } + } + + public bool UnackedError + { + get + { + return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.UNACKED_ERROR], false); + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.UNACKED_ERROR] = value.ToString(); + SaveSettings(); + } + } + + public bool UnackedWarning + { + get + { + return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.UNACKED_WARNING], false); + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.UNACKED_WARNING] = value.ToString(); + SaveSettings(); + } + } + + public bool ServerPortChanged + { + get + { + return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.SERVER_PORT_CHANGED], false); + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.SERVER_PORT_CHANGED] = value.ToString(); + SaveSettings(); + } + } + + public bool DisableTrayIconLogin + { + get + { + return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.DISABLE_TRAY_ICON_LOGIN], false); + } + set + { + lock (databaseConnection.m_lock) + settings[CONST.DISABLE_TRAY_ICON_LOGIN] = value.ToString(); + SaveSettings(); + } + } + + public int LastWebserverPort + { + get + { + var tp = settings[CONST.LAST_WEBSERVER_PORT]; + int p; + if (string.IsNullOrEmpty(tp) || !int.TryParse(tp, out p)) + return -1; + + return p; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.LAST_WEBSERVER_PORT] = value.ToString(); + SaveSettings(); + } + } + + public string WebserverPassword + { + get + { + return settings[CONST.SERVER_PASSPHRASE]; + } + } + + public string WebserverPasswordSalt + { + get + { + return settings[CONST.SERVER_PASSPHRASE_SALT]; + } + } + + public void SetWebserverPassword(string password) + { + if (string.IsNullOrWhiteSpace(password)) + { + lock(databaseConnection.m_lock) + { + settings[CONST.SERVER_PASSPHRASE] = ""; + settings[CONST.SERVER_PASSPHRASE_SALT] = ""; + } + } + else + { + var prng = RandomNumberGenerator.Create(); + var buf = new byte[32]; + prng.GetBytes(buf); + var salt = Convert.ToBase64String(buf); + + var sha256 = System.Security.Cryptography.SHA256.Create(); + var str = System.Text.Encoding.UTF8.GetBytes(password); + + sha256.TransformBlock(str, 0, str.Length, str, 0); + sha256.TransformFinalBlock(buf, 0, buf.Length); + var pwd = Convert.ToBase64String(sha256.Hash); + + lock(databaseConnection.m_lock) + { + settings[CONST.SERVER_PASSPHRASE] = pwd; + settings[CONST.SERVER_PASSPHRASE_SALT] = salt; + } + } + + SaveSettings(); + } + + public void SetAllowedHostnames(string allowedHostnames) + { + lock (databaseConnection.m_lock) + settings[CONST.SERVER_ALLOWED_HOSTNAMES] = allowedHostnames; + + SaveSettings(); + } + + public string WebserverPasswordTrayIcon => settings[CONST.SERVER_PASSPHRASETRAYICON]; + + public string WebserverPasswordTrayIconHash => settings[CONST.SERVER_PASSPHRASETRAYICONHASH]; + + public string AllowedHostnames => settings[CONST.SERVER_ALLOWED_HOSTNAMES]; + + public void GenerateWebserverPasswordTrayIcon() + { + var password = ""; + var pwd = ""; + + if (!string.IsNullOrEmpty(settings[CONST.SERVER_PASSPHRASE])) + { + password = Guid.NewGuid().ToString(); + var buf = Convert.FromBase64String(settings[CONST.SERVER_PASSPHRASE_SALT]); + + var sha256 = System.Security.Cryptography.SHA256.Create(); + var str = System.Text.Encoding.UTF8.GetBytes(password); + + sha256.TransformBlock(str, 0, str.Length, str, 0); + sha256.TransformFinalBlock(buf, 0, buf.Length); + pwd = Convert.ToBase64String(sha256.Hash); + } + + lock (databaseConnection.m_lock) + { + settings[CONST.SERVER_PASSPHRASETRAYICON] = password; + settings[CONST.SERVER_PASSPHRASETRAYICONHASH] = pwd; + } + + SaveSettings(); + } + + public DateTime LastUpdateCheck + { + get + { + long t; + if (long.TryParse(settings[CONST.UPDATE_CHECK_LAST], out t)) + return new DateTime(t, DateTimeKind.Utc); + else + return new DateTime(0, DateTimeKind.Utc); + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.UPDATE_CHECK_LAST] = value.ToUniversalTime().Ticks.ToString(); + SaveSettings(); + } + } + + public string UpdateCheckInterval + { + get + { + var tp = settings[CONST.UPDATE_CHECK_INTERVAL]; + if (string.IsNullOrWhiteSpace(tp)) + tp = "1W"; + + return tp; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.UPDATE_CHECK_INTERVAL] = value; + SaveSettings(); + FIXMEGlobal.UpdatePoller.Reschedule(); + } + } + + public DateTime NextUpdateCheck + { + get + { + try + { + return Duplicati.Library.Utility.Timeparser.ParseTimeInterval(UpdateCheckInterval, LastUpdateCheck); + } + catch + { + return LastUpdateCheck.AddDays(7); + } + } + } + + public Library.AutoUpdater.UpdateInfo UpdatedVersion + { + get + { + if (string.IsNullOrWhiteSpace(settings[CONST.UPDATE_CHECK_NEW_VERSION])) + return null; + + try + { + if (m_latestUpdate != null) + return m_latestUpdate; + + using(var tr = new System.IO.StringReader(settings[CONST.UPDATE_CHECK_NEW_VERSION])) + return m_latestUpdate = Server.Serialization.Serializer.Deserialize(tr); + } + catch + { + } + + return null; + } + set + { + string result = null; + if (value != null) + { + var sb = new System.Text.StringBuilder(); + using(var tw = new System.IO.StringWriter(sb)) + Server.Serialization.Serializer.SerializeJson(tw, value); + + result = sb.ToString(); + } + + m_latestUpdate = value; + lock(databaseConnection.m_lock) + settings[CONST.UPDATE_CHECK_NEW_VERSION] = result; + + SaveSettings(); + } + } + + public string ServerListenInterface + { + get + { + return settings[CONST.SERVER_LISTEN_INTERFACE]; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.SERVER_LISTEN_INTERFACE] = value; + SaveSettings(); + } + } + + public X509Certificate2 ServerSSLCertificate + { + get + { + if (String.IsNullOrEmpty(settings[CONST.SERVER_SSL_CERTIFICATE])) + return null; + + if (Platform.IsClientWindows) + return new X509Certificate2(Convert.FromBase64String(settings[CONST.SERVER_SSL_CERTIFICATE])); + else + return new X509Certificate2(Convert.FromBase64String(settings[CONST.SERVER_SSL_CERTIFICATE]), ""); + } + set + { + if (value == null) + { + lock (databaseConnection.m_lock) + settings[CONST.SERVER_SSL_CERTIFICATE] = String.Empty; + } + else + { + if (Platform.IsClientWindows) + lock (databaseConnection.m_lock) + settings[CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(value.Export(X509ContentType.Pkcs12)); + else + lock (databaseConnection.m_lock) + settings[CONST.SERVER_SSL_CERTIFICATE] = Convert.ToBase64String(value.Export(X509ContentType.Pkcs12, "")); + } + SaveSettings(); + } + } + + public bool FixedInvalidBackupId + { + get + { + return Duplicati.Library.Utility.Utility.ParseBool(settings[CONST.HAS_FIXED_INVALID_BACKUPID], false); + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.HAS_FIXED_INVALID_BACKUPID] = value.ToString(); + SaveSettings(); + } + } + + public string UpdateChannel + { + get + { + return settings[CONST.UPDATE_CHANNEL]; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.UPDATE_CHANNEL] = value; + SaveSettings(); + } + } + + public string UsageReporterLevel + { + get + { + return settings[CONST.USAGE_REPORTER_LEVEL]; + } + set + { + lock(databaseConnection.m_lock) + settings[CONST.USAGE_REPORTER_LEVEL] = value; + SaveSettings(); + } + } + } +} + diff --git a/Duplicati.Library.RestAPI/Database/Setting.cs b/Duplicati.Library.RestAPI/Database/Setting.cs new file mode 100644 index 000000000..84150bf1a --- /dev/null +++ b/Duplicati.Library.RestAPI/Database/Setting.cs @@ -0,0 +1,26 @@ +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; + +namespace Duplicati.Server.Database +{ + public class Setting : Duplicati.Server.Serialization.Implementations.Setting + { + } +} + diff --git a/Duplicati.Library.RestAPI/Database/TempFile.cs b/Duplicati.Library.RestAPI/Database/TempFile.cs new file mode 100644 index 000000000..b652a2920 --- /dev/null +++ b/Duplicati.Library.RestAPI/Database/TempFile.cs @@ -0,0 +1,31 @@ +// Copyright (C) 2015, The Duplicati Team + +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; + +namespace Duplicati.Server.Database +{ + public class TempFile + { + public long ID { get; set; } + public string Origin { get; set; } + public string Path { get; set; } + public DateTime Timestamp { get; set; } + public DateTime Expires { get; set; } + } +} + diff --git a/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj b/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj new file mode 100644 index 000000000..f350e3e34 --- /dev/null +++ b/Duplicati.Library.RestAPI/Duplicati.Library.RestAPI.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + + + + + + + + + + + + + + + + + ..\thirdparty\HttpServer\HttpServer.dll + + + + + + + + + + + + + + diff --git a/Duplicati/Server/EventPollNotify.cs b/Duplicati.Library.RestAPI/EventPollNotify.cs similarity index 66% rename from Duplicati/Server/EventPollNotify.cs rename to Duplicati.Library.RestAPI/EventPollNotify.cs index fcaa5209e..777dc4a1e 100644 --- a/Duplicati/Server/EventPollNotify.cs +++ b/Duplicati.Library.RestAPI/EventPollNotify.cs @@ -1,97 +1,77 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Duplicati.Server -{ - /// - /// This class handles synchronized waiting for events - /// - public class EventPollNotify - { - /// - /// The lock that grants exclusive access to control structures - /// - private readonly object m_lock = new object(); - /// - /// The current eventID - /// - private long m_eventNo = 0; - /// - /// The list of subscribed waiting threads - /// - private readonly Queue m_waitQueue = new Queue(); - - /// - /// An eventhandler for subscribing to event updates without blocking - /// - public event EventHandler NewEvent; - - /// - /// Gets the current event ID - /// - public long EventNo { get { return m_eventNo; } } - - /// - /// Call to wait for an event that is newer than the current known event - /// - /// The last known event id - /// The number of milliseconds to block - /// The current event id - public long Wait(long eventId, int milliseconds) - { - System.Threading.ManualResetEvent mre; - lock (m_lock) - { - //If a newer event has already occured, return immediately - if (eventId != m_eventNo) - return m_eventNo; - - //Otherwise register this thread as waiting - mre = new System.Threading.ManualResetEvent(false); - m_waitQueue.Enqueue(mre); - } - - //Wait until we are signalled or the time has elapsed - mre.WaitOne(milliseconds, false); - return m_eventNo; - } - - /// - /// Signals that an event has occurred and notifies all waiting threads - /// - public void SignalNewEvent() - { - lock (m_lock) - { - m_eventNo++; - while (m_waitQueue.Count > 0) - m_waitQueue.Dequeue().Set(); - } - - if (NewEvent != null) - NewEvent(this, null); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Duplicati.Server +{ + /// + /// This class handles synchronized waiting for events + /// + public class EventPollNotify + { + /// + /// The lock that grants exclusive access to control structures + /// + private readonly object m_lock = new object(); + /// + /// The current eventID + /// + private long m_eventNo = 0; + /// + /// The list of subscribed waiting threads + /// + private readonly Queue m_waitQueue = new Queue(); + + /// + /// An eventhandler for subscribing to event updates without blocking + /// + public event EventHandler NewEvent; + + /// + /// Gets the current event ID + /// + public long EventNo { get { return m_eventNo; } } + + /// + /// Call to wait for an event that is newer than the current known event + /// + /// The last known event id + /// The number of milliseconds to block + /// The current event id + public long Wait(long eventId, int milliseconds) + { + System.Threading.ManualResetEvent mre; + lock (m_lock) + { + //If a newer event has already occured, return immediately + if (eventId != m_eventNo) + return m_eventNo; + + //Otherwise register this thread as waiting + mre = new System.Threading.ManualResetEvent(false); + m_waitQueue.Enqueue(mre); + } + + //Wait until we are signalled or the time has elapsed + mre.WaitOne(milliseconds, false); + return m_eventNo; + } + + /// + /// Signals that an event has occurred and notifies all waiting threads + /// + public void SignalNewEvent() + { + lock (m_lock) + { + m_eventNo++; + while (m_waitQueue.Count > 0) + m_waitQueue.Dequeue().Set(); + } + + if (NewEvent != null) + NewEvent(this, null); + } + } +} diff --git a/Duplicati.Library.RestAPI/FIXMEGlobal.cs b/Duplicati.Library.RestAPI/FIXMEGlobal.cs new file mode 100644 index 000000000..2a8fc3148 --- /dev/null +++ b/Duplicati.Library.RestAPI/FIXMEGlobal.cs @@ -0,0 +1,97 @@ + +using Duplicati.Server; +using System; +using System.Collections.Generic; + +namespace Duplicati.Library.RestAPI +{ + /** + * In the absense of dependancy injection, there is a significant amount of variables exposed through Program as globals. + * This causes a problem decoupling classes and leads to circular dependancies. + */ + public static class FIXMEGlobal + { + + /// + /// This is the only access to the database + /// + public static Server.Database.Connection DataConnection; + + /// + /// The controller interface for pause/resume and throttle options + /// + public static LiveControls LiveControl; + + /// + /// A delegate method for creating a copy of the current progress state + /// + public static Func GenerateProgressState; + + /// + /// The status event signaler, used to control long polling of status updates + /// + public static readonly EventPollNotify StatusEventNotifyer = new EventPollNotify(); + + /// + /// This is the working thread + /// + public static Duplicati.Library.Utility.WorkerThread WorkThread; + + public static Func PeekLastDataUpdateID; + public static Func PeekLastNotificationUpdateID; + + public static Action IncrementLastDataUpdateID; + + public static Action IncrementLastNotificationUpdateID; + + public static Action StartOrStopUsageReporter; + + public static Action UpdateThrottleSpeeds; + + /// + /// Gets the folder where Duplicati data is stored + /// + public static string DataFolder; + + /// + /// This is the scheduling thread + /// + public static Scheduler Scheduler; + + /// + /// The log redirect handler + /// + public static readonly LogWriteHandler LogHandler = new LogWriteHandler(); + + public static Func, Server.Database.Connection> GetDatabaseConnection; + + /// + /// The update poll thread. + /// + public static UpdatePollThread UpdatePoller; + + + /// + /// Used to check the origin of the web server (e.g. Tray icon or a stand alone Server) + /// + public static string Origin = "Server"; + + + /// + /// The application exit event + /// + public static System.Threading.ManualResetEvent ApplicationExitEvent; + + + /// + /// List of completed task results + /// + public static readonly List> TaskResultCache = new List>(); + + + /// + /// This is the lock to be used before manipulating the shared resources + /// + public static readonly object MainLock = new object(); + } +} diff --git a/Duplicati/Server/LiveControls.cs b/Duplicati.Library.RestAPI/LiveControls.cs similarity index 88% rename from Duplicati/Server/LiveControls.cs rename to Duplicati.Library.RestAPI/LiveControls.cs index 9ff6dba3a..1a51636a3 100644 --- a/Duplicati/Server/LiveControls.cs +++ b/Duplicati.Library.RestAPI/LiveControls.cs @@ -1,405 +1,404 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com +#region Disclaimer / License +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com // -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. // -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. // -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Text; +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// +#endregion +using System; +using System.Collections.Generic; +using System.Text; using Duplicati.Library.Common; +using Duplicati.Library.RestAPI; -namespace Duplicati.Server -{ - /// - /// This class keeps track of the users modifications regarding - /// throttling and pause/resume - /// - public class LiveControls - { - /// - /// The tag used for logging - /// - private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(); - - /// - /// An event that is activated when the pause state changes - /// - public event EventHandler StateChanged; - - /// - /// An event that is activated when the thread priority changes - /// - public event EventHandler ThreadPriorityChanged; - - /// - /// An event that is activated when the throttle speed changes - /// - public event EventHandler ThrottleSpeedChanged; - - /// - /// The possible states for the live control - /// - public enum LiveControlState - { - /// - /// Indicates that the backups are running - /// - Running, - /// - /// Indicates that the backups are currently suspended - /// - Paused - } - - /// - /// The current control state - /// - private LiveControlState m_state; - - /// - /// A value that indicates if the current pause state is caused by being suspended - /// - private bool m_pausedForSuspend = false; - - /// - /// The time to pause for, used to ensure that a user set pause can override the suspend pause - /// - private DateTime m_suspendMinimumPause = new DateTime(0); - - /// - /// Gets the current state for the control - /// - public LiveControlState State { get { return m_state; } } - - /// - /// The internal variable that tracks the the priority - /// - private System.Threading.ThreadPriority? m_priority; - - /// - /// The internal variable that tracks the upload limit - /// - private long? m_uploadLimit; - - /// - /// The internal variable that tracks the download limit - /// - private long? m_downloadLimit; - - /// - /// The object that ensures concurrent operations - /// - private readonly object m_lock = new object(); - - /// - /// Gets the current overridden thread priority - /// - public System.Threading.ThreadPriority? ThreadPriority - { - get { return m_priority; } - set - { - if (m_priority != value) - { - m_priority = value; - if (ThreadPriorityChanged != null) - ThreadPriorityChanged(this, null); - } - } - } - - /// - /// Gets the current upload limit in bps - /// - public long? UploadLimit - { - get { return m_uploadLimit; } - set - { - if (m_uploadLimit != value) - { - m_uploadLimit = value; - if (ThrottleSpeedChanged != null) - ThrottleSpeedChanged(this, null); - } - } - } - - /// - /// Gets the download limit in bps - /// - public long? DownloadLimit - { - get { return m_downloadLimit; } - set - { - if (m_downloadLimit != value) - { - m_downloadLimit = value; - if (ThrottleSpeedChanged != null) - ThrottleSpeedChanged(this, null); - } - } - } - - /// - /// The timer that is activated after a pause period. - /// - private readonly System.Threading.Timer m_waitTimer; - - /// - /// The time that the current pause is expected to expire - /// - private DateTime m_waitTimeExpiration = new DateTime(0); - - /// - /// Constructs a new instance of the LiveControl - /// - public LiveControls(Database.ServerSettings settings) - { - m_state = LiveControlState.Running; - m_waitTimer = new System.Threading.Timer(m_waitTimer_Tick, this, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); - - if (!string.IsNullOrEmpty(settings.StartupDelayDuration) && settings.StartupDelayDuration != "0") - { - long milliseconds = 0; - try { milliseconds = (long)Duplicati.Library.Utility.Timeparser.ParseTimeSpan(settings.StartupDelayDuration).TotalMilliseconds; } - catch {} - - if (milliseconds > 0) - { - m_waitTimeExpiration = DateTime.Now.AddMilliseconds(milliseconds); - m_waitTimer.Change(milliseconds, System.Threading.Timeout.Infinite); - m_state = LiveControlState.Paused; - } - } - - m_priority = settings.ThreadPriorityOverride; - if (!string.IsNullOrEmpty(settings.DownloadSpeedLimit)) - try - { - m_downloadLimit = Library.Utility.Sizeparser.ParseSize(settings.DownloadSpeedLimit, "kb"); - } - catch (Exception ex) - { - Library.Logging.Log.WriteErrorMessage(LOGTAG, "ParseDownloadLimitError", ex, "Failed to parse download limit: {0}", settings.DownloadSpeedLimit); - } - - if (!string.IsNullOrEmpty(settings.UploadSpeedLimit)) - try - { - m_uploadLimit = Library.Utility.Sizeparser.ParseSize(settings.UploadSpeedLimit, "kb"); - } - catch (Exception ex) - { - Library.Logging.Log.WriteErrorMessage(LOGTAG, "ParseUploadLimitError", ex, "Failed to parse upload limit: {0}", settings.UploadSpeedLimit); - } - - try - { - if (!Platform.IsClientPosix) - RegisterHibernateMonitor(); - } - catch { } - } - - /// - /// Event that occurs when the timeout duration is exceeded - /// - /// The sender of the event - private void m_waitTimer_Tick(object sender) - { - lock (m_lock) - Resume(); - } - - /// - /// Internal helper to reset the timeout timer - /// - /// The time to wait - private void ResetTimer(string timeout) - { - lock (m_lock) - if (!string.IsNullOrEmpty(timeout)) - { - long milliseconds = (long)Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout).TotalMilliseconds; - m_waitTimeExpiration = DateTime.Now.AddMilliseconds(milliseconds); - m_waitTimer.Change(milliseconds, System.Threading.Timeout.Infinite); - } - else - { - m_waitTimeExpiration = new DateTime(0); - m_waitTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); - } - } - - /// - /// Internal helper to set the pause mode - /// - private void SetPauseMode() - { - lock (m_lock) - { - if (m_state == LiveControlState.Running) - { - m_state = LiveControlState.Paused; - if (StateChanged != null) - StateChanged(this, null); - } - } - } - - /// - /// Pauses the backups until resumed - /// - public void Pause() - { - lock(m_lock) - { - var fireEvent = m_waitTimeExpiration.Ticks != 0 && m_state == LiveControlState.Paused && StateChanged != null; - - ResetTimer(null); - - if (fireEvent) - StateChanged(this, null); - else - SetPauseMode(); - } - } - - /// - /// Resumes a backups to the running state - /// - public void Resume() - { - lock (m_lock) - { - if (m_state == LiveControlState.Paused) - { - //Make sure that the timer is cleared - ResetTimer(null); - - m_state = LiveControlState.Running; - if (StateChanged != null) - StateChanged(this, null); - } - } - } - - /// - /// Suspends the backups for a given period - /// - /// The duration to wait - public void Pause(string timeout) - { - Pause(Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout)); - } - - /// - /// Suspends the backups for a given period - /// - /// The duration to wait - public void Pause(TimeSpan timeout) - { - lock (m_lock) - { - m_waitTimeExpiration = DateTime.Now.AddMilliseconds((long)timeout.TotalMilliseconds); - m_waitTimer.Change((long)timeout.TotalMilliseconds, System.Threading.Timeout.Infinite); - - //We change the time, so we issue a new event - if (m_state == LiveControlState.Paused && StateChanged != null) - StateChanged(this, null); - else - SetPauseMode(); - } - } - - /// - /// Gets the time the current pause is expected to end - /// - public DateTime EstimatedPauseEnd { get { return m_waitTimeExpiration; } } - - /// - /// Method for calling a Win32 API - /// - private void RegisterHibernateMonitor() - { - Microsoft.Win32.SystemEvents.PowerModeChanged += new Microsoft.Win32.PowerModeChangedEventHandler(SystemEvents_PowerModeChanged); - } - - /// - /// A monitor for detecting when the system hibernates or resumes - /// - /// Unused sender parameter - /// The event information - private void SystemEvents_PowerModeChanged(object sender, object _e) - { - Microsoft.Win32.PowerModeChangedEventArgs e = _e as Microsoft.Win32.PowerModeChangedEventArgs; - if (e == null) - return; - - if (e.Mode == Microsoft.Win32.PowerModes.Suspend) - { - //If we are running, register as being paused due to suspending - if (this.m_state == LiveControlState.Running) - { - this.SetPauseMode(); - m_pausedForSuspend = true; - m_suspendMinimumPause = new DateTime(0); - } - else - { - if (m_waitTimeExpiration.Ticks != 0) - { - m_pausedForSuspend = true; - m_suspendMinimumPause = this.EstimatedPauseEnd; - ResetTimer(null); - } - - } - } - else if (e.Mode == Microsoft.Win32.PowerModes.Resume) - { - //If we have been been paused due to suspending, we un-pause now - if (m_pausedForSuspend) - { - long delayTicks = (m_suspendMinimumPause - DateTime.Now).Ticks; - - var appset = Program.DataConnection.ApplicationSettings; - if (!string.IsNullOrEmpty(appset.StartupDelayDuration) && appset.StartupDelayDuration != "0") - try { delayTicks = Math.Max(delayTicks, Library.Utility.Timeparser.ParseTimeSpan(appset.StartupDelayDuration).Ticks); } - catch { } - - if (delayTicks > 0) - { - this.Pause(TimeSpan.FromTicks(delayTicks)); - } - else - { - this.Resume(); - } - } - - m_pausedForSuspend = false; - m_suspendMinimumPause = new DateTime(0); - } - } - - } -} +namespace Duplicati.Server +{ + /// + /// This class keeps track of the users modifications regarding + /// throttling and pause/resume + /// + public class LiveControls + { + /// + /// The tag used for logging + /// + private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(); + + /// + /// An event that is activated when the pause state changes + /// + public event EventHandler StateChanged; + + /// + /// An event that is activated when the thread priority changes + /// + public event EventHandler ThreadPriorityChanged; + + /// + /// An event that is activated when the throttle speed changes + /// + public event EventHandler ThrottleSpeedChanged; + + /// + /// The possible states for the live control + /// + public enum LiveControlState + { + /// + /// Indicates that the backups are running + /// + Running, + /// + /// Indicates that the backups are currently suspended + /// + Paused + } + + /// + /// The current control state + /// + private LiveControlState m_state; + + /// + /// A value that indicates if the current pause state is caused by being suspended + /// + private bool m_pausedForSuspend = false; + + /// + /// The time to pause for, used to ensure that a user set pause can override the suspend pause + /// + private DateTime m_suspendMinimumPause = new DateTime(0); + + /// + /// Gets the current state for the control + /// + public LiveControlState State { get { return m_state; } } + + /// + /// The internal variable that tracks the the priority + /// + private System.Threading.ThreadPriority? m_priority; + + /// + /// The internal variable that tracks the upload limit + /// + private long? m_uploadLimit; + + /// + /// The internal variable that tracks the download limit + /// + private long? m_downloadLimit; + + /// + /// The object that ensures concurrent operations + /// + private readonly object m_lock = new object(); + + /// + /// Gets the current overridden thread priority + /// + public System.Threading.ThreadPriority? ThreadPriority + { + get { return m_priority; } + set + { + if (m_priority != value) + { + m_priority = value; + if (ThreadPriorityChanged != null) + ThreadPriorityChanged(this, null); + } + } + } + + /// + /// Gets the current upload limit in bps + /// + public long? UploadLimit + { + get { return m_uploadLimit; } + set + { + if (m_uploadLimit != value) + { + m_uploadLimit = value; + if (ThrottleSpeedChanged != null) + ThrottleSpeedChanged(this, null); + } + } + } + + /// + /// Gets the download limit in bps + /// + public long? DownloadLimit + { + get { return m_downloadLimit; } + set + { + if (m_downloadLimit != value) + { + m_downloadLimit = value; + if (ThrottleSpeedChanged != null) + ThrottleSpeedChanged(this, null); + } + } + } + + /// + /// The timer that is activated after a pause period. + /// + private readonly System.Threading.Timer m_waitTimer; + + /// + /// The time that the current pause is expected to expire + /// + private DateTime m_waitTimeExpiration = new DateTime(0); + + /// + /// Constructs a new instance of the LiveControl + /// + public LiveControls(Database.ServerSettings settings) + { + m_state = LiveControlState.Running; + m_waitTimer = new System.Threading.Timer(m_waitTimer_Tick, this, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); + + if (!string.IsNullOrEmpty(settings.StartupDelayDuration) && settings.StartupDelayDuration != "0") + { + long milliseconds = 0; + try { milliseconds = (long)Duplicati.Library.Utility.Timeparser.ParseTimeSpan(settings.StartupDelayDuration).TotalMilliseconds; } + catch {} + + if (milliseconds > 0) + { + m_waitTimeExpiration = DateTime.Now.AddMilliseconds(milliseconds); + m_waitTimer.Change(milliseconds, System.Threading.Timeout.Infinite); + m_state = LiveControlState.Paused; + } + } + + m_priority = settings.ThreadPriorityOverride; + if (!string.IsNullOrEmpty(settings.DownloadSpeedLimit)) + try + { + m_downloadLimit = Library.Utility.Sizeparser.ParseSize(settings.DownloadSpeedLimit, "kb"); + } + catch (Exception ex) + { + Library.Logging.Log.WriteErrorMessage(LOGTAG, "ParseDownloadLimitError", ex, "Failed to parse download limit: {0}", settings.DownloadSpeedLimit); + } + + if (!string.IsNullOrEmpty(settings.UploadSpeedLimit)) + try + { + m_uploadLimit = Library.Utility.Sizeparser.ParseSize(settings.UploadSpeedLimit, "kb"); + } + catch (Exception ex) + { + Library.Logging.Log.WriteErrorMessage(LOGTAG, "ParseUploadLimitError", ex, "Failed to parse upload limit: {0}", settings.UploadSpeedLimit); + } + + try + { + if (!Platform.IsClientPosix) + RegisterHibernateMonitor(); + } + catch { } + } + + /// + /// Event that occurs when the timeout duration is exceeded + /// + /// The sender of the event + private void m_waitTimer_Tick(object sender) + { + lock (m_lock) + Resume(); + } + + /// + /// Internal helper to reset the timeout timer + /// + /// The time to wait + private void ResetTimer(string timeout) + { + lock (m_lock) + if (!string.IsNullOrEmpty(timeout)) + { + long milliseconds = (long)Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout).TotalMilliseconds; + m_waitTimeExpiration = DateTime.Now.AddMilliseconds(milliseconds); + m_waitTimer.Change(milliseconds, System.Threading.Timeout.Infinite); + } + else + { + m_waitTimeExpiration = new DateTime(0); + m_waitTimer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); + } + } + + /// + /// Internal helper to set the pause mode + /// + private void SetPauseMode() + { + lock (m_lock) + { + if (m_state == LiveControlState.Running) + { + m_state = LiveControlState.Paused; + if (StateChanged != null) + StateChanged(this, null); + } + } + } + + /// + /// Pauses the backups until resumed + /// + public void Pause() + { + lock(m_lock) + { + var fireEvent = m_waitTimeExpiration.Ticks != 0 && m_state == LiveControlState.Paused && StateChanged != null; + + ResetTimer(null); + + if (fireEvent) + StateChanged(this, null); + else + SetPauseMode(); + } + } + + /// + /// Resumes a backups to the running state + /// + public void Resume() + { + lock (m_lock) + { + if (m_state == LiveControlState.Paused) + { + //Make sure that the timer is cleared + ResetTimer(null); + + m_state = LiveControlState.Running; + if (StateChanged != null) + StateChanged(this, null); + } + } + } + + /// + /// Suspends the backups for a given period + /// + /// The duration to wait + public void Pause(string timeout) + { + Pause(Duplicati.Library.Utility.Timeparser.ParseTimeSpan(timeout)); + } + + /// + /// Suspends the backups for a given period + /// + /// The duration to wait + public void Pause(TimeSpan timeout) + { + lock (m_lock) + { + m_waitTimeExpiration = DateTime.Now.AddMilliseconds((long)timeout.TotalMilliseconds); + m_waitTimer.Change((long)timeout.TotalMilliseconds, System.Threading.Timeout.Infinite); + + //We change the time, so we issue a new event + if (m_state == LiveControlState.Paused && StateChanged != null) + StateChanged(this, null); + else + SetPauseMode(); + } + } + + /// + /// Gets the time the current pause is expected to end + /// + public DateTime EstimatedPauseEnd { get { return m_waitTimeExpiration; } } + + /// + /// Method for calling a Win32 API + /// + private void RegisterHibernateMonitor() + { + Microsoft.Win32.SystemEvents.PowerModeChanged += new Microsoft.Win32.PowerModeChangedEventHandler(SystemEvents_PowerModeChanged); + } + + /// + /// A monitor for detecting when the system hibernates or resumes + /// + /// Unused sender parameter + /// The event information + private void SystemEvents_PowerModeChanged(object sender, object _e) + { + Microsoft.Win32.PowerModeChangedEventArgs e = _e as Microsoft.Win32.PowerModeChangedEventArgs; + if (e == null) + return; + + if (e.Mode == Microsoft.Win32.PowerModes.Suspend) + { + //If we are running, register as being paused due to suspending + if (this.m_state == LiveControlState.Running) + { + this.SetPauseMode(); + m_pausedForSuspend = true; + m_suspendMinimumPause = new DateTime(0); + } + else + { + if (m_waitTimeExpiration.Ticks != 0) + { + m_pausedForSuspend = true; + m_suspendMinimumPause = this.EstimatedPauseEnd; + ResetTimer(null); + } + + } + } + else if (e.Mode == Microsoft.Win32.PowerModes.Resume) + { + //If we have been been paused due to suspending, we un-pause now + if (m_pausedForSuspend) + { + long delayTicks = (m_suspendMinimumPause - DateTime.Now).Ticks; + + var appset = FIXMEGlobal.DataConnection.ApplicationSettings; + if (!string.IsNullOrEmpty(appset.StartupDelayDuration) && appset.StartupDelayDuration != "0") + try { delayTicks = Math.Max(delayTicks, Library.Utility.Timeparser.ParseTimeSpan(appset.StartupDelayDuration).Ticks); } + catch { } + + if (delayTicks > 0) + { + this.Pause(TimeSpan.FromTicks(delayTicks)); + } + else + { + this.Resume(); + } + } + + m_pausedForSuspend = false; + m_suspendMinimumPause = new DateTime(0); + } + } + + } +} diff --git a/Duplicati/Server/LogWriteHandler.cs b/Duplicati.Library.RestAPI/LogWriteHandler.cs similarity index 87% rename from Duplicati/Server/LogWriteHandler.cs rename to Duplicati.Library.RestAPI/LogWriteHandler.cs index c6b605abb..640886f66 100644 --- a/Duplicati/Server/LogWriteHandler.cs +++ b/Duplicati.Library.RestAPI/LogWriteHandler.cs @@ -1,24 +1,20 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using System.Linq; using Duplicati.Library.Logging; diff --git a/Duplicati/Server/WebServer/RESTMethods/Acknowledgements.cs b/Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/Acknowledgements.cs rename to Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs index 12b45e612..f9b558c5b 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Acknowledgements.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Acknowledgements.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Collections.Generic; using Duplicati.Library.Common.IO; diff --git a/Duplicati/Server/WebServer/RESTMethods/Backup.cs b/Duplicati.Library.RestAPI/RESTMethods/Backup.cs similarity index 87% rename from Duplicati/Server/WebServer/RESTMethods/Backup.cs rename to Duplicati.Library.RestAPI/RESTMethods/Backup.cs index dd06d9a45..e18730fea 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Backup.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Backup.cs @@ -1,737 +1,742 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using Duplicati.Server.Serialization; -using System.IO; -using System.Linq; -using Duplicati.Server.Serialization.Interface; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Backup : IRESTMethodGET, IRESTMethodPUT, IRESTMethodPOST, IRESTMethodDELETE, IRESTMethodDocumented - { - public class GetResponse - { - public class GetResponseData - { - public Serialization.Interface.ISchedule Schedule; - public Serialization.Interface.IBackup Backup; - public Dictionary DisplayNames; - } - - public bool success; - - public GetResponseData data; - } - - private void SearchFiles(IBackup backup, string filterstring, RequestInfo info) - { - var filter = filterstring; - var timestring = info.Request.QueryString["time"].Value; - var allversion = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["all-versions"].Value, false); - - if (string.IsNullOrWhiteSpace(timestring) && !allversion) - { - info.ReportClientError("Invalid or missing time", System.Net.HttpStatusCode.BadRequest); - return; - } - - var prefixonly = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["prefix-only"].Value, false); - var foldercontents = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["folder-contents"].Value, false); - var time = new DateTime(); - if (!allversion) - time = Duplicati.Library.Utility.Timeparser.ParseTimeInterval(timestring, DateTime.Now); - - var r = Runner.Run(Runner.CreateListTask(backup, new string[] { filter }, prefixonly, allversion, foldercontents, time), false) as Duplicati.Library.Interface.IListResults; - - var result = new Dictionary(); - - foreach(HttpServer.HttpInputItem n in info.Request.QueryString) - result[n.Name] = n.Value; - - result["Filesets"] = r.Filesets; - result["Files"] = r.Files - // Group directories first - support either directory separator here as we may be restoring data from an alternate platform - .OrderByDescending(f => (f.Path.StartsWith("/", StringComparison.Ordinal) && f.Path.EndsWith("/", StringComparison.Ordinal)) || (!f.Path.StartsWith("/", StringComparison.Ordinal) && f.Path.EndsWith("\\", StringComparison.Ordinal))) - // Sort both groups (directories and files) alphabetically - .ThenBy(f => f.Path); - - info.OutputOK(result); - - } - - private void ListFileSets(IBackup backup, RequestInfo info) - { - var input = info.Request.QueryString; - var extra = new Dictionary - { - ["list-sets-only"] = "true" - }; - if (input["include-metadata"].Value != null) - extra["list-sets-only"] = (!Library.Utility.Utility.ParseBool(input["include-metadata"].Value, false)).ToString(); - if (input["from-remote-only"].Value != null) - extra["no-local-db"] = Library.Utility.Utility.ParseBool(input["from-remote-only"].Value, false).ToString(); - - var r = Runner.Run(Runner.CreateTask(DuplicatiOperation.List, backup, extra), false) as Duplicati.Library.Interface.IListResults; - - if (r.EncryptedFiles && backup.Settings.Any(x => string.Equals("--no-encryption", x.Name, StringComparison.OrdinalIgnoreCase))) - info.ReportServerError("encrypted-storage"); - else - info.OutputOK(r.Filesets); - } - - private void FetchLogData(IBackup backup, RequestInfo info) - { - using(var con = Duplicati.Library.SQLiteHelper.SQLiteLoader.LoadConnection(backup.DBPath)) - using(var cmd = con.CreateCommand()) - info.OutputOK(LogData.DumpTable(cmd, "LogData", "ID", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value)); - } - - private void FetchRemoteLogData(IBackup backup, RequestInfo info) - { - using(var con = Duplicati.Library.SQLiteHelper.SQLiteLoader.LoadConnection(backup.DBPath)) - using(var cmd = con.CreateCommand()) - { - var dt = LogData.DumpTable(cmd, "RemoteOperation", "ID", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value); - - // Unwrap raw data to a string - foreach(var n in dt) - try { n["Data"] = System.Text.Encoding.UTF8.GetString((byte[])n["Data"]); } - catch { } - - info.OutputOK(dt); - } - } - private void IsDBUsedElseWhere(IBackup backup, RequestInfo info) - { - info.OutputOK(new { inuse = Library.Main.DatabaseLocator.IsDatabasePathInUse(backup.DBPath) }); - } - - public static void RemovePasswords(IBackup backup) - { - backup.SanitizeSettings(); - backup.SanitizeTargetUrl(); - } - - private void Export(IBackup backup, RequestInfo info) - { - var cmdline = Library.Utility.Utility.ParseBool(info.Request.QueryString["cmdline"].Value, false); - var argsonly = Library.Utility.Utility.ParseBool(info.Request.QueryString["argsonly"].Value, false); - var exportPasswords = Library.Utility.Utility.ParseBool(info.Request.QueryString["export-passwords"].Value, false); - if (!exportPasswords) - { - Backup.RemovePasswords(backup); - } - - if (cmdline) - { - info.OutputOK(new { Command = Runner.GetCommandLine(Runner.CreateTask(DuplicatiOperation.Backup, backup)) }); - } - else if (argsonly) - { - var parts = Runner.GetCommandLineParts(Runner.CreateTask(DuplicatiOperation.Backup, backup)); - - info.OutputOK(new { - Backend = parts.First(), - Arguments = parts.Skip(1).Where(x => !x.StartsWith("--", StringComparison.Ordinal)), - Options = parts.Skip(1).Where(x => x.StartsWith("--", StringComparison.Ordinal)) - }); - } - else - { - var passphrase = info.Request.QueryString["passphrase"].Value; - byte[] data = Backup.ExportToJSON(backup, passphrase); - - string filename = Library.Utility.Uri.UrlEncode(backup.Name) + "-duplicati-config.json"; - if (!string.IsNullOrWhiteSpace(passphrase)) - { - filename += ".aes"; - } - - info.Response.ContentLength = data.Length; - info.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", filename)); - info.Response.ContentType = "application/octet-stream"; - - info.BodyWriter.SetOK(); - info.Response.SendHeaders(); - info.Response.SendBody(data); - } - } - - public static byte[] ExportToJSON(IBackup backup, string passphrase) - { - Serializable.ImportExportStructure ipx = Program.DataConnection.PrepareBackupForExport(backup); - - byte[] data; - using (MemoryStream ms = new System.IO.MemoryStream()) - { - using (StreamWriter sw = new System.IO.StreamWriter(ms)) - { - Serializer.SerializeJson(sw, ipx, true); - - if (!string.IsNullOrWhiteSpace(passphrase)) - { - ms.Position = 0; - using (MemoryStream ms2 = new System.IO.MemoryStream()) - { - using (Library.Encryption.AESEncryption m = new Duplicati.Library.Encryption.AESEncryption(passphrase, new Dictionary())) - { - m.Encrypt(ms, ms2); - data = ms2.ToArray(); - } - } - } - else - { - data = ms.ToArray(); - } - } - } - - return data; - } - - private void RestoreFiles(IBackup backup, RequestInfo info) - { - var input = info.Request.Form; - - string[] filters = parsePaths(input["paths"].Value ?? string.Empty); - - var passphrase = string.IsNullOrEmpty(input["passphrase"].Value) ? null : input["passphrase"].Value; - - var time = Duplicati.Library.Utility.Timeparser.ParseTimeInterval(input["time"].Value, DateTime.Now); - var restoreTarget = input["restore-path"].Value; - var overwrite = Duplicati.Library.Utility.Utility.ParseBool(input["overwrite"].Value, false); - - var permissions = Duplicati.Library.Utility.Utility.ParseBool(input["permissions"].Value, false); - var skip_metadata = Duplicati.Library.Utility.Utility.ParseBool(input["skip-metadata"].Value, false); - - var task = Runner.CreateRestoreTask(backup, filters, time, restoreTarget, overwrite, permissions, skip_metadata, passphrase); - - Program.WorkThread.AddTask(task); - - info.OutputOK(new { TaskID = task.TaskID }); - } - - private void CreateReport(IBackup backup, RequestInfo info) - { - var task = Runner.CreateTask(DuplicatiOperation.CreateReport, backup); - Program.WorkThread.AddTask(task); - Program.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new { Status = "OK", ID = task.TaskID }); - } - - private void ReportRemoteSize(IBackup backup, RequestInfo info) - { - var task = Runner.CreateTask(DuplicatiOperation.ListRemote, backup); - Program.WorkThread.AddTask(task); - Program.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new { Status = "OK", ID = task.TaskID }); - } - - private void Repair(IBackup backup, RequestInfo info) - { - DoRepair(backup, info, false); - } - - private void RepairUpdate(IBackup backup, RequestInfo info) - { - DoRepair(backup, info, true); - } - - private void Vacuum(IBackup backup, RequestInfo info) - { - var task = Runner.CreateTask(DuplicatiOperation.Vacuum, backup); - Program.WorkThread.AddTask(task); - Program.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new { Status = "OK", ID = task.TaskID }); - } - - private void Verify(IBackup backup, RequestInfo info) - { - var task = Runner.CreateTask(DuplicatiOperation.Verify, backup); - Program.WorkThread.AddTask(task); - Program.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new {Status = "OK", ID = task.TaskID}); - } - - private void Compact(IBackup backup, RequestInfo info) - { - var task = Runner.CreateTask(DuplicatiOperation.Compact, backup); - Program.WorkThread.AddTask(task); - Program.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new { Status = "OK", ID = task.TaskID }); - } - - private string[] parsePaths(string paths) - { - string[] filters; - var rawpaths = (paths ?? string.Empty).Trim(); - - // We send the file list as a JSON array to avoid encoding issues with the path separator - // as it is an allowed character in file and path names. - // We also accept the old way, for compatibility with the greeno theme - if (!string.IsNullOrWhiteSpace(rawpaths) && rawpaths.StartsWith("[", StringComparison.Ordinal) && rawpaths.EndsWith("]", StringComparison.Ordinal)) - filters = Newtonsoft.Json.JsonConvert.DeserializeObject(rawpaths); - else - filters = paths.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries); - - return filters; - } - - private void DoRepair(IBackup backup, RequestInfo info, bool repairUpdate) - { - var input = info.Request.Form; - string[] filters = null; - var extra = new Dictionary(); - if (input["only-paths"].Value != null) - extra["repair-only-paths"] = (Library.Utility.Utility.ParseBool(input["only-paths"].Value, false)).ToString(); - if (input["time"].Value != null) - extra["time"] = input["time"].Value; - if (input["version"].Value != null) - extra["version"] = input["version"].Value; - if (input["paths"].Value != null) - filters = parsePaths(input["paths"].Value); - - var task = Runner.CreateTask(repairUpdate ? DuplicatiOperation.RepairUpdate : DuplicatiOperation.Repair, backup, extra, filters); - Program.WorkThread.AddTask(task); - Program.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new {Status = "OK", ID = task.TaskID}); - } - - private void RunBackup(IBackup backup, RequestInfo info) - { - var t = Program.WorkThread.CurrentTask; - var bt = t == null ? null : t.Backup; - if (bt != null && backup.ID == bt.ID) - { - // Already running - } - else if (Program.WorkThread.CurrentTasks.Any(x => { - var bn = x?.Backup; - return bn == null || bn.ID == backup.ID; - })) - { - // Already in queue - } - else - { - Program.WorkThread.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup), true); - Program.StatusEventNotifyer.SignalNewEvent(); - } - - info.OutputOK(); - } - - private void IsActive(IBackup backup, RequestInfo info) - { - var t = Program.WorkThread.CurrentTask; - var bt = t?.Backup; - if (bt != null && backup.ID == bt.ID) - { - info.OutputOK(new { Status = "OK", Active = true }); - return; - } - else if (Program.WorkThread.CurrentTasks.Any(x => - { - var bn = x?.Backup; - return bn == null || bn.ID == backup.ID; - })) - { - info.OutputOK(new { Status = "OK", Active = true }); - return; - } - else - { - info.OutputOK(new { Status = "OK", Active = false }); - return; - } - } - - private void UpdateDatabasePath(IBackup backup, RequestInfo info, bool move) - { - var np = info.Request.Form["path"].Value; - if (string.IsNullOrWhiteSpace(np)) - info.ReportClientError("No target path supplied", System.Net.HttpStatusCode.BadRequest); - else if (!Path.IsPathRooted(np)) - info.ReportClientError("Target path is relative, please supply a fully qualified path", System.Net.HttpStatusCode.BadRequest); - else - { - if (move && (File.Exists(np) || Directory.Exists(np))) - info.ReportClientError("A file already exists at the new location", System.Net.HttpStatusCode.Conflict); - else - { - if (move) - File.Move(backup.DBPath, np); - - Program.DataConnection.UpdateBackupDBPath(backup, np); - } - - } - - } - - public void GET(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }, 2); - var bk = Program.DataConnection.GetBackup(parts.First()); - if (bk == null) - info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound); - else - { - if (parts.Length > 1) - { - var operation = parts.Last().Split(new char[] {'/'}).First().ToLowerInvariant(); - - switch (operation) - { - case "files": - var filter = parts.Last().Split(new char[] { '/' }, 2).Skip(1).FirstOrDefault(); - if (!string.IsNullOrWhiteSpace(info.Request.QueryString["filter"].Value)) - filter = info.Request.QueryString["filter"].Value; - SearchFiles(bk, filter, info); - return; - case "log": - FetchLogData(bk, info); - return; - case "remotelog": - FetchRemoteLogData(bk, info); - return; - case "filesets": - ListFileSets(bk, info); - return; - case "export": - Export(bk, info); - return; - case "isdbusedelsewhere": - IsDBUsedElseWhere(bk, info); - return; - case "isactive": - IsActive(bk, info); - return; - default: - info.ReportClientError(string.Format("Invalid component: {0}", operation), System.Net.HttpStatusCode.BadRequest); - return; - } - - } - - var scheduleId = Program.DataConnection.GetScheduleIDsFromTags(new string[] { "ID=" + bk.ID }); - var schedule = scheduleId.Any() ? Program.DataConnection.GetSchedule(scheduleId.First()) : null; - var sourcenames = SpecialFolders.GetSourceNames(bk); - - //TODO: Filter out the password in both settings and the target url - - info.OutputOK(new GetResponse() - { - success = true, - data = new GetResponse.GetResponseData { - Schedule = schedule, - Backup = bk, - DisplayNames = sourcenames - } - }); - } - } - - public void POST(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }, 2); - var bk = Program.DataConnection.GetBackup(parts.First()); - if (bk == null) - info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound); - else - { - if (parts.Length > 1) - { - var operation = parts.Last().Split(new char[] { '/' }).First().ToLowerInvariant(); - - switch (operation) - { - case "deletedb": - System.IO.File.Delete(bk.DBPath); - info.OutputOK(); - return; - - case "movedb": - UpdateDatabasePath(bk, info, true); - return; - - case "updatedb": - UpdateDatabasePath(bk, info, false); - return; - - case "restore": - RestoreFiles(bk, info); - return; - - case "createreport": - CreateReport(bk, info); - return; - - case "repair": - Repair(bk, info); - return; - - case "repairupdate": - RepairUpdate(bk, info); - return; - - case "vacuum": - Vacuum(bk, info); - return; - - case "verify": - Verify(bk, info); - return; - - case "compact": - Compact(bk, info); - return; - - case "start": - case "run": - RunBackup(bk, info); - return; - - case "report-remote-size": - ReportRemoteSize(bk, info); - return; - - case "copytotemp": - var ipx = Serializer.Deserialize(new StringReader(Newtonsoft.Json.JsonConvert.SerializeObject(bk))); - - using(var tf = new Duplicati.Library.Utility.TempFile()) - ipx.DBPath = tf; - ipx.ID = null; - - info.OutputOK(new { status = "OK", ID = Program.DataConnection.RegisterTemporaryBackup(ipx) }); - return; - } - } - - info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest); - } - } - - public void PUT(string key, RequestInfo info) - { - string str = info.Request.Form["data"].Value; - if (string.IsNullOrWhiteSpace(str)) - str = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8).ReadToEnd(); - - if (string.IsNullOrWhiteSpace(str)) - { - info.ReportClientError("Missing backup object", System.Net.HttpStatusCode.BadRequest); - return; - } - - Backups.AddOrUpdateBackupData data = null; - try - { - data = Serializer.Deserialize(new StringReader(str)); - if (data.Backup == null) - { - info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest); - return; - } - - if (!string.IsNullOrEmpty(key)) - data.Backup.ID = key; - - if (string.IsNullOrEmpty(data.Backup.ID)) - { - info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.BadRequest); - return; - } - - - if (data.Backup.IsTemporary) - { - var backup = Program.DataConnection.GetBackup(data.Backup.ID); - if (backup.IsTemporary) - throw new InvalidDataException("External is temporary but internal is not?"); - - Program.DataConnection.UpdateTemporaryBackup(backup); - info.OutputOK(); - } - else - { - lock(Program.DataConnection.m_lock) - { - var backup = Program.DataConnection.GetBackup(data.Backup.ID); - if (backup == null) - { - info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound); - return; - } - - if (Program.DataConnection.Backups.Any(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase) && x.ID != data.Backup.ID)) - { - info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict); - return; - } - - var err = Program.DataConnection.ValidateBackup(data.Backup, data.Schedule); - if (!string.IsNullOrWhiteSpace(err)) - { - info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest); - return; - } - - //TODO: Merge in real passwords where the placeholder is found - Program.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule); - - } - - info.OutputOK(); - } - } - catch (Exception ex) - { - if (data == null) - info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest); - else - info.ReportClientError(string.Format("Unable to save backup or schedule: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError); - } - } - - public void DELETE(string key, RequestInfo info) - { - var backup = Program.DataConnection.GetBackup(key); - if (backup == null) - { - info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound); - return; - } - - var delete_remote_files = Library.Utility.Utility.ParseBool(info.Request.Param["delete-remote-files"].Value, false); - - if (delete_remote_files) - { - var captcha_token = info.Request.Param["captcha-token"].Value; - var captcha_answer = info.Request.Param["captcha-answer"].Value; - if (string.IsNullOrWhiteSpace(captcha_token) || string.IsNullOrWhiteSpace(captcha_answer)) - { - info.ReportClientError("Missing captcha", System.Net.HttpStatusCode.Unauthorized); - return; - } - - if (!Captcha.SolvedCaptcha(captcha_token, "DELETE /backup/" + backup.ID, captcha_answer)) - { - info.ReportClientError("Invalid captcha", System.Net.HttpStatusCode.Forbidden); - return; - } - } - - if (Program.WorkThread.Active) - { - try - { - //TODO: It's not safe to access the values like this, - //because the runner thread might interfere - var nt = Program.WorkThread.CurrentTask; - if (backup.Equals(nt == null ? null : nt.Backup)) - { - bool force; - if (!bool.TryParse(info.Request.QueryString["force"].Value, out force)) - force = false; - - if (!force) - { - info.OutputError(new { status = "failed", reason = "backup-in-progress" }); - return; - } - - bool hasPaused = Program.LiveControl.State == LiveControls.LiveControlState.Paused; - Program.LiveControl.Pause(); - - for(int i = 0; i < 10; i++) - if (Program.WorkThread.Active) - { - var t = Program.WorkThread.CurrentTask; - if (backup.Equals(t == null ? null : t.Backup)) - System.Threading.Thread.Sleep(1000); - else - break; - } - else - break; - - if (Program.WorkThread.Active) - { - var t = Program.WorkThread.CurrentTask; - if (backup.Equals(t == null ? null : t.Backup)) - { - if (hasPaused) - Program.LiveControl.Resume(); - info.OutputError(new { status = "failed", reason = "backup-unstoppable" }); - return; - } - } - - if (hasPaused) - Program.LiveControl.Resume(); - } - } - catch (Exception ex) - { - info.OutputError(new { status = "error", message = ex.Message }); - return; - } - } - - var extra = new Dictionary(); - if (!string.IsNullOrWhiteSpace(info.Request.Param["delete-local-db"].Value)) - extra["delete-local-db"] = info.Request.Param["delete-local-db"].Value; - if (delete_remote_files) - extra["delete-remote-files"] = "true"; - - var task = Runner.CreateTask(DuplicatiOperation.Delete, backup, extra); - Program.WorkThread.AddTask(task); - Program.StatusEventNotifyer.SignalNewEvent(); - - info.OutputOK(new { Status = "OK", ID = task.TaskID }); - } - public string Description { get { return "Retrieves, updates or deletes an existing backup and schedule"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(GetResponse)), - new KeyValuePair(HttpServer.Method.Put, typeof(Backups.AddOrUpdateBackupData)), - new KeyValuePair(HttpServer.Method.Delete, typeof(long)) - }; - } - } - } -} - +// Copyright (C) 2024, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +using System; +using System.Collections.Generic; +using Duplicati.Server.Serialization; +using System.IO; +using System.Linq; +using Duplicati.Server.Serialization.Interface; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Backup : IRESTMethodGET, IRESTMethodPUT, IRESTMethodPOST, IRESTMethodDELETE, IRESTMethodDocumented + { + public class GetResponse + { + public class GetResponseData + { + public Serialization.Interface.ISchedule Schedule; + public Serialization.Interface.IBackup Backup; + public Dictionary DisplayNames; + } + + public bool success; + + public GetResponseData data; + } + + private void SearchFiles(IBackup backup, string filterstring, RequestInfo info) + { + var filter = filterstring; + var timestring = info.Request.QueryString["time"].Value; + var allversion = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["all-versions"].Value, false); + + if (string.IsNullOrWhiteSpace(timestring) && !allversion) + { + info.ReportClientError("Invalid or missing time", System.Net.HttpStatusCode.BadRequest); + return; + } + + var prefixonly = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["prefix-only"].Value, false); + var foldercontents = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["folder-contents"].Value, false); + var time = new DateTime(); + if (!allversion) + time = Duplicati.Library.Utility.Timeparser.ParseTimeInterval(timestring, DateTime.Now); + + var r = Runner.Run(Runner.CreateListTask(backup, new string[] { filter }, prefixonly, allversion, foldercontents, time), false) as Duplicati.Library.Interface.IListResults; + + var result = new Dictionary(); + + foreach(HttpServer.HttpInputItem n in info.Request.QueryString) + result[n.Name] = n.Value; + + result["Filesets"] = r.Filesets; + result["Files"] = r.Files + // Group directories first - support either directory separator here as we may be restoring data from an alternate platform + .OrderByDescending(f => (f.Path.StartsWith("/", StringComparison.Ordinal) && f.Path.EndsWith("/", StringComparison.Ordinal)) || (!f.Path.StartsWith("/", StringComparison.Ordinal) && f.Path.EndsWith("\\", StringComparison.Ordinal))) + // Sort both groups (directories and files) alphabetically + .ThenBy(f => f.Path); + + info.OutputOK(result); + + } + + private void ListFileSets(IBackup backup, RequestInfo info) + { + var input = info.Request.QueryString; + var extra = new Dictionary + { + ["list-sets-only"] = "true" + }; + if (input["include-metadata"].Value != null) + extra["list-sets-only"] = (!Library.Utility.Utility.ParseBool(input["include-metadata"].Value, false)).ToString(); + if (input["from-remote-only"].Value != null) + extra["no-local-db"] = Library.Utility.Utility.ParseBool(input["from-remote-only"].Value, false).ToString(); + + var r = Runner.Run(Runner.CreateTask(DuplicatiOperation.List, backup, extra), false) as Duplicati.Library.Interface.IListResults; + + if (r.EncryptedFiles && backup.Settings.Any(x => string.Equals("--no-encryption", x.Name, StringComparison.OrdinalIgnoreCase))) + info.ReportServerError("encrypted-storage"); + else + info.OutputOK(r.Filesets); + } + + private void FetchLogData(IBackup backup, RequestInfo info) + { + using(var con = Duplicati.Library.SQLiteHelper.SQLiteLoader.LoadConnection(backup.DBPath)) + using(var cmd = con.CreateCommand()) + info.OutputOK(LogData.DumpTable(cmd, "LogData", "ID", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value)); + } + + private void FetchRemoteLogData(IBackup backup, RequestInfo info) + { + using(var con = Duplicati.Library.SQLiteHelper.SQLiteLoader.LoadConnection(backup.DBPath)) + using(var cmd = con.CreateCommand()) + { + var dt = LogData.DumpTable(cmd, "RemoteOperation", "ID", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value); + + // Unwrap raw data to a string + foreach(var n in dt) + try { n["Data"] = System.Text.Encoding.UTF8.GetString((byte[])n["Data"]); } + catch { } + + info.OutputOK(dt); + } + } + private void IsDBUsedElseWhere(IBackup backup, RequestInfo info) + { + info.OutputOK(new { inuse = Library.Main.DatabaseLocator.IsDatabasePathInUse(backup.DBPath) }); + } + + public static void RemovePasswords(IBackup backup) + { + backup.SanitizeSettings(); + backup.SanitizeTargetUrl(); + } + + private void Export(IBackup backup, RequestInfo info) + { + var cmdline = Library.Utility.Utility.ParseBool(info.Request.QueryString["cmdline"].Value, false); + var argsonly = Library.Utility.Utility.ParseBool(info.Request.QueryString["argsonly"].Value, false); + var exportPasswords = Library.Utility.Utility.ParseBool(info.Request.QueryString["export-passwords"].Value, false); + if (!exportPasswords) + { + Backup.RemovePasswords(backup); + } + + if (cmdline) + { + info.OutputOK(new { Command = Runner.GetCommandLine(Runner.CreateTask(DuplicatiOperation.Backup, backup)) }); + } + else if (argsonly) + { + var parts = Runner.GetCommandLineParts(Runner.CreateTask(DuplicatiOperation.Backup, backup)); + + info.OutputOK(new { + Backend = parts.First(), + Arguments = parts.Skip(1).Where(x => !x.StartsWith("--", StringComparison.Ordinal)), + Options = parts.Skip(1).Where(x => x.StartsWith("--", StringComparison.Ordinal)) + }); + } + else + { + var passphrase = info.Request.QueryString["passphrase"].Value; + byte[] data = Backup.ExportToJSON(backup, passphrase); + + string filename = Library.Utility.Uri.UrlEncode(backup.Name) + "-duplicati-config.json"; + if (!string.IsNullOrWhiteSpace(passphrase)) + { + filename += ".aes"; + } + + info.Response.ContentLength = data.Length; + info.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", filename)); + info.Response.ContentType = "application/octet-stream"; + + info.BodyWriter.SetOK(); + info.Response.SendHeaders(); + info.Response.SendBody(data); + } + } + + public static byte[] ExportToJSON(IBackup backup, string passphrase) + { + Serializable.ImportExportStructure ipx = FIXMEGlobal.DataConnection.PrepareBackupForExport(backup); + + byte[] data; + using (MemoryStream ms = new System.IO.MemoryStream()) + { + using (StreamWriter sw = new System.IO.StreamWriter(ms)) + { + Serializer.SerializeJson(sw, ipx, true); + + if (!string.IsNullOrWhiteSpace(passphrase)) + { + ms.Position = 0; + using (MemoryStream ms2 = new System.IO.MemoryStream()) + { + using (Library.Encryption.AESEncryption m = new Duplicati.Library.Encryption.AESEncryption(passphrase, new Dictionary())) + { + m.Encrypt(ms, ms2); + data = ms2.ToArray(); + } + } + } + else + { + data = ms.ToArray(); + } + } + } + + return data; + } + + private void RestoreFiles(IBackup backup, RequestInfo info) + { + var input = info.Request.Form; + + string[] filters = parsePaths(input["paths"].Value ?? string.Empty); + + var passphrase = string.IsNullOrEmpty(input["passphrase"].Value) ? null : input["passphrase"].Value; + + var time = Duplicati.Library.Utility.Timeparser.ParseTimeInterval(input["time"].Value, DateTime.Now); + var restoreTarget = input["restore-path"].Value; + var overwrite = Duplicati.Library.Utility.Utility.ParseBool(input["overwrite"].Value, false); + + var permissions = Duplicati.Library.Utility.Utility.ParseBool(input["permissions"].Value, false); + var skip_metadata = Duplicati.Library.Utility.Utility.ParseBool(input["skip-metadata"].Value, false); + + var task = Runner.CreateRestoreTask(backup, filters, time, restoreTarget, overwrite, permissions, skip_metadata, passphrase); + + FIXMEGlobal.WorkThread.AddTask(task); + + info.OutputOK(new { TaskID = task.TaskID }); + } + + private void CreateReport(IBackup backup, RequestInfo info) + { + var task = Runner.CreateTask(DuplicatiOperation.CreateReport, backup); + FIXMEGlobal.WorkThread.AddTask(task); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + info.OutputOK(new { Status = "OK", ID = task.TaskID }); + } + + private void ReportRemoteSize(IBackup backup, RequestInfo info) + { + var task = Runner.CreateTask(DuplicatiOperation.ListRemote, backup); + FIXMEGlobal.WorkThread.AddTask(task); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + info.OutputOK(new { Status = "OK", ID = task.TaskID }); + } + + private void Repair(IBackup backup, RequestInfo info) + { + DoRepair(backup, info, false); + } + + private void RepairUpdate(IBackup backup, RequestInfo info) + { + DoRepair(backup, info, true); + } + + private void Vacuum(IBackup backup, RequestInfo info) + { + var task = Runner.CreateTask(DuplicatiOperation.Vacuum, backup); + FIXMEGlobal.WorkThread.AddTask(task); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + info.OutputOK(new { Status = "OK", ID = task.TaskID }); + } + + private void Verify(IBackup backup, RequestInfo info) + { + var task = Runner.CreateTask(DuplicatiOperation.Verify, backup); + FIXMEGlobal.WorkThread.AddTask(task); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + info.OutputOK(new {Status = "OK", ID = task.TaskID}); + } + + private void Compact(IBackup backup, RequestInfo info) + { + var task = Runner.CreateTask(DuplicatiOperation.Compact, backup); + FIXMEGlobal.WorkThread.AddTask(task); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + info.OutputOK(new { Status = "OK", ID = task.TaskID }); + } + + private string[] parsePaths(string paths) + { + string[] filters; + var rawpaths = (paths ?? string.Empty).Trim(); + + // We send the file list as a JSON array to avoid encoding issues with the path separator + // as it is an allowed character in file and path names. + // We also accept the old way, for compatibility with the greeno theme + if (!string.IsNullOrWhiteSpace(rawpaths) && rawpaths.StartsWith("[", StringComparison.Ordinal) && rawpaths.EndsWith("]", StringComparison.Ordinal)) + filters = Newtonsoft.Json.JsonConvert.DeserializeObject(rawpaths); + else + filters = paths.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries); + + return filters; + } + + private void DoRepair(IBackup backup, RequestInfo info, bool repairUpdate) + { + var input = info.Request.Form; + string[] filters = null; + var extra = new Dictionary(); + if (input["only-paths"].Value != null) + extra["repair-only-paths"] = (Library.Utility.Utility.ParseBool(input["only-paths"].Value, false)).ToString(); + if (input["time"].Value != null) + extra["time"] = input["time"].Value; + if (input["version"].Value != null) + extra["version"] = input["version"].Value; + if (input["paths"].Value != null) + filters = parsePaths(input["paths"].Value); + + var task = Runner.CreateTask(repairUpdate ? DuplicatiOperation.RepairUpdate : DuplicatiOperation.Repair, backup, extra, filters); + FIXMEGlobal.WorkThread.AddTask(task); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + info.OutputOK(new {Status = "OK", ID = task.TaskID}); + } + + private void RunBackup(IBackup backup, RequestInfo info) + { + var t = FIXMEGlobal.WorkThread.CurrentTask; + var bt = t == null ? null : t.Backup; + if (bt != null && backup.ID == bt.ID) + { + // Already running + } + else if (FIXMEGlobal.WorkThread.CurrentTasks.Any(x => { + var bn = x?.Backup; + return bn == null || bn.ID == backup.ID; + })) + { + // Already in queue + } + else + { + FIXMEGlobal.WorkThread.AddTask(Runner.CreateTask(DuplicatiOperation.Backup, backup), true); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + info.OutputOK(); + } + + private void IsActive(IBackup backup, RequestInfo info) + { + var t = FIXMEGlobal.WorkThread.CurrentTask; + var bt = t?.Backup; + if (bt != null && backup.ID == bt.ID) + { + info.OutputOK(new { Status = "OK", Active = true }); + return; + } + else if (FIXMEGlobal.WorkThread.CurrentTasks.Any(x => + { + var bn = x?.Backup; + return bn == null || bn.ID == backup.ID; + })) + { + info.OutputOK(new { Status = "OK", Active = true }); + return; + } + else + { + info.OutputOK(new { Status = "OK", Active = false }); + return; + } + } + + private void UpdateDatabasePath(IBackup backup, RequestInfo info, bool move) + { + var np = info.Request.Form["path"].Value; + if (string.IsNullOrWhiteSpace(np)) + info.ReportClientError("No target path supplied", System.Net.HttpStatusCode.BadRequest); + else if (!Path.IsPathRooted(np)) + info.ReportClientError("Target path is relative, please supply a fully qualified path", System.Net.HttpStatusCode.BadRequest); + else + { + if (move && (File.Exists(np) || Directory.Exists(np))) + info.ReportClientError("A file already exists at the new location", System.Net.HttpStatusCode.Conflict); + else + { + if (move) + File.Move(backup.DBPath, np); + + FIXMEGlobal.DataConnection.UpdateBackupDBPath(backup, np); + } + + } + + } + + public void GET(string key, RequestInfo info) + { + var parts = (key ?? "").Split(new char[] { '/' }, 2); + var bk = FIXMEGlobal.DataConnection.GetBackup(parts.First()); + if (bk == null) + info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound); + else + { + if (parts.Length > 1) + { + var operation = parts.Last().Split(new char[] {'/'}).First().ToLowerInvariant(); + + switch (operation) + { + case "files": + var filter = parts.Last().Split(new char[] { '/' }, 2).Skip(1).FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(info.Request.QueryString["filter"].Value)) + filter = info.Request.QueryString["filter"].Value; + SearchFiles(bk, filter, info); + return; + case "log": + FetchLogData(bk, info); + return; + case "remotelog": + FetchRemoteLogData(bk, info); + return; + case "filesets": + ListFileSets(bk, info); + return; + case "export": + Export(bk, info); + return; + case "isdbusedelsewhere": + IsDBUsedElseWhere(bk, info); + return; + case "isactive": + IsActive(bk, info); + return; + default: + info.ReportClientError(string.Format("Invalid component: {0}", operation), System.Net.HttpStatusCode.BadRequest); + return; + } + + } + + var scheduleId = FIXMEGlobal.DataConnection.GetScheduleIDsFromTags(new string[] { "ID=" + bk.ID }); + var schedule = scheduleId.Any() ? FIXMEGlobal.DataConnection.GetSchedule(scheduleId.First()) : null; + var sourcenames = SpecialFolders.GetSourceNames(bk); + + //TODO: Filter out the password in both settings and the target url + + info.OutputOK(new GetResponse() + { + success = true, + data = new GetResponse.GetResponseData { + Schedule = schedule, + Backup = bk, + DisplayNames = sourcenames + } + }); + } + } + + public void POST(string key, RequestInfo info) + { + var parts = (key ?? "").Split(new char[] { '/' }, 2); + var bk = FIXMEGlobal.DataConnection.GetBackup(parts.First()); + if (bk == null) + info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound); + else + { + if (parts.Length > 1) + { + var operation = parts.Last().Split(new char[] { '/' }).First().ToLowerInvariant(); + + switch (operation) + { + case "deletedb": + System.IO.File.Delete(bk.DBPath); + info.OutputOK(); + return; + + case "movedb": + UpdateDatabasePath(bk, info, true); + return; + + case "updatedb": + UpdateDatabasePath(bk, info, false); + return; + + case "restore": + RestoreFiles(bk, info); + return; + + case "createreport": + CreateReport(bk, info); + return; + + case "repair": + Repair(bk, info); + return; + + case "repairupdate": + RepairUpdate(bk, info); + return; + + case "vacuum": + Vacuum(bk, info); + return; + + case "verify": + Verify(bk, info); + return; + + case "compact": + Compact(bk, info); + return; + + case "start": + case "run": + RunBackup(bk, info); + return; + + case "report-remote-size": + ReportRemoteSize(bk, info); + return; + + case "copytotemp": + var ipx = Serializer.Deserialize(new StringReader(Newtonsoft.Json.JsonConvert.SerializeObject(bk))); + + using(var tf = new Duplicati.Library.Utility.TempFile()) + ipx.DBPath = tf; + ipx.ID = null; + + info.OutputOK(new { status = "OK", ID = FIXMEGlobal.DataConnection.RegisterTemporaryBackup(ipx) }); + return; + } + } + + info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest); + } + } + + public void PUT(string key, RequestInfo info) + { + string str = info.Request.Form["data"].Value; + if (string.IsNullOrWhiteSpace(str)) + str = System.Threading.Tasks.Task.Run(async () => + { + using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) + + return await sr.ReadToEndAsync(); + }).GetAwaiter().GetResult(); + + if (string.IsNullOrWhiteSpace(str)) + { + info.ReportClientError("Missing backup object", System.Net.HttpStatusCode.BadRequest); + return; + } + + Backups.AddOrUpdateBackupData data = null; + try + { + data = Serializer.Deserialize(new StringReader(str)); + if (data.Backup == null) + { + info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest); + return; + } + + if (!string.IsNullOrEmpty(key)) + data.Backup.ID = key; + + if (string.IsNullOrEmpty(data.Backup.ID)) + { + info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.BadRequest); + return; + } + + + if (data.Backup.IsTemporary) + { + var backup = FIXMEGlobal.DataConnection.GetBackup(data.Backup.ID); + if (backup.IsTemporary) + throw new InvalidDataException("External is temporary but internal is not?"); + + FIXMEGlobal.DataConnection.UpdateTemporaryBackup(backup); + info.OutputOK(); + } + else + { + lock(FIXMEGlobal.DataConnection.m_lock) + { + var backup = FIXMEGlobal.DataConnection.GetBackup(data.Backup.ID); + if (backup == null) + { + info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound); + return; + } + + if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase) && x.ID != data.Backup.ID)) + { + info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict); + return; + } + + var err = FIXMEGlobal.DataConnection.ValidateBackup(data.Backup, data.Schedule); + if (!string.IsNullOrWhiteSpace(err)) + { + info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest); + return; + } + + //TODO: Merge in real passwords where the placeholder is found + FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule); + + } + + info.OutputOK(); + } + } + catch (Exception ex) + { + if (data == null) + info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest); + else + info.ReportClientError(string.Format("Unable to save backup or schedule: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError); + } + } + + public void DELETE(string key, RequestInfo info) + { + var backup = FIXMEGlobal.DataConnection.GetBackup(key); + if (backup == null) + { + info.ReportClientError("Invalid or missing backup id", System.Net.HttpStatusCode.NotFound); + return; + } + + var delete_remote_files = Library.Utility.Utility.ParseBool(info.Request.Param["delete-remote-files"].Value, false); + + if (delete_remote_files) + { + var captcha_token = info.Request.Param["captcha-token"].Value; + var captcha_answer = info.Request.Param["captcha-answer"].Value; + if (string.IsNullOrWhiteSpace(captcha_token) || string.IsNullOrWhiteSpace(captcha_answer)) + { + info.ReportClientError("Missing captcha", System.Net.HttpStatusCode.Unauthorized); + return; + } + + if (!Captcha.SolvedCaptcha(captcha_token, "DELETE /backup/" + backup.ID, captcha_answer)) + { + info.ReportClientError("Invalid captcha", System.Net.HttpStatusCode.Forbidden); + return; + } + } + + if (FIXMEGlobal.WorkThread.Active) + { + try + { + //TODO: It's not safe to access the values like this, + //because the runner thread might interfere + var nt = FIXMEGlobal.WorkThread.CurrentTask; + if (backup.Equals(nt == null ? null : nt.Backup)) + { + bool force; + if (!bool.TryParse(info.Request.QueryString["force"].Value, out force)) + force = false; + + if (!force) + { + info.OutputError(new { status = "failed", reason = "backup-in-progress" }); + return; + } + + bool hasPaused = FIXMEGlobal.LiveControl.State == LiveControls.LiveControlState.Paused; + FIXMEGlobal.LiveControl.Pause(); + + for(int i = 0; i < 10; i++) + if (FIXMEGlobal.WorkThread.Active) + { + var t = FIXMEGlobal.WorkThread.CurrentTask; + if (backup.Equals(t == null ? null : t.Backup)) + System.Threading.Thread.Sleep(1000); + else + break; + } + else + break; + + if (FIXMEGlobal.WorkThread.Active) + { + var t = FIXMEGlobal.WorkThread.CurrentTask; + if (backup.Equals(t == null ? null : t.Backup)) + { + if (hasPaused) + FIXMEGlobal.LiveControl.Resume(); + info.OutputError(new { status = "failed", reason = "backup-unstoppable" }); + return; + } + } + + if (hasPaused) + FIXMEGlobal.LiveControl.Resume(); + } + } + catch (Exception ex) + { + info.OutputError(new { status = "error", message = ex.Message }); + return; + } + } + + var extra = new Dictionary(); + if (!string.IsNullOrWhiteSpace(info.Request.Param["delete-local-db"].Value)) + extra["delete-local-db"] = info.Request.Param["delete-local-db"].Value; + if (delete_remote_files) + extra["delete-remote-files"] = "true"; + + var task = Runner.CreateTask(DuplicatiOperation.Delete, backup, extra); + FIXMEGlobal.WorkThread.AddTask(task); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + info.OutputOK(new { Status = "OK", ID = task.TaskID }); + } + public string Description { get { return "Retrieves, updates or deletes an existing backup and schedule"; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(GetResponse)), + new KeyValuePair(HttpServer.Method.Put, typeof(Backups.AddOrUpdateBackupData)), + new KeyValuePair(HttpServer.Method.Delete, typeof(long)) + }; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/BackupDefaults.cs b/Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs similarity index 91% rename from Duplicati/Server/WebServer/RESTMethods/BackupDefaults.cs rename to Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs index 6ab403cff..9138214b2 100644 --- a/Duplicati/Server/WebServer/RESTMethods/BackupDefaults.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/BackupDefaults.cs @@ -1,136 +1,141 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Linq; -using Duplicati.Library.Common.IO; -using Duplicati.Library.Utility; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class BackupDefaults : IRESTMethodGET - { - public void GET(string key, RequestInfo info) - { - // Start with a scratch object - var o = new Newtonsoft.Json.Linq.JObject(); - - // Add application wide settings - o.Add("ApplicationOptions", new Newtonsoft.Json.Linq.JArray( - from n in Program.DataConnection.Settings - select Newtonsoft.Json.Linq.JObject.FromObject(n) - )); - - try - { - // Add built-in defaults - Newtonsoft.Json.Linq.JObject n; - using(var s = new System.IO.StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(System.Reflection.Assembly.GetExecutingAssembly().GetName().Name + ".newbackup.json"))) - n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(s.ReadToEnd()); - - MergeJsonObjects(o, n); - } - catch - { - } - - try - { - // Add install defaults/overrides, if present - var path = SystemIO.IO_OS.PathCombine(Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "newbackup.json"); - if (System.IO.File.Exists(path)) - { - Newtonsoft.Json.Linq.JObject n; - n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(System.IO.File.ReadAllText(path)); - - MergeJsonObjects(o, n); - } - } - catch - { - } - - info.OutputOK(new - { - success = true, - data = o - }); - } - - private static void MergeJsonObjects(Newtonsoft.Json.Linq.JObject self, Newtonsoft.Json.Linq.JObject other) - { - foreach(var p in other.Properties()) - { - var sp = self.Property(p.Name); - if (sp == null) - self.Add(p); - else - { - switch (p.Type) - { - // Primitives override - case Newtonsoft.Json.Linq.JTokenType.Boolean: - case Newtonsoft.Json.Linq.JTokenType.Bytes: - case Newtonsoft.Json.Linq.JTokenType.Comment: - case Newtonsoft.Json.Linq.JTokenType.Constructor: - case Newtonsoft.Json.Linq.JTokenType.Date: - case Newtonsoft.Json.Linq.JTokenType.Float: - case Newtonsoft.Json.Linq.JTokenType.Guid: - case Newtonsoft.Json.Linq.JTokenType.Integer: - case Newtonsoft.Json.Linq.JTokenType.String: - case Newtonsoft.Json.Linq.JTokenType.TimeSpan: - case Newtonsoft.Json.Linq.JTokenType.Uri: - case Newtonsoft.Json.Linq.JTokenType.None: - case Newtonsoft.Json.Linq.JTokenType.Null: - case Newtonsoft.Json.Linq.JTokenType.Undefined: - self.Replace(p); - break; - - // Arrays merge - case Newtonsoft.Json.Linq.JTokenType.Array: - if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Array) - sp.Value = new Newtonsoft.Json.Linq.JArray(((Newtonsoft.Json.Linq.JArray)sp.Value).Union((Newtonsoft.Json.Linq.JArray)p.Value)); - else - { - var a = new Newtonsoft.Json.Linq.JArray(sp.Value); - sp.Value = new Newtonsoft.Json.Linq.JArray(a.Union((Newtonsoft.Json.Linq.JArray)p.Value)); - } - - break; - - // Objects merge - case Newtonsoft.Json.Linq.JTokenType.Object: - if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Object) - MergeJsonObjects((Newtonsoft.Json.Linq.JObject)sp.Value, (Newtonsoft.Json.Linq.JObject)p.Value); - else - sp.Value = p.Value; - break; - - // Ignore other stuff - default: - break; - } - } - } - } - } -} - +// Copyright (C) 2024, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +using System; +using System.Linq; +using Duplicati.Library.Common.IO; +using Duplicati.Library.RestAPI; +using Duplicati.Library.Utility; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class BackupDefaults : IRESTMethodGET + { + + private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType(); + + public void GET(string key, RequestInfo info) + { + // Start with a scratch object + var o = new Newtonsoft.Json.Linq.JObject(); + + // Add application wide settings + o.Add("ApplicationOptions", new Newtonsoft.Json.Linq.JArray( + from n in FIXMEGlobal.DataConnection.Settings + select Newtonsoft.Json.Linq.JObject.FromObject(n) + )); + + try + { + // Add built-in defaults + Newtonsoft.Json.Linq.JObject n; + using(var s = new System.IO.StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(typeof(FIXMEGlobal), "newbackup.json"))) + n = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.JsonConvert.DeserializeObject(s.ReadToEnd()); + + MergeJsonObjects(o, n); + } + catch (Exception e) + { + Library.Logging.Log.WriteErrorMessage(LOGTAG, "BackupDefaultsError", e, "Failed to locate embeded backup defaults"); + } + + try + { + // Add install defaults/overrides, if present + var path = SystemIO.IO_OS.PathCombine(Library.AutoUpdater.UpdaterManager.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 (Exception e) + { + Library.Logging.Log.WriteErrorMessage(LOGTAG, "BackupDefaultsError", e, "Failed to process newbackup.json"); + } + + info.OutputOK(new + { + success = true, + data = o + }); + } + + private static void MergeJsonObjects(Newtonsoft.Json.Linq.JObject self, Newtonsoft.Json.Linq.JObject other) + { + foreach(var p in other.Properties()) + { + var sp = self.Property(p.Name); + if (sp == null) + self.Add(p); + else + { + switch (p.Type) + { + // Primitives override + case Newtonsoft.Json.Linq.JTokenType.Boolean: + case Newtonsoft.Json.Linq.JTokenType.Bytes: + case Newtonsoft.Json.Linq.JTokenType.Comment: + case Newtonsoft.Json.Linq.JTokenType.Constructor: + case Newtonsoft.Json.Linq.JTokenType.Date: + case Newtonsoft.Json.Linq.JTokenType.Float: + case Newtonsoft.Json.Linq.JTokenType.Guid: + case Newtonsoft.Json.Linq.JTokenType.Integer: + case Newtonsoft.Json.Linq.JTokenType.String: + case Newtonsoft.Json.Linq.JTokenType.TimeSpan: + case Newtonsoft.Json.Linq.JTokenType.Uri: + case Newtonsoft.Json.Linq.JTokenType.None: + case Newtonsoft.Json.Linq.JTokenType.Null: + case Newtonsoft.Json.Linq.JTokenType.Undefined: + self.Replace(p); + break; + + // Arrays merge + case Newtonsoft.Json.Linq.JTokenType.Array: + if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Array) + sp.Value = new Newtonsoft.Json.Linq.JArray(((Newtonsoft.Json.Linq.JArray)sp.Value).Union((Newtonsoft.Json.Linq.JArray)p.Value)); + else + { + var a = new Newtonsoft.Json.Linq.JArray(sp.Value); + sp.Value = new Newtonsoft.Json.Linq.JArray(a.Union((Newtonsoft.Json.Linq.JArray)p.Value)); + } + + break; + + // Objects merge + case Newtonsoft.Json.Linq.JTokenType.Object: + if (sp.Type == Newtonsoft.Json.Linq.JTokenType.Object) + MergeJsonObjects((Newtonsoft.Json.Linq.JObject)sp.Value, (Newtonsoft.Json.Linq.JObject)p.Value); + else + sp.Value = p.Value; + break; + + // Ignore other stuff + default: + break; + } + } + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/Backups.cs b/Duplicati.Library.RestAPI/RESTMethods/Backups.cs similarity index 86% rename from Duplicati/Server/WebServer/RESTMethods/Backups.cs rename to Duplicati.Library.RestAPI/RESTMethods/Backups.cs index 6476fba79..bae68fa57 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Backups.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Backups.cs @@ -1,304 +1,310 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Linq; -using System.Collections.Generic; -using Duplicati.Server.Serialization; -using System.IO; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Backups : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented - { - public class AddOrUpdateBackupData - { - public Boolean IsUnencryptedOrPassphraseStored { get; set;} - public Database.Schedule Schedule { get; set;} - public Database.Backup Backup { get; set;} - } - - public void GET(string key, RequestInfo info) - { - var schedules = Program.DataConnection.Schedules; - var backups = Program.DataConnection.Backups; - - var all = from n in backups - select new AddOrUpdateBackupData { - IsUnencryptedOrPassphraseStored = Program.DataConnection.IsUnencryptedOrPassphraseStored(long.Parse(n.ID)), - Backup = (Database.Backup)n, - Schedule = - (from x in schedules - where x.Tags != null && x.Tags.Contains("ID=" + n.ID) - select (Database.Schedule)x).FirstOrDefault() - }; - - info.BodyWriter.OutputOK(all.ToArray()); - } - - private void ImportBackup(RequestInfo info) - { - var output_template = ""; - //output_template = ""; - try - { - var input = info.Request.Form; - var cmdline = Library.Utility.Utility.ParseBool(input["cmdline"].Value, false); - var import_metadata = Library.Utility.Utility.ParseBool(input["import_metadata"].Value, false); - var direct = Library.Utility.Utility.ParseBool(input["direct"].Value, false); - output_template = output_template.Replace("CBM", input["callback"].Value); - if (cmdline) - { - info.Response.ContentType = "text/html"; - info.BodyWriter.Write(output_template.Replace("MSG", "Import from commandline not yet implemented")); - } - else - { - var file = info.Request.Form.GetFile("config"); - if (file == null) - throw new Exception("No file uploaded"); - - Serializable.ImportExportStructure ipx = Backups.LoadConfiguration(file.Filename, import_metadata, () => input["passphrase"].Value); - if (direct) - { - lock (Program.DataConnection.m_lock) - { - var basename = ipx.Backup.Name; - var c = 0; - while (c++ < 100 && Program.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase))) - ipx.Backup.Name = basename + " (" + c.ToString() + ")"; - - if (Program.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase))) - { - info.BodyWriter.SetOK(); - info.Response.ContentType = "text/html"; - info.BodyWriter.Write(output_template.Replace("MSG", "There already exists a backup with the name: " + basename.Replace("\'", "\\'"))); - } - - var err = Program.DataConnection.ValidateBackup(ipx.Backup, ipx.Schedule); - if (!string.IsNullOrWhiteSpace(err)) - { - info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest); - return; - } - - Program.DataConnection.AddOrUpdateBackupAndSchedule(ipx.Backup, ipx.Schedule); - } - - info.Response.ContentType = "text/html"; - info.BodyWriter.Write(output_template.Replace("MSG", "OK")); - - } - else - { - using (var sw = new StringWriter()) - { - Serializer.SerializeJson(sw, ipx, true); - output_template = output_template.Replace("'JSO'", sw.ToString()); - } - info.BodyWriter.Write(output_template.Replace("MSG", "Import completed, but a browser issue prevents loading the contents. Try using the direct import method instead.")); - } - } - } - catch (Exception ex) - { - Program.DataConnection.LogError("", "Failed to import backup", ex); - info.Response.ContentType = "text/html"; - info.BodyWriter.Write(output_template.Replace("MSG", ex.Message.Replace("\'", "\\'").Replace("\r", "\\r").Replace("\n", "\\n"))); - } - } - - public static Serializable.ImportExportStructure ImportBackup(string configurationFile, bool importMetadata, Func getPassword, Dictionary advancedOptions) - { - // This removes the ID and DBPath from the backup configuration. - Serializable.ImportExportStructure importedStructure = Backups.LoadConfiguration(configurationFile, importMetadata, getPassword); - - // This will create the Duplicati-server.sqlite database file if it doesn't exist. - using (Duplicati.Server.Database.Connection connection = Program.GetDatabaseConnection(advancedOptions)) - { - if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase))) - { - throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists."); - } - - string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule); - if (!string.IsNullOrWhiteSpace(error)) - { - throw new InvalidOperationException(error); - } - - // This creates a new ID and DBPath. - connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule); - } - - return importedStructure; - } - - private static Serializable.ImportExportStructure LoadConfiguration(string filename, bool importMetadata, Func getPassword) - { - Serializable.ImportExportStructure ipx; - - var buf = new byte[3]; - using (var fs = System.IO.File.OpenRead(filename)) - { - Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buf, buf.Length); - - fs.Position = 0; - if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S') - { - using (var m = new Duplicati.Library.Encryption.AESEncryption(getPassword(), new Dictionary())) - { - using (var m2 = m.Decrypt(fs)) - { - using (var sr = new System.IO.StreamReader(m2)) - { - ipx = Serializer.Deserialize(sr); - } - } - } - } - else - { - using (var sr = new System.IO.StreamReader(fs)) - { - ipx = Serializer.Deserialize(sr); - } - } - } - - if (ipx.Backup == null) - { - throw new Exception("No backup found in document"); - } - - if (ipx.Backup.Metadata == null) - { - ipx.Backup.Metadata = new Dictionary(); - } - - if (!importMetadata) - { - ipx.Backup.Metadata.Clear(); - } - - ipx.Backup.ID = null; - ipx.Backup.DBPath = null; - - if (ipx.Schedule != null) - { - ipx.Schedule.ID = -1; - } - - return ipx; - } - - public void POST(string key, RequestInfo info) - { - if ("import".Equals(key, StringComparison.OrdinalIgnoreCase)) - { - ImportBackup(info); - return; - } - - AddOrUpdateBackupData data = null; - try - { - var str = info.Request.Form["data"].Value; - if (string.IsNullOrWhiteSpace(str)) - str = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8).ReadToEnd(); - - data = Serializer.Deserialize(new StringReader(str)); - if (data.Backup == null) - { - info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest); - return; - } - - data.Backup.ID = null; - - if (Duplicati.Library.Utility.Utility.ParseBool(info.Request.Form["temporary"].Value, false)) - { - using(var tf = new Duplicati.Library.Utility.TempFile()) - data.Backup.DBPath = tf; - - data.Backup.Filters = data.Backup.Filters ?? new Duplicati.Server.Serialization.Interface.IFilter[0]; - data.Backup.Settings = data.Backup.Settings ?? new Duplicati.Server.Serialization.Interface.ISetting[0]; - - Program.DataConnection.RegisterTemporaryBackup(data.Backup); - - info.OutputOK(new { status = "OK", ID = data.Backup.ID }); - } - else - { - if (Library.Utility.Utility.ParseBool(info.Request.Form["existing_db"].Value, false)) - { - data.Backup.DBPath = Library.Main.DatabaseLocator.GetDatabasePath(data.Backup.TargetURL, null, false, false); - if (string.IsNullOrWhiteSpace(data.Backup.DBPath)) - throw new Exception("Unable to find remote db path?"); - } - - - lock(Program.DataConnection.m_lock) - { - if (Program.DataConnection.Backups.Any(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase))) - { - info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict); - return; - } - - var err = Program.DataConnection.ValidateBackup(data.Backup, data.Schedule); - if (!string.IsNullOrWhiteSpace(err)) - { - info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest); - return; - } - - Program.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule); - } - - info.OutputOK(new { status = "OK", ID = data.Backup.ID }); - } - } - catch (Exception ex) - { - if (data == null) - info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest); - else - info.ReportClientError(string.Format("Unable to save schedule or backup object: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError); - } - } - - - public string Description { get { return "Return a list of current backups and their schedules"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(AddOrUpdateBackupData[])), - new KeyValuePair(HttpServer.Method.Post, typeof(AddOrUpdateBackupData)) - }; - } - } - } -} - +// Copyright (C) 2024, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +using System; +using System.Linq; +using System.Collections.Generic; +using Duplicati.Server.Serialization; +using System.IO; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Backups : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented + { + public class AddOrUpdateBackupData + { + public Boolean IsUnencryptedOrPassphraseStored { get; set;} + public Database.Schedule Schedule { get; set;} + public Database.Backup Backup { get; set;} + } + + public void GET(string key, RequestInfo info) + { + var schedules = FIXMEGlobal.DataConnection.Schedules; + var backups = FIXMEGlobal.DataConnection.Backups; + + var all = from n in backups + select new AddOrUpdateBackupData { + IsUnencryptedOrPassphraseStored = FIXMEGlobal.DataConnection.IsUnencryptedOrPassphraseStored(long.Parse(n.ID)), + Backup = (Database.Backup)n, + Schedule = + (from x in schedules + where x.Tags != null && x.Tags.Contains("ID=" + n.ID) + select (Database.Schedule)x).FirstOrDefault() + }; + + info.BodyWriter.OutputOK(all.ToArray()); + } + + private void ImportBackup(RequestInfo info) + { + var output_template = ""; + //output_template = ""; + try + { + var input = info.Request.Form; + var cmdline = Library.Utility.Utility.ParseBool(input["cmdline"].Value, false); + var import_metadata = Library.Utility.Utility.ParseBool(input["import_metadata"].Value, false); + var direct = Library.Utility.Utility.ParseBool(input["direct"].Value, false); + output_template = output_template.Replace("CBM", input["callback"].Value); + if (cmdline) + { + info.Response.ContentType = "text/html"; + info.BodyWriter.Write(output_template.Replace("MSG", "Import from commandline not yet implemented")); + } + else + { + var file = info.Request.Form.GetFile("config"); + if (file == null) + throw new Exception("No file uploaded"); + + Serializable.ImportExportStructure ipx = Backups.LoadConfiguration(file.Filename, import_metadata, () => input["passphrase"].Value); + if (direct) + { + lock (FIXMEGlobal.DataConnection.m_lock) + { + var basename = ipx.Backup.Name; + var c = 0; + while (c++ < 100 && FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase))) + ipx.Backup.Name = basename + " (" + c.ToString() + ")"; + + if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(ipx.Backup.Name, StringComparison.OrdinalIgnoreCase))) + { + info.BodyWriter.SetOK(); + info.Response.ContentType = "text/html"; + info.BodyWriter.Write(output_template.Replace("MSG", "There already exists a backup with the name: " + basename.Replace("\'", "\\'"))); + } + + var err = FIXMEGlobal.DataConnection.ValidateBackup(ipx.Backup, ipx.Schedule); + if (!string.IsNullOrWhiteSpace(err)) + { + info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest); + return; + } + + FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(ipx.Backup, ipx.Schedule); + } + + info.Response.ContentType = "text/html"; + info.BodyWriter.Write(output_template.Replace("MSG", "OK")); + + } + else + { + using (var sw = new StringWriter()) + { + Serializer.SerializeJson(sw, ipx, true); + output_template = output_template.Replace("'JSO'", sw.ToString()); + } + info.BodyWriter.Write(output_template.Replace("MSG", "Import completed, but a browser issue prevents loading the contents. Try using the direct import method instead.")); + } + } + } + catch (Exception ex) + { + FIXMEGlobal.DataConnection.LogError("", "Failed to import backup", ex); + info.Response.ContentType = "text/html"; + info.BodyWriter.Write(output_template.Replace("MSG", ex.Message.Replace("\'", "\\'").Replace("\r", "\\r").Replace("\n", "\\n"))); + } + } + + public static Serializable.ImportExportStructure ImportBackup(string configurationFile, bool importMetadata, Func getPassword, Dictionary advancedOptions) + { + // This removes the ID and DBPath from the backup configuration. + Serializable.ImportExportStructure importedStructure = Backups.LoadConfiguration(configurationFile, importMetadata, getPassword); + + // This will create the Duplicati-server.sqlite database file if it doesn't exist. + using (Duplicati.Server.Database.Connection connection = FIXMEGlobal.GetDatabaseConnection(advancedOptions)) + { + if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase))) + { + throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists."); + } + + string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule); + if (!string.IsNullOrWhiteSpace(error)) + { + throw new InvalidOperationException(error); + } + + // This creates a new ID and DBPath. + connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule); + } + + return importedStructure; + } + + private static Serializable.ImportExportStructure LoadConfiguration(string filename, bool importMetadata, Func getPassword) + { + Serializable.ImportExportStructure ipx; + + var buf = new byte[3]; + using (var fs = System.IO.File.OpenRead(filename)) + { + Duplicati.Library.Utility.Utility.ForceStreamRead(fs, buf, buf.Length); + + fs.Position = 0; + if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S') + { + using (var m = new Duplicati.Library.Encryption.AESEncryption(getPassword(), new Dictionary())) + { + using (var m2 = m.Decrypt(fs)) + { + using (var sr = new System.IO.StreamReader(m2)) + { + ipx = Serializer.Deserialize(sr); + } + } + } + } + else + { + using (var sr = new System.IO.StreamReader(fs)) + { + ipx = Serializer.Deserialize(sr); + } + } + } + + if (ipx.Backup == null) + { + throw new Exception("No backup found in document"); + } + + if (ipx.Backup.Metadata == null) + { + ipx.Backup.Metadata = new Dictionary(); + } + + if (!importMetadata) + { + ipx.Backup.Metadata.Clear(); + } + + ipx.Backup.ID = null; + ipx.Backup.DBPath = null; + + if (ipx.Schedule != null) + { + ipx.Schedule.ID = -1; + } + + return ipx; + } + + public void POST(string key, RequestInfo info) + { + if ("import".Equals(key, StringComparison.OrdinalIgnoreCase)) + { + ImportBackup(info); + return; + } + + AddOrUpdateBackupData data = null; + try + { + var str = info.Request.Form["data"].Value; + if (string.IsNullOrWhiteSpace(str)) { + str = System.Threading.Tasks.Task.Run(async () => + { + using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) + + return await sr.ReadToEndAsync(); + }).GetAwaiter().GetResult(); + } + + data = Serializer.Deserialize(new StringReader(str)); + if (data.Backup == null) + { + info.ReportClientError("Data object had no backup entry", System.Net.HttpStatusCode.BadRequest); + return; + } + + data.Backup.ID = null; + + if (Duplicati.Library.Utility.Utility.ParseBool(info.Request.Form["temporary"].Value, false)) + { + using(var tf = new Duplicati.Library.Utility.TempFile()) + data.Backup.DBPath = tf; + + data.Backup.Filters = data.Backup.Filters ?? new Duplicati.Server.Serialization.Interface.IFilter[0]; + data.Backup.Settings = data.Backup.Settings ?? new Duplicati.Server.Serialization.Interface.ISetting[0]; + + FIXMEGlobal.DataConnection.RegisterTemporaryBackup(data.Backup); + + info.OutputOK(new { status = "OK", ID = data.Backup.ID }); + } + else + { + if (Library.Utility.Utility.ParseBool(info.Request.Form["existing_db"].Value, false)) + { + data.Backup.DBPath = Library.Main.DatabaseLocator.GetDatabasePath(data.Backup.TargetURL, null, false, false); + if (string.IsNullOrWhiteSpace(data.Backup.DBPath)) + throw new Exception("Unable to find remote db path?"); + } + + + lock(FIXMEGlobal.DataConnection.m_lock) + { + if (FIXMEGlobal.DataConnection.Backups.Any(x => x.Name.Equals(data.Backup.Name, StringComparison.OrdinalIgnoreCase))) + { + info.ReportClientError("There already exists a backup with the name: " + data.Backup.Name, System.Net.HttpStatusCode.Conflict); + return; + } + + var err = FIXMEGlobal.DataConnection.ValidateBackup(data.Backup, data.Schedule); + if (!string.IsNullOrWhiteSpace(err)) + { + info.ReportClientError(err, System.Net.HttpStatusCode.BadRequest); + return; + } + + FIXMEGlobal.DataConnection.AddOrUpdateBackupAndSchedule(data.Backup, data.Schedule); + } + + info.OutputOK(new { status = "OK", ID = data.Backup.ID }); + } + } + catch (Exception ex) + { + if (data == null) + info.ReportClientError(string.Format("Unable to parse backup or schedule object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest); + else + info.ReportClientError(string.Format("Unable to save schedule or backup object: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError); + } + } + + + public string Description { get { return "Return a list of current backups and their schedules"; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(AddOrUpdateBackupData[])), + new KeyValuePair(HttpServer.Method.Post, typeof(AddOrUpdateBackupData)) + }; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/BugReport.cs b/Duplicati.Library.RestAPI/RESTMethods/BugReport.cs similarity index 92% rename from Duplicati/Server/WebServer/RESTMethods/BugReport.cs rename to Duplicati.Library.RestAPI/RESTMethods/BugReport.cs index cf5e8ca66..388a32aad 100644 --- a/Duplicati/Server/WebServer/RESTMethods/BugReport.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/BugReport.cs @@ -18,7 +18,7 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - +using Duplicati.Library.RestAPI; using System; using System.Linq; @@ -31,7 +31,7 @@ namespace Duplicati.Server.WebServer.RESTMethods long id; long.TryParse(key, out id); - var tf = Program.DataConnection.GetTempFiles().FirstOrDefault(x => x.ID == id); + var tf = FIXMEGlobal.DataConnection.GetTempFiles().FirstOrDefault(x => x.ID == id); if (tf == null) { info.ReportClientError("Invalid or missing bugreport id", System.Net.HttpStatusCode.NotFound); diff --git a/Duplicati/Server/WebServer/RESTMethods/Captcha.cs b/Duplicati.Library.RestAPI/RESTMethods/Captcha.cs similarity index 95% rename from Duplicati/Server/WebServer/RESTMethods/Captcha.cs rename to Duplicati.Library.RestAPI/RESTMethods/Captcha.cs index a9c0604c2..93345c8f8 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Captcha.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Captcha.cs @@ -1,171 +1,172 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Linq; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Captcha : IRESTMethodGET, IRESTMethodPOST - { - private class CaptchaEntry - { - public readonly string Answer; - public readonly string Target; - public int Attempts; - public readonly DateTime Expires; - - public CaptchaEntry(string answer, string target) - { - Answer = answer; - Target = target; - Attempts = 4; - Expires = DateTime.Now.AddMinutes(2); - } - } - - private static readonly object m_lock = new object(); - private static readonly Dictionary m_captchas = new Dictionary(); - - public static bool SolvedCaptcha(string token, string target, string answer) - { - lock(m_lock) - { - CaptchaEntry tp; - m_captchas.TryGetValue(token ?? string.Empty, out tp); - if (tp == null) - return false; - - if (tp.Attempts > 0) - tp.Attempts--; - - return tp.Attempts >= 0 && string.Equals(tp.Answer, answer, StringComparison.OrdinalIgnoreCase) && tp.Target == target && tp.Expires >= DateTime.Now; - } - } - - public void GET(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.ReportClientError("Missing token value", System.Net.HttpStatusCode.Unauthorized); - return; - } - else - { - string answer = null; - lock (m_lock) - { - CaptchaEntry tp; - m_captchas.TryGetValue(key, out tp); - if (tp != null && tp.Expires > DateTime.Now) - answer = tp.Answer; - } - - if (string.IsNullOrWhiteSpace(answer)) - { - info.ReportClientError("No such entry", System.Net.HttpStatusCode.NotFound); - return; - } - - using (var bmp = CaptchaUtil.CreateCaptcha(answer)) - using (var ms = new System.IO.MemoryStream()) - { - info.Response.ContentType = "image/jpeg"; - info.Response.ContentLength = ms.Length; - bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); - ms.Position = 0; - - info.Response.ContentType = "image/jpeg"; - info.Response.ContentLength = ms.Length; - info.Response.SendHeaders(); - ms.CopyTo(info.Response.Body); - info.Response.Send(); - } - } - } - - public void POST(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - var target = info.Request.Param["target"].Value; - if (string.IsNullOrWhiteSpace(target)) - { - info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest); - return; - } - - var answer = CaptchaUtil.CreateRandomAnswer(minlength: 6, maxlength: 6); - var nonce = Guid.NewGuid().ToString(); - - string token; - using (var ms = new System.IO.MemoryStream()) - { - var bytes = System.Text.Encoding.UTF8.GetBytes(answer + nonce); - ms.Write(bytes, 0, bytes.Length); - ms.Position = 0; - token = Library.Utility.Utility.Base64PlainToBase64Url(Library.Utility.Utility.CalculateHash(ms)); - } - - lock (m_lock) - { - var expired = m_captchas.Where(x => x.Value.Expires < DateTime.Now).Select(x => x.Key).ToArray(); - foreach (var x in expired) - m_captchas.Remove(x); - - if (m_captchas.Count > 3) - { - info.ReportClientError("Too many captchas, wait 2 minutes and try again", System.Net.HttpStatusCode.ServiceUnavailable); - return; - } - - m_captchas[token] = new CaptchaEntry(answer, target); - } - - info.OutputOK(new - { - token = token - }); - } - else - { - var answer = info.Request.Param["answer"].Value; - var target = info.Request.Param["target"].Value; - if (string.IsNullOrWhiteSpace(answer)) - { - info.ReportClientError("Missing answer parameter", System.Net.HttpStatusCode.BadRequest); - return; - } - if (string.IsNullOrWhiteSpace(target)) - { - info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest); - return; - } - - if (SolvedCaptcha(key, target, answer)) - info.OutputOK(); - else - info.ReportClientError("Incorrect", System.Net.HttpStatusCode.Forbidden); - } - } - } -} +// Copyright (C) 2024, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +using System; +using System.Linq; +using System.Collections.Generic; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Captcha : IRESTMethodGET, IRESTMethodPOST + { + private class CaptchaEntry + { + public readonly string Answer; + public readonly string Target; + public int Attempts; + public readonly DateTime Expires; + + public CaptchaEntry(string answer, string target) + { + Answer = answer; + Target = target; + Attempts = 4; + Expires = DateTime.Now.AddMinutes(2); + } + } + + private static readonly object m_lock = new object(); + private static readonly Dictionary m_captchas = new Dictionary(); + + public static bool SolvedCaptcha(string token, string target, string answer) + { + lock(m_lock) + { + CaptchaEntry tp; + m_captchas.TryGetValue(token ?? string.Empty, out tp); + if (tp == null) + return false; + + if (tp.Attempts > 0) + tp.Attempts--; + + return tp.Attempts >= 0 && string.Equals(tp.Answer, answer, StringComparison.OrdinalIgnoreCase) && tp.Target == target && tp.Expires >= DateTime.Now; + } + } + + public void GET(string key, RequestInfo info) + { + if (string.IsNullOrWhiteSpace(key)) + { + info.ReportClientError("Missing token value", System.Net.HttpStatusCode.Unauthorized); + return; + } + else + { + string answer = null; + lock (m_lock) + { + CaptchaEntry tp; + m_captchas.TryGetValue(key, out tp); + if (tp != null && tp.Expires > DateTime.Now) + answer = tp.Answer; + } + + if (string.IsNullOrWhiteSpace(answer)) + { + info.ReportClientError("No such entry", System.Net.HttpStatusCode.NotFound); + return; + } + + using (var bmp = CaptchaUtil.CreateCaptcha(answer)) + using (var ms = new System.IO.MemoryStream()) + { + info.Response.ContentType = "image/jpeg"; + info.Response.ContentLength = ms.Length; + bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); + ms.Position = 0; + + info.Response.ContentType = "image/jpeg"; + info.Response.ContentLength = ms.Length; + info.Response.SendHeaders(); + ms.CopyTo(info.Response.Body); + info.Response.Send(); + } + } + } + + public void POST(string key, RequestInfo info) + { + if (string.IsNullOrWhiteSpace(key)) + { + var target = info.Request.Param["target"].Value; + if (string.IsNullOrWhiteSpace(target)) + { + info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest); + return; + } + + var answer = CaptchaUtil.CreateRandomAnswer(minlength: 6, maxlength: 6); + var nonce = Guid.NewGuid().ToString(); + + string token; + using (var ms = new System.IO.MemoryStream()) + { + var bytes = System.Text.Encoding.UTF8.GetBytes(answer + nonce); + ms.Write(bytes, 0, bytes.Length); + ms.Position = 0; + using(var hasher = Library.Utility.HashFactory.CreateHasher(Library.Utility.HashFactory.SHA256)){ + token = Library.Utility.Utility.Base64PlainToBase64Url(Convert.ToBase64String(hasher.ComputeHash(ms))); + } + } + + lock (m_lock) + { + var expired = m_captchas.Where(x => x.Value.Expires < DateTime.Now).Select(x => x.Key).ToArray(); + foreach (var x in expired) + m_captchas.Remove(x); + + if (m_captchas.Count > 3) + { + info.ReportClientError("Too many captchas, wait 2 minutes and try again", System.Net.HttpStatusCode.ServiceUnavailable); + return; + } + + m_captchas[token] = new CaptchaEntry(answer, target); + } + + info.OutputOK(new + { + token = token + }); + } + else + { + var answer = info.Request.Param["answer"].Value; + var target = info.Request.Param["target"].Value; + if (string.IsNullOrWhiteSpace(answer)) + { + info.ReportClientError("Missing answer parameter", System.Net.HttpStatusCode.BadRequest); + return; + } + if (string.IsNullOrWhiteSpace(target)) + { + info.ReportClientError("Missing target parameter", System.Net.HttpStatusCode.BadRequest); + return; + } + + if (SolvedCaptcha(key, target, answer)) + info.OutputOK(); + else + info.ReportClientError("Incorrect", System.Net.HttpStatusCode.Forbidden); + } + } + } +} diff --git a/Duplicati/Server/WebServer/RESTMethods/Changelog.cs b/Duplicati.Library.RestAPI/RESTMethods/Changelog.cs similarity index 93% rename from Duplicati/Server/WebServer/RESTMethods/Changelog.cs rename to Duplicati.Library.RestAPI/RESTMethods/Changelog.cs index d991f813d..7a9712562 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Changelog.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Changelog.cs @@ -18,7 +18,7 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - +using Duplicati.Library.RestAPI; using System; using System.Collections.Generic; @@ -47,7 +47,7 @@ namespace Duplicati.Server.WebServer.RESTMethods } else { - var updateInfo = Program.DataConnection.ApplicationSettings.UpdatedVersion; + var updateInfo = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion; if (updateInfo == null) { info.ReportClientError("No update found", System.Net.HttpStatusCode.NotFound); diff --git a/Duplicati/Server/WebServer/RESTMethods/CommandLine.cs b/Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs similarity index 90% rename from Duplicati/Server/WebServer/RESTMethods/CommandLine.cs rename to Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs index 9553a29a5..f12abe25c 100644 --- a/Duplicati/Server/WebServer/RESTMethods/CommandLine.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/CommandLine.cs @@ -1,288 +1,292 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class CommandLine : IRESTMethodGET, IRESTMethodPOST - { - private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType(); - - private class LogWriter : System.IO.TextWriter - { - private readonly ActiveRun m_target; - private readonly StringBuilder m_sb = new StringBuilder(); - private int m_newlinechars = 0; - - public LogWriter(ActiveRun target) - { - m_target = target; - } - - public override Encoding Encoding { get { return System.Text.Encoding.UTF8; } } - - public override void Write(char value) - { - lock(m_target.Lock) - { - m_sb.Append(value); - if (NewLine[m_newlinechars] == value) - { - m_newlinechars++; - if (m_newlinechars == NewLine.Length) - WriteLine(string.Empty); - } - else - m_newlinechars = 0; - } - } - - public override void WriteLine(string value) - { - value = value ?? string.Empty; - lock(m_target.Lock) - { - m_target.LastAccess = DateTime.Now; - - //Avoid writing the log if it does not exist - if (m_target.IsLogDisposed) - { - Program.LogHandler.WriteMessage(new Library.Logging.LogEntry("Attempted to write message after closing: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", null)); - return; - } - - try - { - if (m_sb.Length != 0) - { - m_target.Log.Add(m_sb + value); - m_sb.Length = 0; - m_newlinechars = 0; - } - else - { - m_target.Log.Add(value); - } - } - catch (Exception ex) - { - // This can happen on a very unlucky race where IsLogDisposed is set right after the check - Program.LogHandler.WriteMessage(new Library.Logging.LogEntry("Failed to forward commandline message: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", ex)); - } - } - } - } - - private class ActiveRun - { - public readonly string ID = Guid.NewGuid().ToString(); - public DateTime LastAccess = DateTime.Now; - public readonly Library.Utility.FileBackedStringList Log = new Library.Utility.FileBackedStringList(); - public Runner.IRunnerData Task; - public LogWriter Writer; - public readonly object Lock = new object(); - public bool Finished = false; - public bool Started = false; - public bool IsLogDisposed = false; - public System.Threading.Thread Thread; - } - - private readonly Dictionary m_activeItems = new Dictionary(); - private System.Threading.Tasks.Task m_cleanupTask; - - public void POST(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - string[] args; - using (var sr = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) - args = Newtonsoft.Json.JsonConvert.DeserializeObject(sr.ReadToEnd()); - - var k = new ActiveRun(); - k.Writer = new LogWriter(k); - - m_activeItems[k.ID] = k; - StartCleanupTask(); - - k.Task = Runner.CreateCustomTask((sink) => - { - try - { - k.Thread = System.Threading.Thread.CurrentThread; - k.Started = true; - - var code = Duplicati.CommandLine.Program.RunCommandLine(k.Writer, k.Writer, c => { - k.Task.SetController(c); - c.AppendSink(sink); - }, args); - k.Writer.WriteLine("Return code: {0}", code); - } - catch (Exception ex) - { - var rx = ex; - if (rx is System.Reflection.TargetInvocationException) - rx = rx.InnerException; - - if (rx is Library.Interface.UserInformationException) - k.Log.Add(rx.Message); - else - k.Log.Add(rx.ToString()); - - throw rx; - } - finally - { - k.Finished = true; - k.Thread = null; - } - }); - - Program.WorkThread.AddTask(k.Task); - - info.OutputOK(new - { - ID = k.ID - }); - } - else - { - if (!key.EndsWith("/abort", StringComparison.OrdinalIgnoreCase)) - { - info.ReportClientError("Only abort commands are allowed", System.Net.HttpStatusCode.BadRequest); - return; - } - - key = key.Substring(0, key.Length - "/abort".Length); - if (string.IsNullOrWhiteSpace(key)) - { - info.ReportClientError("No task key found", System.Net.HttpStatusCode.BadRequest); - return; - } - - ActiveRun t; - if (!m_activeItems.TryGetValue(key, out t)) - { - info.OutputError(code: System.Net.HttpStatusCode.NotFound); - return; - } - - var tt = t.Task; - if (tt != null) - tt.Abort(); - - var tr = t.Thread; - if (tr != null) - tr.Abort(); - - info.OutputOK(); - } - } - - private void StartCleanupTask() - { - if (m_cleanupTask == null || m_cleanupTask.IsCompleted || m_cleanupTask.IsFaulted || m_cleanupTask.IsCanceled) - m_cleanupTask = RunCleanupAsync(); - } - - private async System.Threading.Tasks.Task RunCleanupAsync() - { - while (m_activeItems.Count > 0) - { - var oldest = m_activeItems.Values - .OrderBy(x => x.LastAccess) - .FirstOrDefault(); - - if (oldest != null) - { - // If the task has finished, we just wait a little to allow the UI to pick it up - var timeout = oldest.Finished ? TimeSpan.FromMinutes(5) : TimeSpan.FromDays(1); - if (DateTime.Now - oldest.LastAccess > timeout) - { - oldest.IsLogDisposed = true; - m_activeItems.Remove(oldest.ID); - oldest.Log.Dispose(); - - // Fix all expired, or stop running - continue; - } - } - - await System.Threading.Tasks.Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false); - } - } - - public void GET(string key, RequestInfo info) - { - if (string.IsNullOrWhiteSpace(key)) - { - info.OutputOK( - Duplicati.CommandLine.Program.SupportedCommands - ); - } - else - { - ActiveRun t; - if (!m_activeItems.TryGetValue(key, out t)) - { - info.OutputError(code: System.Net.HttpStatusCode.NotFound); - return; - } - - int pagesize; - int offset; - - int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize); - int.TryParse(info.Request.QueryString["offset"].Value, out offset); - pagesize = Math.Max(10, Math.Min(500, pagesize)); - offset = Math.Max(0, offset); - var items = new List(); - long count; - bool started; - bool finished; - - lock(t.Lock) - { - t.LastAccess = DateTime.Now; - count = t.Log.Count; - offset = Math.Min((int)count, offset); - items.AddRange(t.Log.Skip(offset).Take(pagesize)); - finished = t.Finished; - started = t.Started; - } - - info.OutputOK(new - { - Pagesize = pagesize, - Offset = offset, - Count = count, - Items = items, - Finished = finished, - Started = started - }); - } - } - } -} +// Copyright (C) 2024, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE.using Duplicati.Library.RestAPI; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class CommandLine : IRESTMethodGET, IRESTMethodPOST + { + private static readonly string LOGTAG = Library.Logging.Log.LogTagFromType(); + + private class LogWriter : System.IO.TextWriter + { + private readonly ActiveRun m_target; + private readonly StringBuilder m_sb = new StringBuilder(); + private int m_newlinechars = 0; + + public LogWriter(ActiveRun target) + { + m_target = target; + } + + public override Encoding Encoding { get { return System.Text.Encoding.UTF8; } } + + public override void Write(char value) + { + lock(m_target.Lock) + { + m_sb.Append(value); + if (NewLine[m_newlinechars] == value) + { + m_newlinechars++; + if (m_newlinechars == NewLine.Length) + WriteLine(string.Empty); + } + else + m_newlinechars = 0; + } + } + + public override void WriteLine(string value) + { + value = value ?? string.Empty; + lock(m_target.Lock) + { + m_target.LastAccess = DateTime.Now; + + //Avoid writing the log if it does not exist + if (m_target.IsLogDisposed) + { + FIXMEGlobal.LogHandler.WriteMessage(new Library.Logging.LogEntry("Attempted to write message after closing: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", null)); + return; + } + + try + { + if (m_sb.Length != 0) + { + m_target.Log.Add(m_sb + value); + m_sb.Length = 0; + m_newlinechars = 0; + } + else + { + m_target.Log.Add(value); + } + } + catch (Exception ex) + { + // This can happen on a very unlucky race where IsLogDisposed is set right after the check + FIXMEGlobal.LogHandler.WriteMessage(new Library.Logging.LogEntry("Failed to forward commandline message: {0}", new object[] { value }, Library.Logging.LogMessageType.Warning, LOGTAG, "CommandLineOutputAfterLogClosed", ex)); + } + } + } + } + + private class ActiveRun + { + public readonly string ID = Guid.NewGuid().ToString(); + public DateTime LastAccess = DateTime.Now; + public readonly Library.Utility.FileBackedStringList Log = new Library.Utility.FileBackedStringList(); + public Runner.IRunnerData Task; + public LogWriter Writer; + public readonly object Lock = new object(); + public bool Finished = false; + public bool Started = false; + public bool IsLogDisposed = false; + public System.Threading.Thread Thread; + } + + private readonly Dictionary m_activeItems = new Dictionary(); + private System.Threading.Tasks.Task m_cleanupTask; + + public void POST(string key, RequestInfo info) + { + if (string.IsNullOrWhiteSpace(key)) + { + string[] args; + var str = System.Threading.Tasks.Task.Run(async () => + { + using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) + + return await sr.ReadToEndAsync(); + }).GetAwaiter().GetResult(); + args = Newtonsoft.Json.JsonConvert.DeserializeObject(str); + + var k = new ActiveRun(); + k.Writer = new LogWriter(k); + + m_activeItems[k.ID] = k; + StartCleanupTask(); + + k.Task = Runner.CreateCustomTask((sink) => + { + try + { + k.Thread = System.Threading.Thread.CurrentThread; + k.Started = true; + + var code = Duplicati.CommandLine.Program.RunCommandLine(k.Writer, k.Writer, c => { + k.Task.SetController(c); + c.AppendSink(sink); + }, args); + k.Writer.WriteLine("Return code: {0}", code); + } + catch (Exception ex) + { + var rx = ex; + if (rx is System.Reflection.TargetInvocationException) + rx = rx.InnerException; + + if (rx is Library.Interface.UserInformationException) + k.Log.Add(rx.Message); + else + k.Log.Add(rx.ToString()); + + throw rx; + } + finally + { + k.Finished = true; + k.Thread = null; + } + }); + + FIXMEGlobal.WorkThread.AddTask(k.Task); + + info.OutputOK(new + { + ID = k.ID + }); + } + else + { + if (!key.EndsWith("/abort", StringComparison.OrdinalIgnoreCase)) + { + info.ReportClientError("Only abort commands are allowed", System.Net.HttpStatusCode.BadRequest); + return; + } + + key = key.Substring(0, key.Length - "/abort".Length); + if (string.IsNullOrWhiteSpace(key)) + { + info.ReportClientError("No task key found", System.Net.HttpStatusCode.BadRequest); + return; + } + + ActiveRun t; + if (!m_activeItems.TryGetValue(key, out t)) + { + info.OutputError(code: System.Net.HttpStatusCode.NotFound); + return; + } + + var tt = t.Task; + if (tt != null) + tt.Abort(); + + var tr = t.Thread; + if (tr != null) + tr.Interrupt(); + + info.OutputOK(); + } + } + + private void StartCleanupTask() + { + if (m_cleanupTask == null || m_cleanupTask.IsCompleted || m_cleanupTask.IsFaulted || m_cleanupTask.IsCanceled) + m_cleanupTask = RunCleanupAsync(); + } + + private async System.Threading.Tasks.Task RunCleanupAsync() + { + while (m_activeItems.Count > 0) + { + var oldest = m_activeItems.Values + .OrderBy(x => x.LastAccess) + .FirstOrDefault(); + + if (oldest != null) + { + // If the task has finished, we just wait a little to allow the UI to pick it up + var timeout = oldest.Finished ? TimeSpan.FromMinutes(5) : TimeSpan.FromDays(1); + if (DateTime.Now - oldest.LastAccess > timeout) + { + oldest.IsLogDisposed = true; + m_activeItems.Remove(oldest.ID); + oldest.Log.Dispose(); + + // Fix all expired, or stop running + continue; + } + } + + await System.Threading.Tasks.Task.Delay(TimeSpan.FromMinutes(1)).ConfigureAwait(false); + } + } + + public void GET(string key, RequestInfo info) + { + if (string.IsNullOrWhiteSpace(key)) + { + info.OutputOK( + Duplicati.CommandLine.Program.SupportedCommands + ); + } + else + { + ActiveRun t; + if (!m_activeItems.TryGetValue(key, out t)) + { + info.OutputError(code: System.Net.HttpStatusCode.NotFound); + return; + } + + int pagesize; + int offset; + + int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize); + int.TryParse(info.Request.QueryString["offset"].Value, out offset); + pagesize = Math.Max(10, Math.Min(500, pagesize)); + offset = Math.Max(0, offset); + var items = new List(); + long count; + bool started; + bool finished; + + lock(t.Lock) + { + t.LastAccess = DateTime.Now; + count = t.Log.Count; + offset = Math.Min((int)count, offset); + items.AddRange(t.Log.Skip(offset).Take(pagesize)); + finished = t.Finished; + started = t.Started; + } + + info.OutputOK(new + { + Pagesize = pagesize, + Offset = offset, + Count = count, + Items = items, + Finished = finished, + Started = started + }); + } + } + } +} diff --git a/Duplicati/Server/WebServer/RESTMethods/Filesystem.cs b/Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/Filesystem.cs rename to Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs index 8845a7010..cc77df60e 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Filesystem.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Filesystem.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Collections.Generic; using System.Linq; diff --git a/Duplicati/Server/WebServer/RESTMethods/Help.cs b/Duplicati.Library.RestAPI/RESTMethods/Help.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/Help.cs rename to Duplicati.Library.RestAPI/RESTMethods/Help.cs index 0cd860dc2..b409b7f77 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Help.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Help.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Linq; using System.Text; diff --git a/Duplicati/Server/WebServer/RESTMethods/HyperV.cs b/Duplicati.Library.RestAPI/RESTMethods/HyperV.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/HyperV.cs rename to Duplicati.Library.RestAPI/RESTMethods/HyperV.cs index d2eef2d4e..b957cfd48 100644 --- a/Duplicati/Server/WebServer/RESTMethods/HyperV.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/HyperV.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Collections.Generic; using Duplicati.Library.Interface; diff --git a/Duplicati/Server/WebServer/RESTMethods/IRESTMethod.cs b/Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/IRESTMethod.cs rename to Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs index 36b0d7ff8..488f4ce1d 100644 --- a/Duplicati/Server/WebServer/RESTMethods/IRESTMethod.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/IRESTMethod.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Collections.Generic; diff --git a/Duplicati/Server/WebServer/RESTMethods/Licenses.cs b/Duplicati.Library.RestAPI/RESTMethods/Licenses.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/Licenses.cs rename to Duplicati.Library.RestAPI/RESTMethods/Licenses.cs index 289d7f379..f787f5bd5 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Licenses.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Licenses.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; namespace Duplicati.Server.WebServer.RESTMethods diff --git a/Duplicati/Server/WebServer/RESTMethods/LogData.cs b/Duplicati.Library.RestAPI/RESTMethods/LogData.cs similarity index 93% rename from Duplicati/Server/WebServer/RESTMethods/LogData.cs rename to Duplicati.Library.RestAPI/RESTMethods/LogData.cs index c0d10001c..1b78b4898 100644 --- a/Duplicati/Server/WebServer/RESTMethods/LogData.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/LogData.cs @@ -18,9 +18,9 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Collections.Generic; +using Duplicati.Library.RestAPI; namespace Duplicati.Server.WebServer.RESTMethods { @@ -46,13 +46,13 @@ namespace Duplicati.Server.WebServer.RESTMethods long.TryParse(id_str, out id); Enum.TryParse(level_str, true, out level); - info.OutputOK(Program.LogHandler.AfterID(id, level, pagesize)); + info.OutputOK(FIXMEGlobal.LogHandler.AfterID(id, level, pagesize)); } else { List> res = null; - Program.DataConnection.ExecuteWithCommand(x => + FIXMEGlobal.DataConnection.ExecuteWithCommand(x => { res = DumpTable(x, "ErrorLog", "Timestamp", info.Request.QueryString["offset"].Value, info.Request.QueryString["pagesize"].Value); }); diff --git a/Duplicati/Server/WebServer/RESTMethods/MSSQL.cs b/Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/MSSQL.cs rename to Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs index 944bf806b..be873efdb 100644 --- a/Duplicati/Server/WebServer/RESTMethods/MSSQL.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/MSSQL.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Collections.Generic; using Duplicati.Library.Interface; diff --git a/Duplicati/Server/WebServer/RESTMethods/Notification.cs b/Duplicati.Library.RestAPI/RESTMethods/Notification.cs similarity index 86% rename from Duplicati/Server/WebServer/RESTMethods/Notification.cs rename to Duplicati.Library.RestAPI/RESTMethods/Notification.cs index e9174695b..24199cf0b 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Notification.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Notification.cs @@ -18,7 +18,7 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - +using Duplicati.Library.RestAPI; using System; using System.Linq; @@ -35,7 +35,7 @@ namespace Duplicati.Server.WebServer.RESTMethods return; } - var el = Program.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id); + var el = FIXMEGlobal.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id); if (el == null) info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound); else @@ -51,12 +51,12 @@ namespace Duplicati.Server.WebServer.RESTMethods return; } - var el = Program.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id); + var el = FIXMEGlobal.DataConnection.GetNotifications().FirstOrDefault(x => x.ID == id); if (el == null) info.ReportClientError("No such notification", System.Net.HttpStatusCode.NotFound); else { - Program.DataConnection.DismissNotification(id); + FIXMEGlobal.DataConnection.DismissNotification(id); info.OutputOK(); } } diff --git a/Duplicati/Server/WebServer/RESTMethods/Notifications.cs b/Duplicati.Library.RestAPI/RESTMethods/Notifications.cs similarity index 90% rename from Duplicati/Server/WebServer/RESTMethods/Notifications.cs rename to Duplicati.Library.RestAPI/RESTMethods/Notifications.cs index 9962abf18..3e4bb7b8b 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Notifications.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Notifications.cs @@ -18,7 +18,7 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - +using Duplicati.Library.RestAPI; using System; namespace Duplicati.Server.WebServer.RESTMethods @@ -27,7 +27,7 @@ namespace Duplicati.Server.WebServer.RESTMethods { public void GET(string key, RequestInfo info) { - info.OutputOK(Program.DataConnection.GetNotifications()); + info.OutputOK(FIXMEGlobal.DataConnection.GetNotifications()); } } } diff --git a/Duplicati/Server/WebServer/RESTMethods/ProgressState.cs b/Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs similarity index 90% rename from Duplicati/Server/WebServer/RESTMethods/ProgressState.cs rename to Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs index 18703e2c5..e85974dd4 100644 --- a/Duplicati/Server/WebServer/RESTMethods/ProgressState.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/ProgressState.cs @@ -18,7 +18,7 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - +using Duplicati.Library.RestAPI; using System; using System.Collections.Generic; @@ -28,10 +28,10 @@ namespace Duplicati.Server.WebServer.RESTMethods { public void GET(string key, RequestInfo info) { - if (Program.GenerateProgressState == null) + if (FIXMEGlobal.GenerateProgressState == null) info.ReportClientError("No active backup", System.Net.HttpStatusCode.NotFound); else - info.OutputOK(Program.GenerateProgressState()); + info.OutputOK(FIXMEGlobal.GenerateProgressState()); } public string Description { get { return "Return the progress of the currently running operation."; } } diff --git a/Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs b/Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs similarity index 86% rename from Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs rename to Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs index 5c32ad896..18e73a608 100644 --- a/Duplicati/Server/WebServer/RESTMethods/RemoteOperation.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/RemoteOperation.cs @@ -1,192 +1,194 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class RemoteOperation : IRESTMethodGET, IRESTMethodPOST - { - private void LocateDbUri(string uri, RequestInfo info) - { - var path = Library.Main.DatabaseLocator.GetDatabasePath(uri, null, false, false); - info.OutputOK(new { - Exists = !string.IsNullOrWhiteSpace(path), - Path = path - }); - } - - private void CreateFolder(string uri, RequestInfo info) - { - using(var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary())) - b.CreateFolder(); - - info.OutputOK(); - } - - private void UploadFile(string uri, RequestInfo info) - { - var data = info.Request.QueryString["data"].Value; - var remotename = info.Request.QueryString["filename"].Value; - - using(var ms = new System.IO.MemoryStream()) - using(var b = Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary())) - { - using(var tf = new Library.Utility.TempFile()) - { - System.IO.File.WriteAllText(tf, data); - b.PutAsync(remotename, tf, CancellationToken.None).Wait(); - } - } - - info.OutputOK(); - } - - private void ListFolder(string uri, RequestInfo info) - { - using(var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary())) - info.OutputOK(b.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(); - - try - { - var uri = new Library.Utility.Uri(url); - var qp = uri.QueryParameters; - - var opts = new Dictionary(); - foreach (var k in qp.Keys.Cast()) - opts[k] = qp[k]; - - foreach (var n in modules) - n.Configure(opts); - - using (var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(url, new Dictionary())) - b.Test(); - - info.OutputOK(); - } - catch (Duplicati.Library.Interface.FolderMissingException) - { - info.ReportServerError("missing-folder"); - } - catch (Duplicati.Library.Utility.SslCertificateValidator.InvalidCertificateException icex) - { - if (string.IsNullOrWhiteSpace(icex.Certificate)) - info.ReportServerError(icex.Message); - else - info.ReportServerError("incorrect-cert:" + icex.Certificate); - } - catch (Duplicati.Library.Utility.HostKeyException hex) - { - if (string.IsNullOrWhiteSpace(hex.ReportedHostKey)) - info.ReportServerError(hex.Message); - else - { - info.ReportServerError(string.Format( - @"incorrect-host-key:""{0}"", accepted-host-key:""{1}""", - hex.ReportedHostKey, - hex.AcceptedHostKey - )); - } - } - finally - { - foreach (var n in modules) - if (n is IDisposable disposable) - disposable.Dispose(); - } - } - - public void GET(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }, 2); - - if (parts.Length <= 1) - { - info.ReportClientError("No url or operation supplied", System.Net.HttpStatusCode.BadRequest); - return; - } - - var url = Library.Utility.Uri.UrlDecode(parts.First()); - var operation = parts.Last().ToLowerInvariant(); - - switch (operation) - { - case "dbpath": - LocateDbUri(url, info); - return; - case "list": - ListFolder(url, info); - return; - case "create": - CreateFolder(url, info); - return; - case "test": - TestConnection(url, info); - return; - default: - info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest); - return; - } - } - - public void POST(string key, RequestInfo info) - { - string url; - - using(var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) - url = sr.ReadToEnd(); - - switch (key) - { - case "dbpath": - LocateDbUri(url, info); - return; - case "list": - ListFolder(url, info); - return; - case "create": - CreateFolder(url, info); - return; - case "put": - UploadFile(url, info); - return; - case "test": - TestConnection(url, info); - return; - default: - info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest); - return; - } - } - } -} - +// Copyright (C) 2024, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class RemoteOperation : IRESTMethodGET, IRESTMethodPOST + { + private void LocateDbUri(string uri, RequestInfo info) + { + var path = Library.Main.DatabaseLocator.GetDatabasePath(uri, null, false, false); + info.OutputOK(new + { + Exists = !string.IsNullOrWhiteSpace(path), + Path = path + }); + } + + private void CreateFolder(string uri, RequestInfo info) + { + using (var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary())) + b.CreateFolder(); + + info.OutputOK(); + } + + private void UploadFile(string uri, RequestInfo info) + { + var data = info.Request.QueryString["data"].Value; + var remotename = info.Request.QueryString["filename"].Value; + + using (var ms = new System.IO.MemoryStream()) + using (var b = Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary())) + { + using (var tf = new Library.Utility.TempFile()) + { + System.IO.File.WriteAllText(tf, data); + b.PutAsync(remotename, tf, CancellationToken.None).Wait(); + } + } + + info.OutputOK(); + } + + private void ListFolder(string uri, RequestInfo info) + { + using (var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(uri, new Dictionary())) + info.OutputOK(b.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(); + + try + { + var uri = new Library.Utility.Uri(url); + var qp = uri.QueryParameters; + + var opts = new Dictionary(); + foreach (var k in qp.Keys.Cast()) + opts[k] = qp[k]; + + foreach (var n in modules) + n.Configure(opts); + + using (var b = Duplicati.Library.DynamicLoader.BackendLoader.GetBackend(url, new Dictionary())) + b.Test(); + + info.OutputOK(); + } + catch (Duplicati.Library.Interface.FolderMissingException) + { + info.ReportServerError("missing-folder"); + } + catch (Duplicati.Library.Utility.SslCertificateValidator.InvalidCertificateException icex) + { + if (string.IsNullOrWhiteSpace(icex.Certificate)) + info.ReportServerError(icex.Message); + else + info.ReportServerError("incorrect-cert:" + icex.Certificate); + } + catch (Duplicati.Library.Utility.HostKeyException hex) + { + if (string.IsNullOrWhiteSpace(hex.ReportedHostKey)) + info.ReportServerError(hex.Message); + else + { + info.ReportServerError(string.Format( + @"incorrect-host-key:""{0}"", accepted-host-key:""{1}""", + hex.ReportedHostKey, + hex.AcceptedHostKey + )); + } + } + finally + { + foreach (var n in modules) + if (n is IDisposable disposable) + disposable.Dispose(); + } + } + + public void GET(string key, RequestInfo info) + { + var parts = (key ?? "").Split(new char[] { '/' }, 2); + + if (parts.Length <= 1) + { + info.ReportClientError("No url or operation supplied", System.Net.HttpStatusCode.BadRequest); + return; + } + + var url = Library.Utility.Uri.UrlDecode(parts.First()); + var operation = parts.Last().ToLowerInvariant(); + + switch (operation) + { + case "dbpath": + LocateDbUri(url, info); + return; + case "list": + ListFolder(url, info); + return; + case "create": + CreateFolder(url, info); + return; + case "test": + TestConnection(url, info); + return; + default: + info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest); + return; + } + } + + public void POST(string key, RequestInfo info) + { + string url = 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(); + + switch (key) + { + case "dbpath": + LocateDbUri(url, info); + return; + case "list": + ListFolder(url, info); + return; + case "create": + CreateFolder(url, info); + return; + case "put": + UploadFile(url, info); + return; + case "test": + TestConnection(url, info); + return; + default: + info.ReportClientError("No such method", System.Net.HttpStatusCode.BadRequest); + return; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs b/Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs rename to Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs index 5258a871d..2069b6cf9 100644 --- a/Duplicati/Server/WebServer/RESTMethods/RequestInfo.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/RequestInfo.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; namespace Duplicati.Server.WebServer.RESTMethods diff --git a/Duplicati/Server/WebServer/RESTMethods/ServerSetting.cs b/Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs similarity index 85% rename from Duplicati/Server/WebServer/RESTMethods/ServerSetting.cs rename to Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs index 74a92def2..418589837 100644 --- a/Duplicati/Server/WebServer/RESTMethods/ServerSetting.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/ServerSetting.cs @@ -18,7 +18,7 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - +using Duplicati.Library.RestAPI; using System; using System.Collections.Generic; using System.Linq; @@ -37,13 +37,13 @@ namespace Duplicati.Server.WebServer.RESTMethods if (key.Equals("server-ssl-certificate", StringComparison.OrdinalIgnoreCase) || key.Equals("ServerSSLCertificate", StringComparison.OrdinalIgnoreCase)) { - info.OutputOK(Program.DataConnection.ApplicationSettings.ServerSSLCertificate == null ? "False" : "True"); + info.OutputOK(FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate == null ? "False" : "True"); return; } if (key.StartsWith("--", StringComparison.Ordinal)) { - var prop = Program.DataConnection.Settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase)); + var prop = FIXMEGlobal.DataConnection.Settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase)); info.OutputOK(prop == null ? null : prop.Value); } else @@ -52,7 +52,7 @@ namespace Duplicati.Server.WebServer.RESTMethods if (prop == null) info.OutputError(null, System.Net.HttpStatusCode.NotFound, "Not found"); else - info.OutputOK(prop.GetValue(Program.DataConnection.ApplicationSettings)); + info.OutputOK(prop.GetValue(FIXMEGlobal.DataConnection.ApplicationSettings)); } } @@ -72,7 +72,7 @@ namespace Duplicati.Server.WebServer.RESTMethods if (key.StartsWith("--", StringComparison.Ordinal)) { - var settings = Program.DataConnection.Settings.ToList(); + var settings = FIXMEGlobal.DataConnection.Settings.ToList(); var prop = settings.FirstOrDefault(x => string.Equals(key, x.Name, StringComparison.OrdinalIgnoreCase)); if (prop == null) @@ -80,7 +80,7 @@ namespace Duplicati.Server.WebServer.RESTMethods else prop.Value = info.Request.Form["data"].Value; - Program.DataConnection.Settings = settings.ToArray(); + FIXMEGlobal.DataConnection.Settings = settings.ToArray(); info.OutputOK(prop == null ? null : prop.Value); } @@ -93,7 +93,7 @@ namespace Duplicati.Server.WebServer.RESTMethods { var dict = new Dictionary(); dict[key] = info.Request.Form["data"].Value; - Program.DataConnection.ApplicationSettings.UpdateSettings(dict, false); + FIXMEGlobal.DataConnection.ApplicationSettings.UpdateSettings(dict, false); info.OutputOK(); } } diff --git a/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs b/Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs similarity index 86% rename from Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs rename to Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs index 40c9d81c8..77258f307 100644 --- a/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/ServerSettings.cs @@ -1,131 +1,136 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Linq; -using System.Collections.Generic; -using Duplicati.Server.Database; -using System.IO; -using Duplicati.Server.Serialization; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class ServerSettings : IRESTMethodGET, IRESTMethodPATCH, IRESTMethodDocumented - { - public void GET(string key, RequestInfo info) - { - // Join server settings and global settings - var adv_props = - Program.DataConnection.GetSettings(Database.Connection.SERVER_SETTINGS_ID) - .Where(x => !string.IsNullOrWhiteSpace(x.Name)) - .Union( - Program.DataConnection.Settings - .Where(x => !string.IsNullOrWhiteSpace(x.Name) && x.Name.StartsWith("--", StringComparison.Ordinal)) - ); - - var dict = new Dictionary(); - foreach (var n in adv_props) - dict[n.Name] = n.Value; - - string sslcert; - dict.TryGetValue("server-ssl-certificate", out sslcert); - dict["server-ssl-certificate"] = (!string.IsNullOrWhiteSpace(sslcert)).ToString(); - - info.OutputOK(dict); - } - - public void PATCH(string key, RequestInfo info) - { - string str = info.Request.Form["data"].Value; - - if (string.IsNullOrWhiteSpace(str)) - str = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8).ReadToEnd(); - - if (string.IsNullOrWhiteSpace(str)) - { - info.ReportClientError("Missing data object", System.Net.HttpStatusCode.BadRequest); - return; - } - - Dictionary data = null; - try - { - data = Serializer.Deserialize>(new StringReader(str)); - if (data == null) - { - info.ReportClientError("Data object had no entry", System.Net.HttpStatusCode.BadRequest); - return; - } - - // Split into server settings and global settings - - var serversettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key)).ToDictionary(x => x.Key, x => x.Key.StartsWith("--", StringComparison.Ordinal) ? null : x.Value); - var globalsettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Key.StartsWith("--", StringComparison.Ordinal)); - - serversettings.Remove("server-ssl-certificate"); - serversettings.Remove("ServerSSLCertificate"); - - if (serversettings.Any()) - Program.DataConnection.ApplicationSettings.UpdateSettings(serversettings, false); - - if (globalsettings.Any()) - { - // Update based on inputs - var existing = Program.DataConnection.Settings.ToDictionary(x => x.Name, x => x); - foreach (var g in globalsettings) - if (g.Value == null) - existing.Remove(g.Key); - else - { - if (existing.ContainsKey(g.Key)) - existing[g.Key].Value = g.Value; - else - existing[g.Key] = new Setting() { Name = g.Key, Value = g.Value }; - } - - Program.DataConnection.Settings = existing.Select(x => x.Value).ToArray(); - } - - info.OutputOK(); - } - catch (Exception ex) - { - if (data == null) - info.ReportClientError(string.Format("Unable to parse data object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest); - else - info.ReportClientError(string.Format("Unable to save settings: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError); - } - } - - public string Description { get { return "Return a list of settings for the server"; } } - - public IEnumerable> Types - { - get - { - return new KeyValuePair[] { - new KeyValuePair(HttpServer.Method.Get, typeof(Database.ServerSettings)) - }; - } - } - } -} - +// Copyright (C) 2024, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +using System; +using System.Linq; +using System.Collections.Generic; +using Duplicati.Server.Database; +using System.IO; +using Duplicati.Server.Serialization; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class ServerSettings : IRESTMethodGET, IRESTMethodPATCH, IRESTMethodDocumented + { + public void GET(string key, RequestInfo info) + { + // Join server settings and global settings + var adv_props = + FIXMEGlobal.DataConnection.GetSettings(Database.Connection.SERVER_SETTINGS_ID) + .Where(x => !string.IsNullOrWhiteSpace(x.Name)) + .Union( + FIXMEGlobal.DataConnection.Settings + .Where(x => !string.IsNullOrWhiteSpace(x.Name) && x.Name.StartsWith("--", StringComparison.Ordinal)) + ); + + var dict = new Dictionary(); + foreach (var n in adv_props) + dict[n.Name] = n.Value; + + string sslcert; + dict.TryGetValue("server-ssl-certificate", out sslcert); + dict["server-ssl-certificate"] = (!string.IsNullOrWhiteSpace(sslcert)).ToString(); + + info.OutputOK(dict); + } + + public void PATCH(string key, RequestInfo info) + { + string str = info.Request.Form["data"].Value; + + if (string.IsNullOrWhiteSpace(str)) + str = System.Threading.Tasks.Task.Run(async () => + { + using (var sr = new System.IO.StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true)) + + return await sr.ReadToEndAsync(); + }).GetAwaiter().GetResult(); + + if (string.IsNullOrWhiteSpace(str)) + { + info.ReportClientError("Missing data object", System.Net.HttpStatusCode.BadRequest); + return; + } + + Dictionary data = null; + try + { + data = Serializer.Deserialize>(new StringReader(str)); + if (data == null) + { + info.ReportClientError("Data object had no entry", System.Net.HttpStatusCode.BadRequest); + return; + } + + // Split into server settings and global settings + + var serversettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key)).ToDictionary(x => x.Key, x => x.Key.StartsWith("--", StringComparison.Ordinal) ? null : x.Value); + var globalsettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Key.StartsWith("--", StringComparison.Ordinal)); + + serversettings.Remove("server-ssl-certificate"); + serversettings.Remove("ServerSSLCertificate"); + + if (serversettings.Any()) + FIXMEGlobal.DataConnection.ApplicationSettings.UpdateSettings(serversettings, false); + + if (globalsettings.Any()) + { + // Update based on inputs + var existing = FIXMEGlobal.DataConnection.Settings.ToDictionary(x => x.Name, x => x); + foreach (var g in globalsettings) + if (g.Value == null) + existing.Remove(g.Key); + else + { + if (existing.ContainsKey(g.Key)) + existing[g.Key].Value = g.Value; + else + existing[g.Key] = new Setting() { Name = g.Key, Value = g.Value }; + } + + FIXMEGlobal.DataConnection.Settings = existing.Select(x => x.Value).ToArray(); + } + + info.OutputOK(); + } + catch (Exception ex) + { + if (data == null) + info.ReportClientError(string.Format("Unable to parse data object: {0}", ex.Message), System.Net.HttpStatusCode.BadRequest); + else + info.ReportClientError(string.Format("Unable to save settings: {0}", ex.Message), System.Net.HttpStatusCode.InternalServerError); + } + } + + public string Description { get { return "Return a list of settings for the server"; } } + + public IEnumerable> Types + { + get + { + return new KeyValuePair[] { + new KeyValuePair(HttpServer.Method.Get, typeof(Database.ServerSettings)) + }; + } + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/ServerState.cs b/Duplicati.Library.RestAPI/RESTMethods/ServerState.cs similarity index 89% rename from Duplicati/Server/WebServer/RESTMethods/ServerState.cs rename to Duplicati.Library.RestAPI/RESTMethods/ServerState.cs index 2bc6939b4..bd1fa914a 100644 --- a/Duplicati/Server/WebServer/RESTMethods/ServerState.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/ServerState.cs @@ -18,7 +18,7 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - +using Duplicati.Library.RestAPI; using System; using System.Collections.Generic; @@ -32,7 +32,7 @@ namespace Duplicati.Server.WebServer.RESTMethods long id = 0; long.TryParse(key, out id); - if (info.LongPollCheck(Program.StatusEventNotifyer, ref id, out isError)) + if (info.LongPollCheck(FIXMEGlobal.StatusEventNotifyer, ref id, out isError)) { //Make sure we do not report a higher number than the eventnotifier says var st = new Serializable.ServerStatus(); @@ -64,20 +64,20 @@ namespace Duplicati.Server.WebServer.RESTMethods return; } if (ts.TotalMilliseconds > 0) - Program.LiveControl.Pause(ts); + FIXMEGlobal.LiveControl.Pause(ts); else - Program.LiveControl.Pause(); + FIXMEGlobal.LiveControl.Pause(); } else { - Program.LiveControl.Pause(); + FIXMEGlobal.LiveControl.Pause(); } info.OutputOK(); return; case "resume": - Program.LiveControl.Resume(); + FIXMEGlobal.LiveControl.Resume(); info.OutputOK(); return; diff --git a/Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs b/Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs similarity index 96% rename from Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs rename to Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs index 7a0c57686..465396ea9 100644 --- a/Duplicati/Server/WebServer/RESTMethods/SystemInfo.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/SystemInfo.cs @@ -18,12 +18,12 @@ // 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 { @@ -76,7 +76,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, + StartedBy = FIXMEGlobal.Origin, BaseVersionName = Duplicati.Library.AutoUpdater.UpdaterManager.BaseVersion.Displayname, DefaultUpdateChannel = Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel, DefaultUsageReportLevel = Duplicati.Library.UsageReporter.Reporter.DefaultReportLevel, diff --git a/Duplicati/Server/WebServer/RESTMethods/SystemWideSettings.cs b/Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/SystemWideSettings.cs rename to Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs index b2cc4369a..09eebe706 100644 --- a/Duplicati/Server/WebServer/RESTMethods/SystemWideSettings.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/SystemWideSettings.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Collections.Generic; using Duplicati.Library.Interface; diff --git a/Duplicati/Server/WebServer/RESTMethods/Tags.cs b/Duplicati.Library.RestAPI/RESTMethods/Tags.cs similarity index 90% rename from Duplicati/Server/WebServer/RESTMethods/Tags.cs rename to Duplicati.Library.RestAPI/RESTMethods/Tags.cs index c1deecce8..90333250a 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Tags.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Tags.cs @@ -18,7 +18,7 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - +using Duplicati.Library.RestAPI; using System; using System.Collections.Generic; using System.Globalization; @@ -39,7 +39,7 @@ namespace Duplicati.Server.WebServer.RESTMethods select n.Key.ToLower(CultureInfo.InvariantCulture); // Append all known tags - r = r.Union(from n in Program.DataConnection.Backups select n.Tags into p from x in p select x.ToLower(CultureInfo.InvariantCulture)); + r = r.Union(from n in FIXMEGlobal.DataConnection.Backups select n.Tags into p from x in p select x.ToLower(CultureInfo.InvariantCulture)); info.OutputOK(r); } diff --git a/Duplicati/Server/WebServer/RESTMethods/Task.cs b/Duplicati.Library.RestAPI/RESTMethods/Task.cs similarity index 89% rename from Duplicati/Server/WebServer/RESTMethods/Task.cs rename to Duplicati.Library.RestAPI/RESTMethods/Task.cs index 7b0960df0..1af624e61 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Task.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Task.cs @@ -18,96 +18,96 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - -using System; -using System.Linq; -using System.Collections.Generic; - -namespace Duplicati.Server.WebServer.RESTMethods -{ - public class Task : IRESTMethodGET, IRESTMethodPOST - { - public void GET(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }, 2); - long taskid; - if (long.TryParse(parts.FirstOrDefault(), out taskid)) - { - var task = Program.WorkThread.CurrentTask; - var tasks = Program.WorkThread.CurrentTasks; - - if (task != null && task.TaskID == taskid) - { - info.OutputOK(new { Status = "Running" }); - return; - } - - if (tasks.FirstOrDefault(x => x.TaskID == taskid) == null) - { - KeyValuePair[] matches; - lock(Program.MainLock) - matches = Program.TaskResultCache.Where(x => x.Key == taskid).ToArray(); - - if (matches.Length == 0) - info.ReportClientError("No such task found", System.Net.HttpStatusCode.NotFound); - else - info.OutputOK(new { - Status = matches[0].Value == null ? "Completed" : "Failed", - ErrorMessage = matches[0].Value == null ? null : matches[0].Value.Message, - Exception = matches[0].Value == null ? null : matches[0].Value.ToString() - }); - } - else - { - info.OutputOK(new { Status = "Waiting" }); - } - } - else - { - info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest); - } - } - - public void POST(string key, RequestInfo info) - { - var parts = (key ?? "").Split(new char[] { '/' }, 2); - long taskid; - if (parts.Length == 2 && long.TryParse(parts.First(), out taskid)) - { - var task = Program.WorkThread.CurrentTask; - var tasks = Program.WorkThread.CurrentTasks; - - if (task != null) - tasks.Insert(0, task); - - task = tasks.FirstOrDefault(x => x.TaskID == taskid); - if (task == null) - { - info.ReportClientError("No such task", System.Net.HttpStatusCode.NotFound); - return; - } - - switch (parts.Last().ToLowerInvariant()) - { - case "stopaftercurrentfile": - task.Stop(allowCurrentFileToFinish: true); - info.OutputOK(); - return; - - case "stopnow": - task.Stop(allowCurrentFileToFinish: false); - info.OutputOK(); - return; - - case "abort": - task.Abort(); - info.OutputOK(); - return; - } - } - - info.ReportClientError("Invalid or missing task id", System.Net.HttpStatusCode.NotFound); - } - } -} - +using System; +using System.Linq; +using System.Collections.Generic; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.WebServer.RESTMethods +{ + public class Task : IRESTMethodGET, IRESTMethodPOST + { + public void GET(string key, RequestInfo info) + { + var parts = (key ?? "").Split(new char[] { '/' }, 2); + long taskid; + if (long.TryParse(parts.FirstOrDefault(), out taskid)) + { + var task = FIXMEGlobal.WorkThread.CurrentTask; + var tasks = FIXMEGlobal.WorkThread.CurrentTasks; + + if (task != null && task.TaskID == taskid) + { + info.OutputOK(new { Status = "Running" }); + return; + } + + if (tasks.FirstOrDefault(x => x.TaskID == taskid) == null) + { + KeyValuePair[] matches; + lock(FIXMEGlobal.MainLock) + matches = FIXMEGlobal.TaskResultCache.Where(x => x.Key == taskid).ToArray(); + + if (matches.Length == 0) + info.ReportClientError("No such task found", System.Net.HttpStatusCode.NotFound); + else + info.OutputOK(new { + Status = matches[0].Value == null ? "Completed" : "Failed", + ErrorMessage = matches[0].Value == null ? null : matches[0].Value.Message, + Exception = matches[0].Value == null ? null : matches[0].Value.ToString() + }); + } + else + { + info.OutputOK(new { Status = "Waiting" }); + } + } + else + { + info.ReportClientError("Invalid request", System.Net.HttpStatusCode.BadRequest); + } + } + + public void POST(string key, RequestInfo info) + { + var parts = (key ?? "").Split(new char[] { '/' }, 2); + long taskid; + if (parts.Length == 2 && long.TryParse(parts.First(), out taskid)) + { + var task = FIXMEGlobal.WorkThread.CurrentTask; + var tasks = FIXMEGlobal.WorkThread.CurrentTasks; + + if (task != null) + tasks.Insert(0, task); + + task = tasks.FirstOrDefault(x => x.TaskID == taskid); + if (task == null) + { + info.ReportClientError("No such task", System.Net.HttpStatusCode.NotFound); + return; + } + + switch (parts.Last().ToLowerInvariant()) + { + case "stopaftercurrentfile": + task.Stop(allowCurrentFileToFinish: true); + info.OutputOK(); + return; + + case "stopnow": + task.Stop(allowCurrentFileToFinish: false); + info.OutputOK(); + return; + + case "abort": + task.Abort(); + info.OutputOK(); + return; + } + } + + info.ReportClientError("Invalid or missing task id", System.Net.HttpStatusCode.NotFound); + } + } +} + diff --git a/Duplicati/Server/WebServer/RESTMethods/Tasks.cs b/Duplicati.Library.RestAPI/RESTMethods/Tasks.cs similarity index 88% rename from Duplicati/Server/WebServer/RESTMethods/Tasks.cs rename to Duplicati.Library.RestAPI/RESTMethods/Tasks.cs index 84d4b9a15..cd900c522 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Tasks.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Tasks.cs @@ -18,7 +18,7 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - +using Duplicati.Library.RestAPI; using System; using System.Collections.Generic; @@ -28,8 +28,8 @@ namespace Duplicati.Server.WebServer.RESTMethods { public void GET(string key, RequestInfo info) { - var cur = Program.WorkThread.CurrentTask; - var n = Program.WorkThread.CurrentTasks; + var cur = FIXMEGlobal.WorkThread.CurrentTask; + var n = FIXMEGlobal.WorkThread.CurrentTasks; if (cur != null) n.Insert(0, cur); diff --git a/Duplicati/Server/WebServer/RESTMethods/UISettings.cs b/Duplicati.Library.RestAPI/RESTMethods/UISettings.cs similarity index 86% rename from Duplicati/Server/WebServer/RESTMethods/UISettings.cs rename to Duplicati.Library.RestAPI/RESTMethods/UISettings.cs index 440aa9825..37330516d 100644 --- a/Duplicati/Server/WebServer/RESTMethods/UISettings.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/UISettings.cs @@ -18,11 +18,11 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Collections.Generic; using Duplicati.Server.Serialization; using System.IO; +using Duplicati.Library.RestAPI; namespace Duplicati.Server.WebServer.RESTMethods { @@ -32,11 +32,11 @@ namespace Duplicati.Server.WebServer.RESTMethods { if (string.IsNullOrWhiteSpace(key)) { - info.OutputOK(Program.DataConnection.GetUISettingsSchemes()); + info.OutputOK(FIXMEGlobal.DataConnection.GetUISettingsSchemes()); } else { - info.OutputOK(Program.DataConnection.GetUISettings(key)); + info.OutputOK(FIXMEGlobal.DataConnection.GetUISettings(key)); } } @@ -71,9 +71,9 @@ namespace Duplicati.Server.WebServer.RESTMethods } if (info.Request.Method == "POST") - Program.DataConnection.SetUISettings(key, data); + FIXMEGlobal.DataConnection.SetUISettings(key, data); else - Program.DataConnection.UpdateUISettings(key, data); + FIXMEGlobal.DataConnection.UpdateUISettings(key, data); info.OutputOK(); } diff --git a/Duplicati/Server/WebServer/RESTMethods/Updates.cs b/Duplicati.Library.RestAPI/RESTMethods/Updates.cs similarity index 84% rename from Duplicati/Server/WebServer/RESTMethods/Updates.cs rename to Duplicati.Library.RestAPI/RESTMethods/Updates.cs index 34b782e14..5eec5a43b 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Updates.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/Updates.cs @@ -18,7 +18,7 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - +using Duplicati.Library.RestAPI; using System; namespace Duplicati.Server.WebServer.RESTMethods @@ -30,23 +30,23 @@ namespace Duplicati.Server.WebServer.RESTMethods switch ((key ?? "").ToLowerInvariant()) { case "check": - Program.UpdatePoller.CheckNow(); + FIXMEGlobal.UpdatePoller.CheckNow(); info.OutputOK(); return; case "install": - Program.UpdatePoller.InstallUpdate(); + FIXMEGlobal.UpdatePoller.InstallUpdate(); info.OutputOK(); return; case "activate": - if (Program.WorkThread.CurrentTask != null || Program.WorkThread.CurrentTasks.Count != 0) + if (FIXMEGlobal.WorkThread.CurrentTask != null || FIXMEGlobal.WorkThread.CurrentTasks.Count != 0) { info.ReportServerError("Cannot activate update while task is running or scheduled"); } else { - Program.UpdatePoller.ActivateUpdate(); + FIXMEGlobal.UpdatePoller.ActivateUpdate(); info.OutputOK(); } return; diff --git a/Duplicati/Server/WebServer/RESTMethods/WebModule.cs b/Duplicati.Library.RestAPI/RESTMethods/WebModule.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/WebModule.cs rename to Duplicati.Library.RestAPI/RESTMethods/WebModule.cs index 09f8bc970..676b3c963 100644 --- a/Duplicati/Server/WebServer/RESTMethods/WebModule.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/WebModule.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Linq; diff --git a/Duplicati/Server/WebServer/RESTMethods/WebModules.cs b/Duplicati.Library.RestAPI/RESTMethods/WebModules.cs similarity index 97% rename from Duplicati/Server/WebServer/RESTMethods/WebModules.cs rename to Duplicati.Library.RestAPI/RESTMethods/WebModules.cs index 536c33800..9f90d91db 100644 --- a/Duplicati/Server/WebServer/RESTMethods/WebModules.cs +++ b/Duplicati.Library.RestAPI/RESTMethods/WebModules.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Linq; diff --git a/Duplicati/Server/Runner.cs b/Duplicati.Library.RestAPI/Runner.cs similarity index 89% rename from Duplicati/Server/Runner.cs rename to Duplicati.Library.RestAPI/Runner.cs index 2cd9a3e90..dcc89baf3 100644 --- a/Duplicati/Server/Runner.cs +++ b/Duplicati.Library.RestAPI/Runner.cs @@ -1,970 +1,969 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Linq; -using System.Collections.Generic; -using Duplicati.Library.Interface; -using Duplicati.Server.Serialization; - -namespace Duplicati.Server -{ - public static class Runner - { - public interface IRunnerData : Duplicati.Server.Serialization.Interface.IQueuedTask - { - Duplicati.Server.Serialization.Interface.IBackup Backup { get; } - IDictionary ExtraOptions { get; } - string[] FilterStrings { get; } - void Stop(bool allowCurrentFileToFinish); - void Abort(); - void Pause(); - void Resume(); - void UpdateThrottleSpeed(); - void SetController(Duplicati.Library.Main.Controller controller); - } - - private class RunnerData : IRunnerData - { - private static long RunnerTaskID = 1; - - public Duplicati.Server.Serialization.DuplicatiOperation Operation { get; internal set; } - public Duplicati.Server.Serialization.Interface.IBackup Backup { get; internal set; } - public IDictionary ExtraOptions { get; internal set; } - public string[] FilterStrings { get; internal set; } - - public string BackupID { get { return Backup.ID; } } - public long TaskID { get { return m_taskID; } } - - internal Duplicati.Library.Main.Controller Controller { get; set; } - - public void SetController(Duplicati.Library.Main.Controller controller) - { - Controller = controller; - } - - public void Stop(bool allowCurrentFileToFinish) - { - var c = Controller; - if (c != null) - c.Stop(allowCurrentFileToFinish); - } - - public void Abort() - { - var c = Controller; - if (c != null) - c.Abort(); - } - - public void Pause() - { - var c = Controller; - if (c != null) - c.Pause(); - } - - public void Resume() - { - var c = Controller; - if (c != null) - c.Resume(); - } - - public long OriginalUploadSpeed { get; set; } - public long OriginalDownloadSpeed { get; set; } - - public void UpdateThrottleSpeed() - { - var controller = this.Controller; - if (controller == null) - return; - - var job_upload_throttle = this.OriginalUploadSpeed <= 0 ? long.MaxValue : this.OriginalUploadSpeed; - var job_download_throttle = this.OriginalDownloadSpeed <= 0 ? long.MaxValue : this.OriginalDownloadSpeed; - - var server_upload_throttle = long.MaxValue; - var server_download_throttle = long.MaxValue; - - try - { - if (!string.IsNullOrWhiteSpace(Program.DataConnection.ApplicationSettings.UploadSpeedLimit)) - server_upload_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(Program.DataConnection.ApplicationSettings.UploadSpeedLimit, "kb"); - } - catch { } - - try - { - if (!string.IsNullOrWhiteSpace(Program.DataConnection.ApplicationSettings.DownloadSpeedLimit)) - server_download_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(Program.DataConnection.ApplicationSettings.DownloadSpeedLimit, "kb"); - } - catch { } - - var upload_throttle = Math.Min(job_upload_throttle, server_upload_throttle); - var download_throttle = Math.Min(job_download_throttle, server_download_throttle); - - if (upload_throttle <= 0 || upload_throttle == long.MaxValue) - upload_throttle = 0; - - if (download_throttle <= 0 || download_throttle == long.MaxValue) - download_throttle = 0; - - controller.MaxUploadSpeed = upload_throttle; - controller.MaxDownloadSpeed = download_throttle; - } - - private readonly long m_taskID; - - public RunnerData() - { - m_taskID = System.Threading.Interlocked.Increment(ref RunnerTaskID); - } - } - - private class CustomRunnerTask : RunnerData - { - public readonly Action Run; - - public CustomRunnerTask(Action runner) - : base() - { - if (runner == null) - throw new ArgumentNullException(nameof(runner)); - Run = runner; - Operation = DuplicatiOperation.CustomRunner; - Backup = new Database.Backup(); - } - } - - public static IRunnerData CreateCustomTask(Action runner) - { - return new CustomRunnerTask(runner); - } - - public static IRunnerData CreateTask(Duplicati.Server.Serialization.DuplicatiOperation operation, Duplicati.Server.Serialization.Interface.IBackup backup, IDictionary extraOptions = null, string[] filterStrings = null) - { - return new RunnerData() { - Operation = operation, - Backup = backup, - ExtraOptions = extraOptions, - FilterStrings = filterStrings - }; - } - - public static IRunnerData CreateListTask(Duplicati.Server.Serialization.Interface.IBackup backup, string[] filters, bool onlyPrefix, bool allVersions, bool folderContents, DateTime time) - { - var dict = new Dictionary(); - if (onlyPrefix) - dict["list-prefix-only"] = "true"; - if (allVersions) - dict["all-versions"] = "true"; - if (time.Ticks > 0) - dict["time"] = Duplicati.Library.Utility.Utility.SerializeDateTime(time.ToUniversalTime()); - if (folderContents) - dict["list-folder-contents"] = "true"; - - return CreateTask( - DuplicatiOperation.List, - backup, - dict, - filters); - } - - public static IRunnerData CreateRestoreTask(Duplicati.Server.Serialization.Interface.IBackup backup, string[] filters, - DateTime time, string restoreTarget, bool overwrite, bool restore_permissions, - bool skip_metadata, string passphrase) - { - var dict = new Dictionary - { - ["time"] = Library.Utility.Utility.SerializeDateTime(time.ToUniversalTime()), - ["overwrite"] = overwrite? Boolean.TrueString : Boolean.FalseString, - ["restore-permissions"] = restore_permissions ? Boolean.TrueString : Boolean.FalseString, - ["skip-metadata"] = skip_metadata ? Boolean.TrueString : Boolean.FalseString, - ["allow-passphrase-change"] = Boolean.TrueString - }; - if (!string.IsNullOrWhiteSpace(restoreTarget)) - dict["restore-path"] = SpecialFolders.ExpandEnvironmentVariables(restoreTarget); - if (!(passphrase is null)) - dict["passphrase"] = passphrase; - - return CreateTask( - DuplicatiOperation.Restore, - backup, - dict, - filters); - } - private class MessageSink : Duplicati.Library.Main.IMessageSink - { - private class ProgressState : Server.Serialization.Interface.IProgressEventData - { - private readonly string m_backupID; - private readonly long m_taskID; - - internal Duplicati.Library.Main.BackendActionType m_backendAction; - internal string m_backendPath; - internal long m_backendFileSize; - internal long m_backendFileProgress; - internal long m_backendSpeed; - internal bool m_backendIsBlocking; - - internal string m_currentFilename; - internal long m_currentFilesize; - internal long m_currentFileoffset; - internal bool m_currentFilecomplete; - - internal Duplicati.Library.Main.OperationPhase m_phase; - internal float m_overallProgress; - internal long m_processedFileCount; - internal long m_processedFileSize; - internal long m_totalFileCount; - internal long m_totalFileSize; - internal bool m_stillCounting; - - public ProgressState(long taskId, string backupId) - { - m_backupID = backupId; - m_taskID = taskId; - } - - internal ProgressState Clone() - { - return (ProgressState)this.MemberwiseClone(); - } - - #region IProgressEventData implementation - public string BackupID { get { return m_backupID; } } - public long TaskID { get { return m_taskID; } } - public string BackendAction { get { return m_backendAction.ToString(); } } - public string BackendPath { get { return m_backendPath; } } - public long BackendFileSize { get { return m_backendFileSize; } } - public long BackendFileProgress { get { return m_backendFileProgress; } } - public long BackendSpeed { get { return m_backendSpeed; } } - public bool BackendIsBlocking { get { return m_backendIsBlocking; } } - public string CurrentFilename { get { return m_currentFilename; } } - public long CurrentFilesize { get { return m_currentFilesize; } } - public long CurrentFileoffset { get { return m_currentFileoffset; } } - public bool CurrentFilecomplete { get { return m_currentFilecomplete; } } - public string Phase { get { return m_phase.ToString(); } } - public float OverallProgress { get { return m_overallProgress; } } - public long ProcessedFileCount { get { return m_processedFileCount; } } - public long ProcessedFileSize { get { return m_processedFileSize; } } - public long TotalFileCount { get { return m_totalFileCount; } } - public long TotalFileSize { get { return m_totalFileSize; } } - public bool StillCounting { get { return m_stillCounting; } } - #endregion - } - - private readonly ProgressState m_state; - private Duplicati.Library.Main.IBackendProgress m_backendProgress; - private Duplicati.Library.Main.IOperationProgress m_operationProgress; - private readonly object m_lock = new object(); - - public MessageSink(long taskId, string backupId) - { - m_state = new ProgressState(taskId, backupId); - } - - public Server.Serialization.Interface.IProgressEventData Copy() - { - lock(m_lock) - { - if (m_backendProgress != null) - m_backendProgress.Update(out m_state.m_backendAction, out m_state.m_backendPath, out m_state.m_backendFileSize, out m_state.m_backendFileProgress, out m_state.m_backendSpeed, out m_state.m_backendIsBlocking); - if (m_operationProgress != null) - { - m_operationProgress.UpdateFile(out m_state.m_currentFilename, out m_state.m_currentFilesize, out m_state.m_currentFileoffset, out m_state.m_currentFilecomplete); - m_operationProgress.UpdateOverall(out m_state.m_phase, out m_state.m_overallProgress, out m_state.m_processedFileCount, out m_state.m_processedFileSize, out m_state.m_totalFileCount, out m_state.m_totalFileSize, out m_state.m_stillCounting); - } - - return m_state.Clone(); - } - } - - #region IMessageSink implementation - public void BackendEvent(Duplicati.Library.Main.BackendActionType action, Duplicati.Library.Main.BackendEventType type, string path, long size) - { - lock(m_lock) - { - m_state.m_backendAction = action; - m_state.m_backendPath = path; - if (type == Duplicati.Library.Main.BackendEventType.Started) - m_state.m_backendFileSize = size; - else if (type == Duplicati.Library.Main.BackendEventType.Progress) - m_state.m_backendFileProgress = size; - else - { - m_state.m_backendFileSize = 0; - m_state.m_backendFileProgress = 0; - m_state.m_backendSpeed = 0; - } - } - } - - public void SetBackendProgress(Library.Main.IBackendProgress progress) - { - lock (m_lock) - m_backendProgress = progress; - } - - public void SetOperationProgress(Library.Main.IOperationProgress progress) - { - lock (m_lock) - m_operationProgress = progress; - } - - public void WriteMessage(Library.Logging.LogEntry entry) - { - // Do nothing. Implementation needed for ILogDestination interface. - } - #endregion - } - - public static string GetCommandLine(IRunnerData data) - { - var backup = data.Backup; - - var options = ApplyOptions(backup, GetCommonOptions()); - if (data.ExtraOptions != null) - foreach(var k in data.ExtraOptions) - options[k.Key] = k.Value; - - var cf = Program.DataConnection.Filters; - var bf = backup.Filters; - - var sources = - (from n in backup.Sources - let p = SpecialFolders.ExpandEnvironmentVariables(n) - where !string.IsNullOrWhiteSpace(p) - select p).ToArray(); - - var exe = - System.IO.Path.Combine( - Library.AutoUpdater.UpdaterManager.InstalledBaseDir, - System.IO.Path.GetFileName( - typeof(Duplicati.CommandLine.Commands).Assembly.Location - ) - ); - - var cmd = new System.Text.StringBuilder(); - if (Library.Utility.Utility.IsMono) - cmd.Append("mono "); - - cmd.Append(Library.Utility.Utility.WrapAsCommandLine(new string[] { exe, "backup", backup.TargetURL }, false)); - - cmd.Append(" "); - cmd.Append(Library.Utility.Utility.WrapAsCommandLine(sources, true)); - - // TODO: We should check each option to see if it is a path, and allow expansion on that - foreach(var opt in options) - cmd.AppendFormat(" --{0}={1}", opt.Key, Library.Utility.Utility.WrapCommandLineElement(opt.Value, false)); - - if (cf != null) - foreach(var f in cf) - cmd.AppendFormat(" --{0}={1}", f.Include ? "include" : "exclude", Library.Utility.Utility.WrapCommandLineElement(f.Expression, true)); - - if (bf != null) - foreach(var f in bf) - cmd.AppendFormat(" --{0}={1}", f.Include ? "include" : "exclude", Library.Utility.Utility.WrapCommandLineElement(f.Expression, true)); - - return cmd.ToString(); - } - - public static string[] GetCommandLineParts(IRunnerData data) - { - var backup = data.Backup; - - var options = ApplyOptions(backup, GetCommonOptions()); - if (data.ExtraOptions != null) - foreach (var k in data.ExtraOptions) - options[k.Key] = k.Value; - - var cf = Program.DataConnection.Filters; - var bf = backup.Filters; - - var sources = - (from n in backup.Sources - let p = SpecialFolders.ExpandEnvironmentVariables(n) - where !string.IsNullOrWhiteSpace(p) - select p).ToArray(); - - var parts = new List - { - backup.TargetURL - }; - parts.AddRange(sources); - - foreach (var opt in options) - parts.Add(string.Format("--{0}={1}", opt.Key, opt.Value)); - - if (cf != null) - foreach (var f in cf) - parts.Add(string.Format("--{0}={1}", f.Include ? "include" : "exclude", f.Expression)); - - if (bf != null) - foreach (var f in bf) - parts.Add(string.Format("--{0}={1}", f.Include ? "include" : "exclude", f.Expression)); - - return parts.ToArray(); - } - - public static Duplicati.Library.Interface.IBasicResults Run(IRunnerData data, bool fromQueue) - { - if (data is CustomRunnerTask task) - { - try - { - var sink = new MessageSink(task.TaskID, null); - Program.GenerateProgressState = sink.Copy; - Program.StatusEventNotifyer.SignalNewEvent(); - - task.Run(sink); - } - catch(Exception ex) - { - Program.DataConnection.LogError(string.Empty, "Failed while executing custom task", ex); - } - - return null; - } - - var backup = data.Backup; - if (backup.Metadata == null) - { - backup.Metadata = new Dictionary(); - } - - Duplicati.Library.Utility.TempFolder tempfolder = null; - - try - { - var sink = new MessageSink(data.TaskID, backup.ID); - if (fromQueue) - { - Program.GenerateProgressState = () => sink.Copy(); - Program.StatusEventNotifyer.SignalNewEvent(); - } - - var options = ApplyOptions(backup, GetCommonOptions()); - if (data.ExtraOptions != null) - foreach(var k in data.ExtraOptions) - options[k.Key] = k.Value; - - // Pack in the system or task config for easy restore - if (data.Operation == DuplicatiOperation.Backup && options.ContainsKey("store-task-config")) - { - tempfolder = StoreTaskConfigAndGetTempFolder(data, options); - } - - // Attach a log scope that tags all messages to relay the TaskID and BackupID - using (Library.Logging.Log.StartScope(log => { - log[LogWriteHandler.LOG_EXTRA_TASKID] = data.TaskID.ToString(); - log[LogWriteHandler.LOG_EXTRA_BACKUPID] = data.BackupID; - })) - - using(tempfolder) - using(var controller = new Duplicati.Library.Main.Controller(backup.TargetURL, options, sink)) - { - try - { - if (options.ContainsKey("throttle-upload")) - ((RunnerData)data).OriginalUploadSpeed = Duplicati.Library.Utility.Sizeparser.ParseSize(options["throttle-upload"], "kb"); - } - catch { } - - try - { - if (options.ContainsKey("throttle-download")) - ((RunnerData)data).OriginalDownloadSpeed = Duplicati.Library.Utility.Sizeparser.ParseSize(options["throttle-download"], "kb"); - } - catch { } - - ((RunnerData)data).Controller = controller; - data.UpdateThrottleSpeed(); - - if (backup.Metadata.ContainsKey("LastCompactFinished")) - controller.LastCompact = Library.Utility.Utility.DeserializeDateTime(backup.Metadata["LastCompactFinished"]); - - if (backup.Metadata.ContainsKey("LastVacuumFinished")) - controller.LastVacuum = Library.Utility.Utility.DeserializeDateTime(backup.Metadata["LastVacuumFinished"]); - - switch (data.Operation) - { - case DuplicatiOperation.Backup: - { - var filter = ApplyFilter(backup, GetCommonFilter()); - var sources = - (from n in backup.Sources - let p = SpecialFolders.ExpandEnvironmentVariables(n) - where !string.IsNullOrWhiteSpace(p) - select p).ToArray(); - - var r = controller.Backup(sources, filter); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.List: - { - var r = controller.List(data.FilterStrings, null); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.Repair: - { - var r = controller.Repair(data.FilterStrings == null ? null : new Library.Utility.FilterExpression(data.FilterStrings)); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.RepairUpdate: - { - var r = controller.UpdateDatabaseWithVersions(); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.Remove: - { - var r = controller.Delete(); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.Restore: - { - var r = controller.Restore(data.FilterStrings); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.Verify: - { - var r = controller.Test(); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.Compact: - { - var r = controller.Compact(); - UpdateMetadata(backup, r); - return r; - } - case DuplicatiOperation.CreateReport: - { - using(var tf = new Duplicati.Library.Utility.TempFile()) - { - var r = controller.CreateLogDatabase(tf); - var tempid = Program.DataConnection.RegisterTempFile("create-bug-report", r.TargetPath, DateTime.Now.AddDays(3)); - - if (string.Equals(tf, r.TargetPath, Library.Utility.Utility.ClientFilenameStringComparison)) - tf.Protected = true; - - Program.DataConnection.RegisterNotification( - NotificationType.Information, - "Bugreport ready", - "Bugreport is ready for download", - null, - null, - "bug-report:created:" + tempid, - null, - "BugreportCreatedReady", - "", - (n, a) => n - ); - - return r; - } - } - - case DuplicatiOperation.ListRemote: - { - var r = controller.ListRemote(); - UpdateMetadata(backup, r); - return r; - } - - case DuplicatiOperation.Delete: - { - if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions, "delete-remote-files")) - controller.DeleteAllRemoteFiles(); - - if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions, "delete-local-db")) - { - string dbpath; - options.TryGetValue("dbpath", out dbpath); - - if (!string.IsNullOrWhiteSpace(dbpath) && System.IO.File.Exists(dbpath)) - System.IO.File.Delete(dbpath); - } - Program.DataConnection.DeleteBackup(backup); - Program.Scheduler.Reschedule(); - return null; - } - case DuplicatiOperation.Vacuum: - { - var r = controller.Vacuum(); - UpdateMetadata(backup, r); - return r; - } - default: - //TODO: Log this - return null; - } - } - } - catch (Exception ex) - { - Program.DataConnection.LogError(data.Backup.ID, string.Format("Failed while executing {0} \"{1}\" (id: {2})", data.Operation, data.Backup.Name, data.Backup.ID), ex); - UpdateMetadataError(data.Backup, ex); - Library.UsageReporter.Reporter.Report(ex); - - if (!fromQueue) - throw; - - return null; - } - finally - { - ((RunnerData)data).Controller = null; - } - } - - private static Duplicati.Library.Utility.TempFolder StoreTaskConfigAndGetTempFolder(IRunnerData data, Dictionary options) - { - var all_tasks = string.Equals(options["store-task-config"], "all", StringComparison.OrdinalIgnoreCase) || string.Equals(options["store-task-config"], "*", StringComparison.OrdinalIgnoreCase); - var this_task = Duplicati.Library.Utility.Utility.ParseBool(options["store-task-config"], false); - - options.Remove("store-task-config"); - - Duplicati.Library.Utility.TempFolder tempfolder = null; - if (all_tasks || this_task) - { - tempfolder = new Duplicati.Library.Utility.TempFolder(); - var temppath = System.IO.Path.Combine(tempfolder, "task-setup.json"); - using (var tempfile = Duplicati.Library.Utility.TempFile.WrapExistingFile(temppath)) - { - object taskdata = null; - if (all_tasks) - taskdata = Program.DataConnection.Backups.Where(x => !x.IsTemporary).Select(x => Program.DataConnection.PrepareBackupForExport(Program.DataConnection.GetBackup(x.ID))); - else - taskdata = new[] { Program.DataConnection.PrepareBackupForExport(data.Backup) }; - - using (var fs = System.IO.File.OpenWrite(tempfile)) - using (var sw = new System.IO.StreamWriter(fs, System.Text.Encoding.UTF8)) - Serializer.SerializeJson(sw, taskdata, true); - - tempfile.Protected = true; - - options.TryGetValue("control-files", out string controlfiles); - - if (string.IsNullOrWhiteSpace(controlfiles)) - controlfiles = tempfile; - else - controlfiles += System.IO.Path.PathSeparator + tempfile; - - options["control-files"] = controlfiles; - } - } - return tempfolder; - } - - private static void UpdateMetadataError(Duplicati.Server.Serialization.Interface.IBackup backup, Exception ex) - { - backup.Metadata["LastErrorDate"] = Library.Utility.Utility.SerializeDateTime(DateTime.UtcNow); - backup.Metadata["LastErrorMessage"] = ex.Message; - - if (!backup.IsTemporary) - Program.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null); - - string messageid = null; - if (ex is UserInformationException exception) - messageid = exception.HelpID; - - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.DataConnection.RegisterNotification( - NotificationType.Error, - backup.IsTemporary ? - "Error" : string.Format("Error while running {0}", backup.Name), - ex.Message, - ex, - backup.ID, - "backup:show-log", - null, - messageid, - null, - (n, a) => { - return a.FirstOrDefault(x => x.BackupID == backup.ID) ?? n; - } - ); - } - - private static void UpdateMetadataLastCompact(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.ICompactResults r) - { - if (r != null) - { - backup.Metadata["LastCompactDuration"] = r.Duration.ToString(); - backup.Metadata["LastCompactStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); - backup.Metadata["LastCompactFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); - } - } - - private static void UpdateMetadataLastVacuum(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IVacuumResults r) - { - if (r != null) - { - backup.Metadata["LastVacuumDuration"] = r.Duration.ToString(); - backup.Metadata["LastVacuumStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); - backup.Metadata["LastVacuumFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); - } - } - - private static void UpdateMetadata(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IParsedBackendStatistics r) - { - if (r != null) - { - backup.Metadata["LastBackupDate"] = Library.Utility.Utility.SerializeDateTime(r.LastBackupDate.ToUniversalTime()); - backup.Metadata["BackupListCount"] = r.BackupListCount.ToString(); - backup.Metadata["TotalQuotaSpace"] = r.TotalQuotaSpace.ToString(); - backup.Metadata["FreeQuotaSpace"] = r.FreeQuotaSpace.ToString(); - backup.Metadata["AssignedQuotaSpace"] = r.AssignedQuotaSpace.ToString(); - - backup.Metadata["TargetFilesSize"] = r.KnownFileSize.ToString(); - backup.Metadata["TargetFilesCount"] = r.KnownFileCount.ToString(); - backup.Metadata["TargetSizeString"] = Duplicati.Library.Utility.Utility.FormatSizeString(r.KnownFileSize); - } - } - - private static void UpdateMetadata(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IBasicResults result) - { - if (result is IRestoreResults r1) - { - backup.Metadata["LastRestoreDuration"] = r1.Duration.ToString(); - backup.Metadata["LastRestoreStarted"] = Library.Utility.Utility.SerializeDateTime(result.BeginTime.ToUniversalTime()); - backup.Metadata["LastRestoreFinished"] = Library.Utility.Utility.SerializeDateTime(result.EndTime.ToUniversalTime()); - } - - if (result is IParsedBackendStatistics r2 && !result.Interrupted) - { - UpdateMetadata(backup, r2); - } - - if (result is IBackendStatsticsReporter r3 && !result.Interrupted) - { - if (r3.BackendStatistics is IParsedBackendStatistics statistics) - UpdateMetadata(backup, statistics); - } - - if (result is ICompactResults r4 && !result.Interrupted) - { - UpdateMetadataLastCompact(backup, r4); - - if (r4.VacuumResults != null) - UpdateMetadataLastVacuum(backup, r4.VacuumResults); - } - - if (result is IVacuumResults r5 && !result.Interrupted) - { - UpdateMetadataLastVacuum(backup, r5); - } - - if (result is IBackupResults r) - { - if (!result.Interrupted) - { - backup.Metadata["SourceFilesSize"] = r.SizeOfExaminedFiles.ToString(); - backup.Metadata["SourceFilesCount"] = r.ExaminedFiles.ToString(); - backup.Metadata["SourceSizeString"] = Duplicati.Library.Utility.Utility.FormatSizeString(r.SizeOfExaminedFiles); - backup.Metadata["LastBackupStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); - backup.Metadata["LastBackupFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); - backup.Metadata["LastBackupDuration"] = r.Duration.ToString(); - - if (r.CompactResults != null) - UpdateMetadataLastCompact(backup, r.CompactResults); - - if (r.VacuumResults != null) - UpdateMetadataLastVacuum(backup, r.VacuumResults); - } - - if (r.FilesWithError > 0 || r.Warnings.Any() || r.Errors.Any()) - { - string message; - string titleType; - if (r.FilesWithError > 0) - { - message = $"Errors affected {r.FilesWithError} file(s)."; - titleType = "Error"; - } - else if (r.Errors.Any()) - { - message = r.Errors.Count() == 1 ? r.Errors.Single() : $"Encountered {r.Errors.Count()} errors."; - titleType = "Error"; - } - else - { - message = r.Warnings.Count() == 1 ? r.Warnings.Single() : $"Encountered {r.Warnings.Count()} warnings."; - titleType = "Warning"; - } - - Program.DataConnection.RegisterNotification( - r.FilesWithError == 0 && !r.Errors.Any() ? NotificationType.Warning : NotificationType.Error, - backup.IsTemporary ? "Warning" : $"{titleType} while running {backup.Name}", - message, - null, - backup.ID, - "backup:show-log", - null, - null, - null, - (n, a) => - { - var existing = a.FirstOrDefault(x => x.BackupID == backup.ID); - if (existing == null) - return n; - - if (existing.Type == NotificationType.Error) - return existing; - - return n; - } - ); - } - } - else if (result.ParsedResult != ParsedResultType.Success) - { - var type = result.ParsedResult == ParsedResultType.Warning - ? NotificationType.Warning - : NotificationType.Error; - - var title = result.ParsedResult == ParsedResultType.Warning - ? (backup.IsTemporary ? - "Warning" : string.Format("Warning while running {0}", backup.Name)) - : (backup.IsTemporary ? - "Error" : string.Format("Error while running {0}", backup.Name)); - - var message = result.ParsedResult == ParsedResultType.Warning - ? string.Format("Got {0} warning(s)", result.Warnings.Count()) - : string.Format("Got {0} error(s)", result.Errors.Count()); - - Program.DataConnection.RegisterNotification( - type, - title, - message, - null, - backup.ID, - "backup:show-log", - null, - null, - "backup:show-log", - (n, a) => n - ); - } - - if (!backup.IsTemporary) - Program.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null); - - System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); - Program.StatusEventNotifyer.SignalNewEvent(); - } - - private static bool TestIfOptionApplies() - { - //TODO: Implement to avoid warnings - return true; - } - - private static void DisableModule(string module, Dictionary options) - { - string disabledModules; - string enabledModules; - - if (options.TryGetValue("enable-module", out enabledModules)) - { - var emods = (enabledModules ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - options["enable-module"] = string.Join(",", emods.Where(x => module.Equals(x, StringComparison.OrdinalIgnoreCase))); - } - - options.TryGetValue("disable-module", out disabledModules); - var mods = (disabledModules ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - options["disable-module"] = string.Join(",", mods.Union(new string[] { module }).Distinct(StringComparer.OrdinalIgnoreCase)); - } - - internal static Dictionary ApplyOptions(Duplicati.Server.Serialization.Interface.IBackup backup, Dictionary options) - { - options["backup-name"] = backup.Name; - options["dbpath"] = backup.DBPath; - - // Apply normal options - foreach(var o in backup.Settings) - if (!o.Name.StartsWith("--", StringComparison.Ordinal) && TestIfOptionApplies()) - options[o.Name] = o.Value; - - // Apply override options - foreach(var o in backup.Settings) - if (o.Name.StartsWith("--", StringComparison.Ordinal) && TestIfOptionApplies()) - options[o.Name.Substring(2)] = o.Value; - - - // The server hangs if the module is enabled as there is no console attached - DisableModule("console-password-input", options); - - return options; - } - - private static Library.Utility.IFilter ApplyFilter(Serialization.Interface.IBackup backup, Library.Utility.IFilter filter) - { - var f2 = backup.Filters; - if (f2 != null && f2.Length > 0) - { - var nf = - (from n in f2 - let exp = - n.Expression.StartsWith("[", StringComparison.Ordinal) && n.Expression.EndsWith("]", StringComparison.Ordinal) - ? SpecialFolders.ExpandEnvironmentVariablesRegexp(n.Expression) - : SpecialFolders.ExpandEnvironmentVariables(n.Expression) - orderby n.Order - select (Library.Utility.IFilter)(new Library.Utility.FilterExpression(exp, n.Include))) - .Aggregate((a, b) => Library.Utility.FilterExpression.Combine(a, b)); - - filter = Library.Utility.FilterExpression.Combine(filter, nf); - } - - return filter; - } - - internal static Dictionary GetCommonOptions() - { - return - (from n in Program.DataConnection.Settings - where TestIfOptionApplies() - select n).ToDictionary(k => k.Name.StartsWith("--", StringComparison.Ordinal) ? k.Name.Substring(2) : k.Name, k => k.Value); - } - - private static Duplicati.Library.Utility.IFilter GetCommonFilter() - { - var filters = Program.DataConnection.Filters; - if (filters == null || filters.Length == 0) - return null; - - return - (from n in filters - orderby n.Order - let exp = Environment.ExpandEnvironmentVariables(n.Expression) - select (Duplicati.Library.Utility.IFilter)(new Duplicati.Library.Utility.FilterExpression(exp, n.Include))) - .Aggregate((a, b) => Duplicati.Library.Utility.FilterExpression.Combine(a, b)); - } - } -} - +#region Disclaimer / License +// Copyright (C) 2019, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// +#endregion +using System; +using System.Linq; +using System.Collections.Generic; +using Duplicati.Library.Interface; +using Duplicati.Server.Serialization; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server +{ + public static class Runner + { + public interface IRunnerData : Duplicati.Server.Serialization.Interface.IQueuedTask + { + Duplicati.Server.Serialization.Interface.IBackup Backup { get; } + IDictionary ExtraOptions { get; } + string[] FilterStrings { get; } + void Stop(bool allowCurrentFileToFinish); + void Abort(); + void Pause(); + void Resume(); + void UpdateThrottleSpeed(); + void SetController(Duplicati.Library.Main.Controller controller); + } + + private class RunnerData : IRunnerData + { + private static long RunnerTaskID = 1; + + public Duplicati.Server.Serialization.DuplicatiOperation Operation { get; internal set; } + public Duplicati.Server.Serialization.Interface.IBackup Backup { get; internal set; } + public IDictionary ExtraOptions { get; internal set; } + public string[] FilterStrings { get; internal set; } + + public string BackupID { get { return Backup.ID; } } + public long TaskID { get { return m_taskID; } } + + internal Duplicati.Library.Main.Controller Controller { get; set; } + + public void SetController(Duplicati.Library.Main.Controller controller) + { + Controller = controller; + } + + public void Stop(bool allowCurrentFileToFinish) + { + var c = Controller; + if (c != null) + c.Stop(allowCurrentFileToFinish); + } + + public void Abort() + { + var c = Controller; + if (c != null) + c.Abort(); + } + + public void Pause() + { + var c = Controller; + if (c != null) + c.Pause(); + } + + public void Resume() + { + var c = Controller; + if (c != null) + c.Resume(); + } + + public long OriginalUploadSpeed { get; set; } + public long OriginalDownloadSpeed { get; set; } + + public void UpdateThrottleSpeed() + { + var controller = this.Controller; + if (controller == null) + return; + + var job_upload_throttle = this.OriginalUploadSpeed <= 0 ? long.MaxValue : this.OriginalUploadSpeed; + var job_download_throttle = this.OriginalDownloadSpeed <= 0 ? long.MaxValue : this.OriginalDownloadSpeed; + + var server_upload_throttle = long.MaxValue; + var server_download_throttle = long.MaxValue; + + try + { + if (!string.IsNullOrWhiteSpace(FIXMEGlobal.DataConnection.ApplicationSettings.UploadSpeedLimit)) + server_upload_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(FIXMEGlobal.DataConnection.ApplicationSettings.UploadSpeedLimit, "kb"); + } + catch { } + + try + { + if (!string.IsNullOrWhiteSpace(FIXMEGlobal.DataConnection.ApplicationSettings.DownloadSpeedLimit)) + server_download_throttle = Duplicati.Library.Utility.Sizeparser.ParseSize(FIXMEGlobal.DataConnection.ApplicationSettings.DownloadSpeedLimit, "kb"); + } + catch { } + + var upload_throttle = Math.Min(job_upload_throttle, server_upload_throttle); + var download_throttle = Math.Min(job_download_throttle, server_download_throttle); + + if (upload_throttle <= 0 || upload_throttle == long.MaxValue) + upload_throttle = 0; + + if (download_throttle <= 0 || download_throttle == long.MaxValue) + download_throttle = 0; + + controller.MaxUploadSpeed = upload_throttle; + controller.MaxDownloadSpeed = download_throttle; + } + + private readonly long m_taskID; + + public RunnerData() + { + m_taskID = System.Threading.Interlocked.Increment(ref RunnerTaskID); + } + } + + private class CustomRunnerTask : RunnerData + { + public readonly Action Run; + + public CustomRunnerTask(Action runner) + : base() + { + if (runner == null) + throw new ArgumentNullException(nameof(runner)); + Run = runner; + Operation = DuplicatiOperation.CustomRunner; + Backup = new Database.Backup(); + } + } + + public static IRunnerData CreateCustomTask(Action runner) + { + return new CustomRunnerTask(runner); + } + + public static IRunnerData CreateTask(Duplicati.Server.Serialization.DuplicatiOperation operation, Duplicati.Server.Serialization.Interface.IBackup backup, IDictionary extraOptions = null, string[] filterStrings = null) + { + return new RunnerData() { + Operation = operation, + Backup = backup, + ExtraOptions = extraOptions, + FilterStrings = filterStrings + }; + } + + public static IRunnerData CreateListTask(Duplicati.Server.Serialization.Interface.IBackup backup, string[] filters, bool onlyPrefix, bool allVersions, bool folderContents, DateTime time) + { + var dict = new Dictionary(); + if (onlyPrefix) + dict["list-prefix-only"] = "true"; + if (allVersions) + dict["all-versions"] = "true"; + if (time.Ticks > 0) + dict["time"] = Duplicati.Library.Utility.Utility.SerializeDateTime(time.ToUniversalTime()); + if (folderContents) + dict["list-folder-contents"] = "true"; + + return CreateTask( + DuplicatiOperation.List, + backup, + dict, + filters); + } + + public static IRunnerData CreateRestoreTask(Duplicati.Server.Serialization.Interface.IBackup backup, string[] filters, + DateTime time, string restoreTarget, bool overwrite, bool restore_permissions, + bool skip_metadata, string passphrase) + { + var dict = new Dictionary + { + ["time"] = Library.Utility.Utility.SerializeDateTime(time.ToUniversalTime()), + ["overwrite"] = overwrite? Boolean.TrueString : Boolean.FalseString, + ["restore-permissions"] = restore_permissions ? Boolean.TrueString : Boolean.FalseString, + ["skip-metadata"] = skip_metadata ? Boolean.TrueString : Boolean.FalseString, + ["allow-passphrase-change"] = Boolean.TrueString + }; + if (!string.IsNullOrWhiteSpace(restoreTarget)) + dict["restore-path"] = SpecialFolders.ExpandEnvironmentVariables(restoreTarget); + if (!(passphrase is null)) + dict["passphrase"] = passphrase; + + return CreateTask( + DuplicatiOperation.Restore, + backup, + dict, + filters); + } + private class MessageSink : Duplicati.Library.Main.IMessageSink + { + private class ProgressState : Server.Serialization.Interface.IProgressEventData + { + private readonly string m_backupID; + private readonly long m_taskID; + + internal Duplicati.Library.Main.BackendActionType m_backendAction; + internal string m_backendPath; + internal long m_backendFileSize; + internal long m_backendFileProgress; + internal long m_backendSpeed; + internal bool m_backendIsBlocking; + + internal string m_currentFilename; + internal long m_currentFilesize; + internal long m_currentFileoffset; + internal bool m_currentFilecomplete; + + internal Duplicati.Library.Main.OperationPhase m_phase; + internal float m_overallProgress; + internal long m_processedFileCount; + internal long m_processedFileSize; + internal long m_totalFileCount; + internal long m_totalFileSize; + internal bool m_stillCounting; + + public ProgressState(long taskId, string backupId) + { + m_backupID = backupId; + m_taskID = taskId; + } + + internal ProgressState Clone() + { + return (ProgressState)this.MemberwiseClone(); + } + + #region IProgressEventData implementation + public string BackupID { get { return m_backupID; } } + public long TaskID { get { return m_taskID; } } + public string BackendAction { get { return m_backendAction.ToString(); } } + public string BackendPath { get { return m_backendPath; } } + public long BackendFileSize { get { return m_backendFileSize; } } + public long BackendFileProgress { get { return m_backendFileProgress; } } + public long BackendSpeed { get { return m_backendSpeed; } } + public bool BackendIsBlocking { get { return m_backendIsBlocking; } } + public string CurrentFilename { get { return m_currentFilename; } } + public long CurrentFilesize { get { return m_currentFilesize; } } + public long CurrentFileoffset { get { return m_currentFileoffset; } } + public bool CurrentFilecomplete { get { return m_currentFilecomplete; } } + public string Phase { get { return m_phase.ToString(); } } + public float OverallProgress { get { return m_overallProgress; } } + public long ProcessedFileCount { get { return m_processedFileCount; } } + public long ProcessedFileSize { get { return m_processedFileSize; } } + public long TotalFileCount { get { return m_totalFileCount; } } + public long TotalFileSize { get { return m_totalFileSize; } } + public bool StillCounting { get { return m_stillCounting; } } + #endregion + } + + private readonly ProgressState m_state; + private Duplicati.Library.Main.IBackendProgress m_backendProgress; + private Duplicati.Library.Main.IOperationProgress m_operationProgress; + private readonly object m_lock = new object(); + + public MessageSink(long taskId, string backupId) + { + m_state = new ProgressState(taskId, backupId); + } + + public Server.Serialization.Interface.IProgressEventData Copy() + { + lock(m_lock) + { + if (m_backendProgress != null) + m_backendProgress.Update(out m_state.m_backendAction, out m_state.m_backendPath, out m_state.m_backendFileSize, out m_state.m_backendFileProgress, out m_state.m_backendSpeed, out m_state.m_backendIsBlocking); + if (m_operationProgress != null) + { + m_operationProgress.UpdateFile(out m_state.m_currentFilename, out m_state.m_currentFilesize, out m_state.m_currentFileoffset, out m_state.m_currentFilecomplete); + m_operationProgress.UpdateOverall(out m_state.m_phase, out m_state.m_overallProgress, out m_state.m_processedFileCount, out m_state.m_processedFileSize, out m_state.m_totalFileCount, out m_state.m_totalFileSize, out m_state.m_stillCounting); + } + + return m_state.Clone(); + } + } + + #region IMessageSink implementation + public void BackendEvent(Duplicati.Library.Main.BackendActionType action, Duplicati.Library.Main.BackendEventType type, string path, long size) + { + lock(m_lock) + { + m_state.m_backendAction = action; + m_state.m_backendPath = path; + if (type == Duplicati.Library.Main.BackendEventType.Started) + m_state.m_backendFileSize = size; + else if (type == Duplicati.Library.Main.BackendEventType.Progress) + m_state.m_backendFileProgress = size; + else + { + m_state.m_backendFileSize = 0; + m_state.m_backendFileProgress = 0; + m_state.m_backendSpeed = 0; + } + } + } + + public void SetBackendProgress(Library.Main.IBackendProgress progress) + { + lock (m_lock) + m_backendProgress = progress; + } + + public void SetOperationProgress(Library.Main.IOperationProgress progress) + { + lock (m_lock) + m_operationProgress = progress; + } + + public void WriteMessage(Library.Logging.LogEntry entry) + { + // Do nothing. Implementation needed for ILogDestination interface. + } + #endregion + } + + public static string GetCommandLine(IRunnerData data) + { + var backup = data.Backup; + + var options = ApplyOptions(backup, GetCommonOptions()); + if (data.ExtraOptions != null) + foreach(var k in data.ExtraOptions) + options[k.Key] = k.Value; + + var cf = FIXMEGlobal.DataConnection.Filters; + var bf = backup.Filters; + + var sources = + (from n in backup.Sources + let p = SpecialFolders.ExpandEnvironmentVariables(n) + where !string.IsNullOrWhiteSpace(p) + select p).ToArray(); + + var exe = + System.IO.Path.Combine( + Library.AutoUpdater.UpdaterManager.InstalledBaseDir, + System.IO.Path.GetFileName( + typeof(Duplicati.CommandLine.Commands).Assembly.Location + ) + ); + + var cmd = new System.Text.StringBuilder(); + if (Library.Utility.Utility.IsMono) + cmd.Append("mono "); + + cmd.Append(Library.Utility.Utility.WrapAsCommandLine(new string[] { exe, "backup", backup.TargetURL }, false)); + + cmd.Append(" "); + cmd.Append(Library.Utility.Utility.WrapAsCommandLine(sources, true)); + + // TODO: We should check each option to see if it is a path, and allow expansion on that + foreach(var opt in options) + cmd.AppendFormat(" --{0}={1}", opt.Key, Library.Utility.Utility.WrapCommandLineElement(opt.Value, false)); + + if (cf != null) + foreach(var f in cf) + cmd.AppendFormat(" --{0}={1}", f.Include ? "include" : "exclude", Library.Utility.Utility.WrapCommandLineElement(f.Expression, true)); + + if (bf != null) + foreach(var f in bf) + cmd.AppendFormat(" --{0}={1}", f.Include ? "include" : "exclude", Library.Utility.Utility.WrapCommandLineElement(f.Expression, true)); + + return cmd.ToString(); + } + + public static string[] GetCommandLineParts(IRunnerData data) + { + var backup = data.Backup; + + var options = ApplyOptions(backup, GetCommonOptions()); + if (data.ExtraOptions != null) + foreach (var k in data.ExtraOptions) + options[k.Key] = k.Value; + + var cf = FIXMEGlobal.DataConnection.Filters; + var bf = backup.Filters; + + var sources = + (from n in backup.Sources + let p = SpecialFolders.ExpandEnvironmentVariables(n) + where !string.IsNullOrWhiteSpace(p) + select p).ToArray(); + + var parts = new List + { + backup.TargetURL + }; + parts.AddRange(sources); + + foreach (var opt in options) + parts.Add(string.Format("--{0}={1}", opt.Key, opt.Value)); + + if (cf != null) + foreach (var f in cf) + parts.Add(string.Format("--{0}={1}", f.Include ? "include" : "exclude", f.Expression)); + + if (bf != null) + foreach (var f in bf) + parts.Add(string.Format("--{0}={1}", f.Include ? "include" : "exclude", f.Expression)); + + return parts.ToArray(); + } + + public static Duplicati.Library.Interface.IBasicResults Run(IRunnerData data, bool fromQueue) + { + if (data is CustomRunnerTask task) + { + try + { + var sink = new MessageSink(task.TaskID, null); + FIXMEGlobal.GenerateProgressState = sink.Copy; + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + task.Run(sink); + } + catch(Exception ex) + { + FIXMEGlobal.DataConnection.LogError(string.Empty, "Failed while executing custom task", ex); + } + + return null; + } + + var backup = data.Backup; + if (backup.Metadata == null) + { + backup.Metadata = new Dictionary(); + } + + Duplicati.Library.Utility.TempFolder tempfolder = null; + + try + { + var sink = new MessageSink(data.TaskID, backup.ID); + if (fromQueue) + { + FIXMEGlobal.GenerateProgressState = () => sink.Copy(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + var options = ApplyOptions(backup, GetCommonOptions()); + if (data.ExtraOptions != null) + foreach(var k in data.ExtraOptions) + options[k.Key] = k.Value; + + // Pack in the system or task config for easy restore + if (data.Operation == DuplicatiOperation.Backup && options.ContainsKey("store-task-config")) + { + tempfolder = StoreTaskConfigAndGetTempFolder(data, options); + } + + // Attach a log scope that tags all messages to relay the TaskID and BackupID + using (Library.Logging.Log.StartScope(log => { + log[LogWriteHandler.LOG_EXTRA_TASKID] = data.TaskID.ToString(); + log[LogWriteHandler.LOG_EXTRA_BACKUPID] = data.BackupID; + })) + + using(tempfolder) + using(var controller = new Duplicati.Library.Main.Controller(backup.TargetURL, options, sink)) + { + try + { + if (options.ContainsKey("throttle-upload")) + ((RunnerData)data).OriginalUploadSpeed = Duplicati.Library.Utility.Sizeparser.ParseSize(options["throttle-upload"], "kb"); + } + catch { } + + try + { + if (options.ContainsKey("throttle-download")) + ((RunnerData)data).OriginalDownloadSpeed = Duplicati.Library.Utility.Sizeparser.ParseSize(options["throttle-download"], "kb"); + } + catch { } + + ((RunnerData)data).Controller = controller; + data.UpdateThrottleSpeed(); + + if (backup.Metadata.ContainsKey("LastCompactFinished")) + controller.LastCompact = Library.Utility.Utility.DeserializeDateTime(backup.Metadata["LastCompactFinished"]); + + if (backup.Metadata.ContainsKey("LastVacuumFinished")) + controller.LastVacuum = Library.Utility.Utility.DeserializeDateTime(backup.Metadata["LastVacuumFinished"]); + + switch (data.Operation) + { + case DuplicatiOperation.Backup: + { + var filter = ApplyFilter(backup, GetCommonFilter()); + var sources = + (from n in backup.Sources + let p = SpecialFolders.ExpandEnvironmentVariables(n) + where !string.IsNullOrWhiteSpace(p) + select p).ToArray(); + + var r = controller.Backup(sources, filter); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.List: + { + var r = controller.List(data.FilterStrings, null); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.Repair: + { + var r = controller.Repair(data.FilterStrings == null ? null : new Library.Utility.FilterExpression(data.FilterStrings)); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.RepairUpdate: + { + var r = controller.UpdateDatabaseWithVersions(); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.Remove: + { + var r = controller.Delete(); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.Restore: + { + var r = controller.Restore(data.FilterStrings); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.Verify: + { + var r = controller.Test(); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.Compact: + { + var r = controller.Compact(); + UpdateMetadata(backup, r); + return r; + } + case DuplicatiOperation.CreateReport: + { + using(var tf = new Duplicati.Library.Utility.TempFile()) + { + var r = controller.CreateLogDatabase(tf); + var tempid = FIXMEGlobal.DataConnection.RegisterTempFile("create-bug-report", r.TargetPath, DateTime.Now.AddDays(3)); + + if (string.Equals(tf, r.TargetPath, Library.Utility.Utility.ClientFilenameStringComparison)) + tf.Protected = true; + + FIXMEGlobal.DataConnection.RegisterNotification( + NotificationType.Information, + "Bugreport ready", + "Bugreport is ready for download", + null, + null, + "bug-report:created:" + tempid, + null, + "BugreportCreatedReady", + "", + (n, a) => n + ); + + return r; + } + } + + case DuplicatiOperation.ListRemote: + { + var r = controller.ListRemote(); + UpdateMetadata(backup, r); + return r; + } + + case DuplicatiOperation.Delete: + { + if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions, "delete-remote-files")) + controller.DeleteAllRemoteFiles(); + + if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions, "delete-local-db")) + { + string dbpath; + options.TryGetValue("dbpath", out dbpath); + + if (!string.IsNullOrWhiteSpace(dbpath) && System.IO.File.Exists(dbpath)) + System.IO.File.Delete(dbpath); + } + FIXMEGlobal.DataConnection.DeleteBackup(backup); + FIXMEGlobal.Scheduler.Reschedule(); + return null; + } + case DuplicatiOperation.Vacuum: + { + var r = controller.Vacuum(); + UpdateMetadata(backup, r); + return r; + } + default: + //TODO: Log this + return null; + } + } + } + catch (Exception ex) + { + FIXMEGlobal.DataConnection.LogError(data.Backup.ID, string.Format("Failed while executing {0} \"{1}\" (id: {2})", data.Operation, data.Backup.Name, data.Backup.ID), ex); + UpdateMetadataError(data.Backup, ex); + Library.UsageReporter.Reporter.Report(ex); + + if (!fromQueue) + throw; + + return null; + } + finally + { + ((RunnerData)data).Controller = null; + } + } + + private static Duplicati.Library.Utility.TempFolder StoreTaskConfigAndGetTempFolder(IRunnerData data, Dictionary options) + { + var all_tasks = string.Equals(options["store-task-config"], "all", StringComparison.OrdinalIgnoreCase) || string.Equals(options["store-task-config"], "*", StringComparison.OrdinalIgnoreCase); + var this_task = Duplicati.Library.Utility.Utility.ParseBool(options["store-task-config"], false); + + options.Remove("store-task-config"); + + Duplicati.Library.Utility.TempFolder tempfolder = null; + if (all_tasks || this_task) + { + tempfolder = new Duplicati.Library.Utility.TempFolder(); + var temppath = System.IO.Path.Combine(tempfolder, "task-setup.json"); + using (var tempfile = Duplicati.Library.Utility.TempFile.WrapExistingFile(temppath)) + { + object taskdata = null; + if (all_tasks) + taskdata = FIXMEGlobal.DataConnection.Backups.Where(x => !x.IsTemporary).Select(x => FIXMEGlobal.DataConnection.PrepareBackupForExport(FIXMEGlobal.DataConnection.GetBackup(x.ID))); + else + taskdata = new[] { FIXMEGlobal.DataConnection.PrepareBackupForExport(data.Backup) }; + + using (var fs = System.IO.File.OpenWrite(tempfile)) + using (var sw = new System.IO.StreamWriter(fs, System.Text.Encoding.UTF8)) + Serializer.SerializeJson(sw, taskdata, true); + + tempfile.Protected = true; + + options.TryGetValue("control-files", out string controlfiles); + + if (string.IsNullOrWhiteSpace(controlfiles)) + controlfiles = tempfile; + else + controlfiles += System.IO.Path.PathSeparator + tempfile; + + options["control-files"] = controlfiles; + } + } + return tempfolder; + } + + private static void UpdateMetadataError(Duplicati.Server.Serialization.Interface.IBackup backup, Exception ex) + { + backup.Metadata["LastErrorDate"] = Library.Utility.Utility.SerializeDateTime(DateTime.UtcNow); + backup.Metadata["LastErrorMessage"] = ex.Message; + + if (!backup.IsTemporary) + FIXMEGlobal.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null); + + string messageid = null; + if (ex is UserInformationException exception) + messageid = exception.HelpID; + + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.DataConnection.RegisterNotification( + NotificationType.Error, + backup.IsTemporary ? + "Error" : string.Format("Error while running {0}", backup.Name), + ex.Message, + ex, + backup.ID, + "backup:show-log", + null, + messageid, + null, + (n, a) => { + return a.FirstOrDefault(x => x.BackupID == backup.ID) ?? n; + } + ); + } + + private static void UpdateMetadataLastCompact(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.ICompactResults r) + { + if (r != null) + { + backup.Metadata["LastCompactDuration"] = r.Duration.ToString(); + backup.Metadata["LastCompactStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); + backup.Metadata["LastCompactFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); + } + } + + private static void UpdateMetadataLastVacuum(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IVacuumResults r) + { + if (r != null) + { + backup.Metadata["LastVacuumDuration"] = r.Duration.ToString(); + backup.Metadata["LastVacuumStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); + backup.Metadata["LastVacuumFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); + } + } + + private static void UpdateMetadata(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IParsedBackendStatistics r) + { + if (r != null) + { + backup.Metadata["LastBackupDate"] = Library.Utility.Utility.SerializeDateTime(r.LastBackupDate.ToUniversalTime()); + backup.Metadata["BackupListCount"] = r.BackupListCount.ToString(); + backup.Metadata["TotalQuotaSpace"] = r.TotalQuotaSpace.ToString(); + backup.Metadata["FreeQuotaSpace"] = r.FreeQuotaSpace.ToString(); + backup.Metadata["AssignedQuotaSpace"] = r.AssignedQuotaSpace.ToString(); + + backup.Metadata["TargetFilesSize"] = r.KnownFileSize.ToString(); + backup.Metadata["TargetFilesCount"] = r.KnownFileCount.ToString(); + backup.Metadata["TargetSizeString"] = Duplicati.Library.Utility.Utility.FormatSizeString(r.KnownFileSize); + } + } + + private static void UpdateMetadata(Duplicati.Server.Serialization.Interface.IBackup backup, Duplicati.Library.Interface.IBasicResults result) + { + if (result is IRestoreResults r1) + { + backup.Metadata["LastRestoreDuration"] = r1.Duration.ToString(); + backup.Metadata["LastRestoreStarted"] = Library.Utility.Utility.SerializeDateTime(result.BeginTime.ToUniversalTime()); + backup.Metadata["LastRestoreFinished"] = Library.Utility.Utility.SerializeDateTime(result.EndTime.ToUniversalTime()); + } + + if (result is IParsedBackendStatistics r2 && !result.Interrupted) + { + UpdateMetadata(backup, r2); + } + + if (result is IBackendStatsticsReporter r3 && !result.Interrupted) + { + if (r3.BackendStatistics is IParsedBackendStatistics statistics) + UpdateMetadata(backup, statistics); + } + + if (result is ICompactResults r4 && !result.Interrupted) + { + UpdateMetadataLastCompact(backup, r4); + + if (r4.VacuumResults != null) + UpdateMetadataLastVacuum(backup, r4.VacuumResults); + } + + if (result is IVacuumResults r5 && !result.Interrupted) + { + UpdateMetadataLastVacuum(backup, r5); + } + + if (result is IBackupResults r) + { + if (!result.Interrupted) + { + backup.Metadata["SourceFilesSize"] = r.SizeOfExaminedFiles.ToString(); + backup.Metadata["SourceFilesCount"] = r.ExaminedFiles.ToString(); + backup.Metadata["SourceSizeString"] = Duplicati.Library.Utility.Utility.FormatSizeString(r.SizeOfExaminedFiles); + backup.Metadata["LastBackupStarted"] = Library.Utility.Utility.SerializeDateTime(r.BeginTime.ToUniversalTime()); + backup.Metadata["LastBackupFinished"] = Library.Utility.Utility.SerializeDateTime(r.EndTime.ToUniversalTime()); + backup.Metadata["LastBackupDuration"] = r.Duration.ToString(); + + if (r.CompactResults != null) + UpdateMetadataLastCompact(backup, r.CompactResults); + + if (r.VacuumResults != null) + UpdateMetadataLastVacuum(backup, r.VacuumResults); + } + + if (r.FilesWithError > 0 || r.Warnings.Any() || r.Errors.Any()) + { + string message; + string titleType; + if (r.FilesWithError > 0) + { + message = $"Errors affected {r.FilesWithError} file(s)."; + titleType = "Error"; + } + else if (r.Errors.Any()) + { + message = r.Errors.Count() == 1 ? r.Errors.Single() : $"Encountered {r.Errors.Count()} errors."; + titleType = "Error"; + } + else + { + message = r.Warnings.Count() == 1 ? r.Warnings.Single() : $"Encountered {r.Warnings.Count()} warnings."; + titleType = "Warning"; + } + + FIXMEGlobal.DataConnection.RegisterNotification( + r.FilesWithError == 0 && !r.Errors.Any() ? NotificationType.Warning : NotificationType.Error, + backup.IsTemporary ? "Warning" : $"{titleType} while running {backup.Name}", + message, + null, + backup.ID, + "backup:show-log", + null, + null, + null, + (n, a) => + { + var existing = a.FirstOrDefault(x => x.BackupID == backup.ID); + if (existing == null) + return n; + + if (existing.Type == NotificationType.Error) + return existing; + + return n; + } + ); + } + } + else if (result.ParsedResult != ParsedResultType.Success) + { + var type = result.ParsedResult == ParsedResultType.Warning + ? NotificationType.Warning + : NotificationType.Error; + + var title = result.ParsedResult == ParsedResultType.Warning + ? (backup.IsTemporary ? + "Warning" : string.Format("Warning while running {0}", backup.Name)) + : (backup.IsTemporary ? + "Error" : string.Format("Error while running {0}", backup.Name)); + + var message = result.ParsedResult == ParsedResultType.Warning + ? string.Format("Got {0} warning(s)", result.Warnings.Count()) + : string.Format("Got {0} error(s)", result.Errors.Count()); + + FIXMEGlobal.DataConnection.RegisterNotification( + type, + title, + message, + null, + backup.ID, + "backup:show-log", + null, + null, + "backup:show-log", + (n, a) => n + ); + } + + if (!backup.IsTemporary) + FIXMEGlobal.DataConnection.SetMetadata(backup.Metadata, long.Parse(backup.ID), null); + + FIXMEGlobal.IncrementLastDataUpdateID(); + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + private static bool TestIfOptionApplies() + { + //TODO: Implement to avoid warnings + return true; + } + + private static void DisableModule(string module, Dictionary options) + { + string disabledModules; + string enabledModules; + + if (options.TryGetValue("enable-module", out enabledModules)) + { + var emods = (enabledModules ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + options["enable-module"] = string.Join(",", emods.Where(x => module.Equals(x, StringComparison.OrdinalIgnoreCase))); + } + + options.TryGetValue("disable-module", out disabledModules); + var mods = (disabledModules ?? "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + options["disable-module"] = string.Join(",", mods.Union(new string[] { module }).Distinct(StringComparer.OrdinalIgnoreCase)); + } + + internal static Dictionary ApplyOptions(Duplicati.Server.Serialization.Interface.IBackup backup, Dictionary options) + { + options["backup-name"] = backup.Name; + options["dbpath"] = backup.DBPath; + + // Apply normal options + foreach(var o in backup.Settings) + if (!o.Name.StartsWith("--", StringComparison.Ordinal) && TestIfOptionApplies()) + options[o.Name] = o.Value; + + // Apply override options + foreach(var o in backup.Settings) + if (o.Name.StartsWith("--", StringComparison.Ordinal) && TestIfOptionApplies()) + options[o.Name.Substring(2)] = o.Value; + + + // The server hangs if the module is enabled as there is no console attached + DisableModule("console-password-input", options); + + return options; + } + + private static Library.Utility.IFilter ApplyFilter(Serialization.Interface.IBackup backup, Library.Utility.IFilter filter) + { + var f2 = backup.Filters; + if (f2 != null && f2.Length > 0) + { + var nf = + (from n in f2 + let exp = + n.Expression.StartsWith("[", StringComparison.Ordinal) && n.Expression.EndsWith("]", StringComparison.Ordinal) + ? SpecialFolders.ExpandEnvironmentVariablesRegexp(n.Expression) + : SpecialFolders.ExpandEnvironmentVariables(n.Expression) + orderby n.Order + select (Library.Utility.IFilter)(new Library.Utility.FilterExpression(exp, n.Include))) + .Aggregate((a, b) => Library.Utility.FilterExpression.Combine(a, b)); + + filter = Library.Utility.FilterExpression.Combine(filter, nf); + } + + return filter; + } + + internal static Dictionary GetCommonOptions() + { + return + (from n in FIXMEGlobal.DataConnection.Settings + where TestIfOptionApplies() + select n).ToDictionary(k => k.Name.StartsWith("--", StringComparison.Ordinal) ? k.Name.Substring(2) : k.Name, k => k.Value); + } + + private static Duplicati.Library.Utility.IFilter GetCommonFilter() + { + var filters = FIXMEGlobal.DataConnection.Filters; + if (filters == null || filters.Length == 0) + return null; + + return + (from n in filters + orderby n.Order + let exp = Environment.ExpandEnvironmentVariables(n.Expression) + select (Duplicati.Library.Utility.IFilter)(new Duplicati.Library.Utility.FilterExpression(exp, n.Include))) + .Aggregate((a, b) => Duplicati.Library.Utility.FilterExpression.Combine(a, b)); + } + } +} + diff --git a/Duplicati/Server/Scheduler.cs b/Duplicati.Library.RestAPI/Scheduler.cs similarity index 88% rename from Duplicati/Server/Scheduler.cs rename to Duplicati.Library.RestAPI/Scheduler.cs index 2b4d46a9e..d680a9a61 100644 --- a/Duplicati/Server/Scheduler.cs +++ b/Duplicati.Library.RestAPI/Scheduler.cs @@ -1,409 +1,410 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com +#region Disclaimer / License +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com // -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. // -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Text; -using System.Linq; -using System.Threading; -using Duplicati.Library.Utility; using Duplicati.Server.Serialization.Interface; -namespace Duplicati.Server -{ - /// - /// This class handles scheduled runs of backups - /// - public class Scheduler - { - private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(); - - /// - /// The thread that runs the scheduler - /// - private readonly Thread m_thread; - /// - /// A termination flag - /// - private volatile bool m_terminate; - /// - /// The worker thread that is invoked to do work - /// - private readonly WorkerThread m_worker; - /// - /// The wait event - /// - private readonly AutoResetEvent m_event; - /// - /// The data synchronization lock - /// - private readonly object m_lock = new object(); - - /// - /// An event that is raised when the schedule changes - /// - public event EventHandler NewSchedule; - - /// - /// The currently scheduled items - /// - private KeyValuePair[] m_schedule; - - /// - /// List of update tasks, used to set the timestamp on the schedule once completed - /// - private readonly Dictionary> m_updateTasks; - - /// - /// Constructs a new scheduler - /// - /// The worker thread - public Scheduler(WorkerThread worker) - { - m_thread = new Thread(new ThreadStart(Runner)); - m_worker = worker; - m_worker.CompletedWork += OnCompleted; - m_worker.StartingWork += OnStartingWork; - m_schedule = new KeyValuePair[0]; - m_terminate = false; - m_event = new AutoResetEvent(false); - m_updateTasks = new Dictionary>(); - m_thread.IsBackground = true; - m_thread.Name = "TaskScheduler"; - m_thread.Start(); - } - - /// - /// Forces the scheduler to re-evaluate the order. - /// Call this method if something changes - /// - public void Reschedule() - { - m_event.Set(); - } - - /// - /// A snapshot copy of the current schedule list - /// - public List> Schedule - { - get - { - lock (m_lock) - return m_schedule.ToList(); - } - } - - /// - /// A snapshot copy of the current worker queue, that is items that are scheduled, but waiting for execution - /// - public List WorkerQueue - { - get - { - return (from t in m_worker.CurrentTasks where t != null select t).ToList(); - } - } - - /// - /// Terminates the thread. Any items still in queue will be removed - /// - /// True if the call should block until the thread has exited, false otherwise - public void Terminate(bool wait) - { - m_terminate = true; - m_event.Set(); - - if (wait) - m_thread.Join(); - } - - /// - /// Returns the next valid date, given the start and the interval - /// - /// The base time - /// The first allowed date - /// The repetition interval - /// The days the backup is allowed to run - /// The next valid date, or throws an exception if no such date can be found - public static DateTime GetNextValidTime(DateTime basetime, DateTime firstdate, string repetition, DayOfWeek[] allowedDays) - { - var res = basetime; - - var i = 50000; - while (res < firstdate && i-- > 0) - res = Timeparser.ParseTimeInterval(repetition, res); - - // If we arrived somewhere after the first allowed date - if (res >= firstdate) - { - var ts = Timeparser.ParseTimeSpan(repetition); - - if (ts.TotalDays >= 1) - { - // We jump in days, so we pick the first valid day after firstdate - - for (var n = 0; n < 8; n++) - if (IsDateAllowed(res, allowedDays)) - break; - else - res = res.AddDays(1); - } - else - { - // We jump less than a day, so we keep adding the repetition until - // we hit a valid day - - i = 50000; - while (!IsDateAllowed(res, allowedDays) && i-- > 0) - res = Timeparser.ParseTimeInterval(repetition, res); - } - } - - if (!IsDateAllowed(res, allowedDays) || res < firstdate) - { - StringBuilder sb = new StringBuilder(); - if (allowedDays != null) - foreach (DayOfWeek w in allowedDays) - { - if (sb.Length != 0) - sb.Append(", "); - sb.Append(w.ToString()); - } - - throw new Exception(Strings.Scheduler.InvalidTimeSetupError(basetime, repetition, sb.ToString())); - } - - return res; - } - - private void OnCompleted(WorkerThread worker, Runner.IRunnerData task) - { - Tuple t = null; - lock(m_lock) - { - if (task != null && m_updateTasks.TryGetValue(task, out t)) - m_updateTasks.Remove(task); - } - - if (t != null) - { - t.Item1.Time = t.Item2; - t.Item1.LastRun = t.Item3; - Program.DataConnection.AddOrUpdateSchedule(t.Item1); - } - - } - - private void OnStartingWork(WorkerThread worker, Runner.IRunnerData task) - { - if (task is null) - { - return; - } - - lock(m_lock) - { - if (m_updateTasks.TryGetValue(task, out Tuple scheduleInfo)) - { - // Item2 is the scheduled start time (Time in the Schedule table). - // Item3 is the actual start time (LastRun in the Schedule table). - m_updateTasks[task] = Tuple.Create(scheduleInfo.Item1, scheduleInfo.Item2, DateTime.UtcNow); - } - } - } - - /// - /// The actual scheduling procedure - /// - private void Runner() - { - var scheduled = new Dictionary>(); - while (!m_terminate) - { - //TODO: As this is executed repeatedly we should cache it - // to avoid frequent db lookups - - //Determine schedule list - var lst = Program.DataConnection.Schedules; - foreach(var sc in lst) - { - if (!string.IsNullOrEmpty(sc.Repeat)) - { - KeyValuePair startkey; - - DateTime last = new DateTime(0, DateTimeKind.Utc); - DateTime start; - var scticks = sc.Time.Ticks; - - if (!scheduled.TryGetValue(sc.ID, out startkey) || startkey.Key != scticks) - { - start = new DateTime(scticks, DateTimeKind.Utc); - last = sc.LastRun; - } - else - { - start = startkey.Value; - } - - try - { - // Recover from timedrift issues by overriding the dates if the last run date is in the future. - if (last > DateTime.UtcNow) - { - start = DateTime.UtcNow; - last = DateTime.UtcNow; - } - start = GetNextValidTime(start, last, sc.Repeat, sc.AllowedDays); - } - catch (Exception ex) - { - Program.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex); - } - - //If time is exceeded, run it now - if (start <= DateTime.UtcNow) - { - var jobsToRun = new List(); - //TODO: Cache this to avoid frequent lookups - foreach(var id in Program.DataConnection.GetBackupIDsForTags(sc.Tags).Distinct().Select(x => x.ToString())) - { - //See if it is already queued - var tmplst = from n in m_worker.CurrentTasks - where n.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup - select n.Backup; - var tastTemp = m_worker.CurrentTask; - if (tastTemp != null && tastTemp.Operation == Duplicati.Server.Serialization.DuplicatiOperation.Backup) - tmplst = tmplst.Union(new [] { tastTemp.Backup }); - - //If it is not already in queue, put it there - if (!tmplst.Any(x => x.ID == id)) - { - var entry = Program.DataConnection.GetBackup(id); - if (entry != null) - { - Dictionary options = Duplicati.Server.Runner.GetCommonOptions(); - Duplicati.Server.Runner.ApplyOptions(entry, options); - if ((new Duplicati.Library.Main.Options(options)).DisableOnBattery && (Duplicati.Library.Utility.Power.PowerSupply.GetSource() == Duplicati.Library.Utility.Power.PowerSupply.Source.Battery)) - { - Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "BackupDisabledOnBattery", "Scheduled backup disabled while on battery power."); - } - else - { - jobsToRun.Add(Server.Runner.CreateTask(Duplicati.Server.Serialization.DuplicatiOperation.Backup, entry)); - } - } - } - } - - // Calculate next time, by finding the first entry later than now - try - { - start = GetNextValidTime(start, new DateTime(Math.Max(DateTime.UtcNow.AddSeconds(1).Ticks, start.AddSeconds(1).Ticks), DateTimeKind.Utc), sc.Repeat, sc.AllowedDays); - } - catch(Exception ex) - { - Program.DataConnection.LogError(sc.ID.ToString(), "Scheduler failed to find next date", ex); - continue; - } - - Server.Runner.IRunnerData lastJob = jobsToRun.LastOrDefault(); - if (lastJob != null) - { - lock (m_lock) - { - // The actual last run time will be updated when the StartingWork event is raised. - m_updateTasks[lastJob] = new Tuple(sc, start, DateTime.UtcNow); - } - } - - foreach (var job in jobsToRun) - m_worker.AddTask(job); - - if (start < DateTime.UtcNow) - { - //TODO: Report this somehow - continue; - } - } - - scheduled[sc.ID] = new KeyValuePair(scticks, start); - } - } - - var existing = lst.ToDictionary(x => x.ID); - //Sort them, lock as we assign the m_schedule variable - lock(m_lock) - m_schedule = (from n in scheduled - where existing.ContainsKey(n.Key) - orderby n.Value.Value - select new KeyValuePair(n.Value.Value, existing[n.Key])).ToArray(); - - // Remove unused entries - foreach(var c in (from n in scheduled where !existing.ContainsKey(n.Key) select n.Key).ToArray()) - scheduled.Remove(c); - - //Raise event if needed - if (NewSchedule != null) - NewSchedule(this, null); - - int waittime = 0; - - //Figure out a sensible amount of time to sleep the thread - if (scheduled.Count > 0) - { - //When is the next run scheduled? - TimeSpan nextrun = scheduled.Values.Min((x) => x.Value) - DateTime.UtcNow; - if (nextrun.TotalMilliseconds < 0) - continue; - - //Don't sleep for more than 5 minutes - waittime = (int)Math.Min(nextrun.TotalMilliseconds, 60 * 1000 * 5); - } - else - { - //No tasks, check back later - waittime = 60 * 1000; - } - - //Waiting on the event, enables a wakeup call from termination - // never use waittime = 0 - m_event.WaitOne(Math.Max(100, waittime), false); - } - } - - /// - /// Returns true if the time is at an allowed weekday, false otherwise - /// - /// The time to evaluate - /// The allowed days - /// True if the backup is allowed to run, false otherwise - private static bool IsDateAllowed(DateTime time, DayOfWeek[] allowedDays) - { - var localTime = time.ToLocalTime(); - if (allowedDays == null || allowedDays.Length == 0) - return true; - else - return Array.IndexOf(allowedDays, localTime.DayOfWeek) >= 0; - } - - } -} + +#endregion +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using System.Threading; +using Duplicati.Library.Utility; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server +{ + /// + /// This class handles scheduled runs of backups + /// + public class Scheduler + { + private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(); + + /// + /// The thread that runs the scheduler + /// + private readonly Thread m_thread; + /// + /// A termination flag + /// + private volatile bool m_terminate; + /// + /// The worker thread that is invoked to do work + /// + private readonly WorkerThread m_worker; + /// + /// The wait event + /// + private readonly AutoResetEvent m_event; + /// + /// The data synchronization lock + /// + private readonly object m_lock = new object(); + + /// + /// An event that is raised when the schedule changes + /// + public event EventHandler NewSchedule; + + /// + /// The currently scheduled items + /// + private KeyValuePair[] m_schedule; + + /// + /// List of update tasks, used to set the timestamp on the schedule once completed + /// + private readonly Dictionary> m_updateTasks; + + /// + /// Constructs a new scheduler + /// + /// The worker thread + public Scheduler(WorkerThread worker) + { + m_thread = new Thread(new ThreadStart(Runner)); + m_worker = worker; + m_worker.CompletedWork += OnCompleted; + m_worker.StartingWork += OnStartingWork; + m_schedule = new KeyValuePair[0]; + m_terminate = false; + m_event = new AutoResetEvent(false); + m_updateTasks = new Dictionary>(); + m_thread.IsBackground = true; + m_thread.Name = "TaskScheduler"; + m_thread.Start(); + } + + /// + /// Forces the scheduler to re-evaluate the order. + /// Call this method if something changes + /// + public void Reschedule() + { + m_event.Set(); + } + + /// + /// A snapshot copy of the current schedule list + /// + public List> Schedule + { + get + { + lock (m_lock) + return m_schedule.ToList(); + } + } + + /// + /// A snapshot copy of the current worker queue, that is items that are scheduled, but waiting for execution + /// + public List WorkerQueue + { + get + { + return (from t in m_worker.CurrentTasks where t != null select t).ToList(); + } + } + + /// + /// Terminates the thread. Any items still in queue will be removed + /// + /// True if the call should block until the thread has exited, false otherwise + public void Terminate(bool wait) + { + m_terminate = true; + m_event.Set(); + + if (wait) + m_thread.Join(); + } + + /// + /// Returns the next valid date, given the start and the interval + /// + /// The base time + /// The first allowed date + /// The repetition interval + /// The days the backup is allowed to run + /// The next valid date, or throws an exception if no such date can be found + public static DateTime GetNextValidTime(DateTime basetime, DateTime firstdate, string repetition, DayOfWeek[] allowedDays) + { + var res = basetime; + + var i = 50000; + while (res < firstdate && i-- > 0) + res = Timeparser.ParseTimeInterval(repetition, res); + + // If we arrived somewhere after the first allowed date + if (res >= firstdate) + { + var ts = Timeparser.ParseTimeSpan(repetition); + + if (ts.TotalDays >= 1) + { + // We jump in days, so we pick the first valid day after firstdate + + for (var n = 0; n < 8; n++) + if (IsDateAllowed(res, allowedDays)) + break; + else + res = res.AddDays(1); + } + else + { + // We jump less than a day, so we keep adding the repetition until + // we hit a valid day + + i = 50000; + while (!IsDateAllowed(res, allowedDays) && i-- > 0) + res = Timeparser.ParseTimeInterval(repetition, res); + } + } + + if (!IsDateAllowed(res, allowedDays) || res < firstdate) + { + StringBuilder sb = new StringBuilder(); + if (allowedDays != null) + foreach (DayOfWeek w in allowedDays) + { + if (sb.Length != 0) + sb.Append(", "); + sb.Append(w.ToString()); + } + + throw new Exception(Strings.Scheduler.InvalidTimeSetupError(basetime, repetition, sb.ToString())); + } + + return res; + } + + private void OnCompleted(WorkerThread worker, Runner.IRunnerData task) + { + Tuple t = null; + lock(m_lock) + { + if (task != null && m_updateTasks.TryGetValue(task, out t)) + m_updateTasks.Remove(task); + } + + if (t != null) + { + t.Item1.Time = t.Item2; + t.Item1.LastRun = t.Item3; + FIXMEGlobal.DataConnection.AddOrUpdateSchedule(t.Item1); + } + + } + + private void OnStartingWork(WorkerThread worker, Runner.IRunnerData task) + { + if (task is null) + { + return; + } + + lock(m_lock) + { + if (m_updateTasks.TryGetValue(task, out Tuple scheduleInfo)) + { + // Item2 is the scheduled start time (Time in the Schedule table). + // Item3 is the actual start time (LastRun in the Schedule table). + m_updateTasks[task] = Tuple.Create(scheduleInfo.Item1, scheduleInfo.Item2, DateTime.UtcNow); + } + } + } + + /// + /// The actual scheduling procedure + /// + private void Runner() + { + var scheduled = new Dictionary>(); + while (!m_terminate) + { + //TODO: As this is executed repeatedly we should cache it + // to avoid frequent db lookups + + //Determine schedule list + var lst = FIXMEGlobal.DataConnection.Schedules; + foreach(var sc in lst) + { + if (!string.IsNullOrEmpty(sc.Repeat)) + { + KeyValuePair startkey; + + DateTime last = new DateTime(0, DateTimeKind.Utc); + DateTime start; + var scticks = sc.Time.Ticks; + + if (!scheduled.TryGetValue(sc.ID, out startkey) || startkey.Key != scticks) + { + start = new DateTime(scticks, DateTimeKind.Utc); + last = sc.LastRun; + } + else + { + start = startkey.Value; + } + + try + { + // Recover from timedrift issues by overriding the dates if the last run date is in the future. + if (last > DateTime.UtcNow) + { + start = DateTime.UtcNow; + last = DateTime.UtcNow; + } + start = GetNextValidTime(start, last, sc.Repeat, sc.AllowedDays); + } + catch (Exception ex) + { + 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(); + //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 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(sc, start, DateTime.UtcNow); + } + } + + foreach (var job in jobsToRun) + m_worker.AddTask(job); + + if (start < DateTime.UtcNow) + { + //TODO: Report this somehow + continue; + } + } + + scheduled[sc.ID] = new KeyValuePair(scticks, start); + } + } + + var existing = lst.ToDictionary(x => x.ID); + //Sort them, lock as we assign the m_schedule variable + lock(m_lock) + m_schedule = (from n in scheduled + where existing.ContainsKey(n.Key) + orderby n.Value.Value + select new KeyValuePair(n.Value.Value, existing[n.Key])).ToArray(); + + // Remove unused entries + foreach(var c in (from n in scheduled where !existing.ContainsKey(n.Key) select n.Key).ToArray()) + scheduled.Remove(c); + + //Raise event if needed + if (NewSchedule != null) + NewSchedule(this, null); + + int waittime = 0; + + //Figure out a sensible amount of time to sleep the thread + if (scheduled.Count > 0) + { + //When is the next run scheduled? + TimeSpan nextrun = scheduled.Values.Min((x) => x.Value) - DateTime.UtcNow; + if (nextrun.TotalMilliseconds < 0) + continue; + + //Don't sleep for more than 5 minutes + waittime = (int)Math.Min(nextrun.TotalMilliseconds, 60 * 1000 * 5); + } + else + { + //No tasks, check back later + waittime = 60 * 1000; + } + + //Waiting on the event, enables a wakeup call from termination + // never use waittime = 0 + m_event.WaitOne(Math.Max(100, waittime), false); + } + } + + /// + /// Returns true if the time is at an allowed weekday, false otherwise + /// + /// The time to evaluate + /// The allowed days + /// True if the backup is allowed to run, false otherwise + private static bool IsDateAllowed(DateTime time, DayOfWeek[] allowedDays) + { + var localTime = time.ToLocalTime(); + if (allowedDays == null || allowedDays.Length == 0) + return true; + else + return Array.IndexOf(allowedDays, localTime.DayOfWeek) >= 0; + } + + } +} diff --git a/Duplicati/Server/Serializable/ImportExportStructure.cs b/Duplicati.Library.RestAPI/Serializable/ImportExportStructure.cs similarity index 97% rename from Duplicati/Server/Serializable/ImportExportStructure.cs rename to Duplicati.Library.RestAPI/Serializable/ImportExportStructure.cs index 82855e216..c33d7225c 100644 --- a/Duplicati/Server/Serializable/ImportExportStructure.cs +++ b/Duplicati.Library.RestAPI/Serializable/ImportExportStructure.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System.Collections.Generic; namespace Duplicati.Server.Serializable diff --git a/Duplicati/Server/Serializable/ServerSettings.cs b/Duplicati.Library.RestAPI/Serializable/ServerSettings.cs similarity index 95% rename from Duplicati/Server/Serializable/ServerSettings.cs rename to Duplicati.Library.RestAPI/Serializable/ServerSettings.cs index 4d7c65c0b..7d59af99d 100644 --- a/Duplicati/Server/Serializable/ServerSettings.cs +++ b/Duplicati.Library.RestAPI/Serializable/ServerSettings.cs @@ -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 -{ - /// - /// The server config - /// - public static class ServerSettings - { - /// - /// Shared implementation for reporting dynamic modules - /// - private class DynamicModule : IDynamicModule - { - /// - /// Constructor for backend interface - /// - 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(); - } - - /// - /// Constructor for compression module interface - /// - 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(); - } - - /// - /// Constructor for encryption module interface - /// - 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(); - } - - /// - /// Constructor for generic module interface - /// - 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(); - } - - /// - /// Constructor for webmodule interface - /// - 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(); - } - /// - /// The module key - /// - public string Key { get; private set; } - /// - /// The localized module description - /// - public string Description { get; private set; } - /// - /// Gets the localized display name - /// - /// The display name. - public string DisplayName { get; private set; } - /// - /// The options supported by the module - /// - public Duplicati.Library.Interface.ICommandLineArgument[] Options { get; private set; } - } - - /// - /// Gets all supported options - /// - public static Duplicati.Library.Interface.ICommandLineArgument[] Options - { - get - { - return new Duplicati.Library.Main.Options(new System.Collections.Generic.Dictionary()).SupportedCommands.ToArray(); - } - } - - /// - /// The backend modules known by the server - /// - public static IDynamicModule[] BackendModules - { - get - { - return - (from n in Library.DynamicLoader.BackendLoader.Backends - select new DynamicModule(n)) - .ToArray(); - } - } - /// - /// The encryption modules known by the server - /// - public static IDynamicModule[] EncryptionModules - { - get - { - return - (from n in Library.DynamicLoader.EncryptionLoader.Modules - select new DynamicModule(n)) - .ToArray(); - } - } - - /// - /// The compression modules known by the server - /// - public static IDynamicModule[] CompressionModules - { - get - { - return - (from n in Library.DynamicLoader.CompressionLoader.Modules - select new DynamicModule(n)) - .ToArray(); - } - } - - /// - /// The generic modules known by the server - /// - public static IDynamicModule[] GenericModules - { - get - { - return - (from n in Library.DynamicLoader.GenericLoader.Modules - select new DynamicModule(n)) - .ToArray(); - } - } - - /// - /// The web modules known by the server - /// - public static IDynamicModule[] WebModules - { - get - { - return - (from n in Library.DynamicLoader.WebLoader.Modules - select new DynamicModule(n)) - .ToArray(); - } - } - - /// - /// The web modules known by the server - /// - public static IDynamicModule[] ConnectionModules - { - get - { - return - (from n in Library.DynamicLoader.GenericLoader.Modules - where n is Library.Interface.IConnectionModule - select new DynamicModule(n)) - .ToArray(); - } - } - - /// - /// The server modules known by the server - /// - public static object[] ServerModules - { - get - { - return - (from n in Library.DynamicLoader.GenericLoader.Modules - where n is Library.Interface.IGenericServerModule - select n) - .ToArray(); - } - } - - /// - /// The filters that are applied to all backups - /// - public static IFilter[] Filters - { - get { return Program.DataConnection.Filters; } - } - - /// - /// The settings applied to all backups by default - /// - 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 +{ + /// + /// The server config + /// + public static class ServerSettings + { + /// + /// Shared implementation for reporting dynamic modules + /// + private class DynamicModule : IDynamicModule + { + /// + /// Constructor for backend interface + /// + 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(); + } + + /// + /// Constructor for compression module interface + /// + 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(); + } + + /// + /// Constructor for encryption module interface + /// + 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(); + } + + /// + /// Constructor for generic module interface + /// + 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(); + } + + /// + /// Constructor for webmodule interface + /// + 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(); + } + /// + /// The module key + /// + public string Key { get; private set; } + /// + /// The localized module description + /// + public string Description { get; private set; } + /// + /// Gets the localized display name + /// + /// The display name. + public string DisplayName { get; private set; } + /// + /// The options supported by the module + /// + public Duplicati.Library.Interface.ICommandLineArgument[] Options { get; private set; } + } + + /// + /// Gets all supported options + /// + public static Duplicati.Library.Interface.ICommandLineArgument[] Options + { + get + { + return new Duplicati.Library.Main.Options(new System.Collections.Generic.Dictionary()).SupportedCommands.ToArray(); + } + } + + /// + /// The backend modules known by the server + /// + public static IDynamicModule[] BackendModules + { + get + { + return + (from n in Library.DynamicLoader.BackendLoader.Backends + select new DynamicModule(n)) + .ToArray(); + } + } + /// + /// The encryption modules known by the server + /// + public static IDynamicModule[] EncryptionModules + { + get + { + return + (from n in Library.DynamicLoader.EncryptionLoader.Modules + select new DynamicModule(n)) + .ToArray(); + } + } + + /// + /// The compression modules known by the server + /// + public static IDynamicModule[] CompressionModules + { + get + { + return + (from n in Library.DynamicLoader.CompressionLoader.Modules + select new DynamicModule(n)) + .ToArray(); + } + } + + /// + /// The generic modules known by the server + /// + public static IDynamicModule[] GenericModules + { + get + { + return + (from n in Library.DynamicLoader.GenericLoader.Modules + select new DynamicModule(n)) + .ToArray(); + } + } + + /// + /// The web modules known by the server + /// + public static IDynamicModule[] WebModules + { + get + { + return + (from n in Library.DynamicLoader.WebLoader.Modules + select new DynamicModule(n)) + .ToArray(); + } + } + + /// + /// The web modules known by the server + /// + public static IDynamicModule[] ConnectionModules + { + get + { + return + (from n in Library.DynamicLoader.GenericLoader.Modules + where n is Library.Interface.IConnectionModule + select new DynamicModule(n)) + .ToArray(); + } + } + + /// + /// The server modules known by the server + /// + public static object[] ServerModules + { + get + { + return + (from n in Library.DynamicLoader.GenericLoader.Modules + where n is Library.Interface.IGenericServerModule + select n) + .ToArray(); + } + } + + /// + /// The filters that are applied to all backups + /// + public static IFilter[] Filters + { + get { return FIXMEGlobal.DataConnection.Filters; } + } + + /// + /// The settings applied to all backups by default + /// + public static ISetting[] Settings + { + get { return FIXMEGlobal.DataConnection.Settings; } + } + } +} diff --git a/Duplicati/Server/Serializable/ServerStatus.cs b/Duplicati.Library.RestAPI/Serializable/ServerStatus.cs similarity index 77% rename from Duplicati/Server/Serializable/ServerStatus.cs rename to Duplicati.Library.RestAPI/Serializable/ServerStatus.cs index 5c6c365fb..e6930bd4a 100644 --- a/Duplicati/Server/Serializable/ServerStatus.cs +++ b/Duplicati.Library.RestAPI/Serializable/ServerStatus.cs @@ -1,148 +1,149 @@ -// 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 -{ - /// - /// This class collects all reportable status properties into a single class that can be exported as JSON - /// - public class ServerStatus : Duplicati.Server.Serialization.Interface.IServerStatus - { - public LiveControlState ProgramState - { - get { return EnumConverter.Convert(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 ActiveTask - { - get - { - var t = Program.WorkThread.CurrentTask; - if (t == null) - return null; - else - return new Tuple(t.TaskID, t.Backup == null ? null : t.Backup.ID); - } - } - - public IList> SchedulerQueueIds - { - get { return (from n in Program.Scheduler.WorkerQueue where n.Backup != null select new Tuple(n.TaskID, n.Backup.ID)).ToList(); } - } - - public IList> 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(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. +#endregion +using System; +using System.Linq; +using System.Collections.Generic; +using Duplicati.Server.Serialization; +using Duplicati.Library.RestAPI; + +namespace Duplicati.Server.Serializable +{ + /// + /// This class collects all reportable status properties into a single class that can be exported as JSON + /// + public class ServerStatus : Duplicati.Server.Serialization.Interface.IServerStatus + { + public LiveControlState ProgramState + { + get { return EnumConverter.Convert(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 UpdatePollerStates UpdaterState { get { return FIXMEGlobal.UpdatePoller.ThreadState; } } + + public bool UpdateReady { get { return Duplicati.Library.AutoUpdater.UpdaterManager.HasUpdateInstalled; } } + + public double UpdateDownloadProgress { get { return FIXMEGlobal.UpdatePoller.DownloadProgess; } } + + + public Tuple ActiveTask + { + get + { + var t = FIXMEGlobal.WorkThread.CurrentTask; + if (t == null) + return null; + else + return new Tuple(t.TaskID, t.Backup == null ? null : t.Backup.ID); + } + } + + public IList> SchedulerQueueIds + { + get { return (from n in FIXMEGlobal.Scheduler.WorkerQueue where n.Backup != null select new Tuple(n.TaskID, n.Backup.ID)).ToList(); } + } + + public IList> 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(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(); } } + + } +} + diff --git a/Duplicati/Server/Serializable/TreeNode.cs b/Duplicati.Library.RestAPI/Serializable/TreeNode.cs similarity index 57% rename from Duplicati/Server/Serializable/TreeNode.cs rename to Duplicati.Library.RestAPI/Serializable/TreeNode.cs index 13216fbd4..02a107971 100644 --- a/Duplicati/Server/Serializable/TreeNode.cs +++ b/Duplicati.Library.RestAPI/Serializable/TreeNode.cs @@ -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 -{ - /// - /// Implementation of a ExtJS treenode-like class for easy JSON export - /// - public class TreeNode - { - /// - /// The text displayed for the node - /// - public string text { get; set; } - /// - /// The node id - /// - public string id { get; set; } - /// - /// The class applied to the node - /// - public string cls { get; set; } - /// - /// The class applied to the icon - /// - public string iconCls { get; set; } - /// - /// True if the element should be checked - /// - public bool check { get; set; } - /// - /// True if the element is a leaf node - /// - public bool leaf { get; set; } - /// - /// Gets or sets the current path, if the item is a symbolic path - /// - public string resolvedpath { get; set; } - /// - /// True if the element is hidden - /// - public bool hidden { get; set; } - /// - /// True if the element is a symlink - /// - public bool symlink { get; set; } - - /// - /// Constructs a new TreeNode - /// - 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 +{ + /// + /// Implementation of a ExtJS treenode-like class for easy JSON export + /// + public class TreeNode + { + /// + /// The text displayed for the node + /// + public string text { get; set; } + /// + /// The node id + /// + public string id { get; set; } + /// + /// The class applied to the node + /// + public string cls { get; set; } + /// + /// The class applied to the icon + /// + public string iconCls { get; set; } + /// + /// True if the element should be checked + /// + public bool check { get; set; } + /// + /// True if the element is a leaf node + /// + public bool leaf { get; set; } + /// + /// Gets or sets the current path, if the item is a symbolic path + /// + public string resolvedpath { get; set; } + /// + /// True if the element is hidden + /// + public bool hidden { get; set; } + /// + /// True if the element is a symlink + /// + public bool symlink { get; set; } + + /// + /// Constructs a new TreeNode + /// + public TreeNode() + { + this.cls = "folder"; + this.iconCls = "x-tree-icon-parent"; + this.check = false; + } + } +} diff --git a/Duplicati/Server/SpecialFolders.cs b/Duplicati.Library.RestAPI/SpecialFolders.cs similarity index 83% rename from Duplicati/Server/SpecialFolders.cs rename to Duplicati.Library.RestAPI/SpecialFolders.cs index 865eaacf7..5bcbd239f 100644 --- a/Duplicati/Server/SpecialFolders.cs +++ b/Duplicati.Library.RestAPI/SpecialFolders.cs @@ -1,24 +1,20 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using System.Linq; using System.Collections.Generic; diff --git a/Duplicati/Server/Strings.cs b/Duplicati.Library.RestAPI/Strings.cs similarity index 86% rename from Duplicati/Server/Strings.cs rename to Duplicati.Library.RestAPI/Strings.cs index 438960d48..d925113eb 100644 --- a/Duplicati/Server/Strings.cs +++ b/Duplicati.Library.RestAPI/Strings.cs @@ -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 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 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())); } + } + +} diff --git a/Duplicati/Server/UpdatePollThread.cs b/Duplicati.Library.RestAPI/UpdatePollThread.cs similarity index 68% rename from Duplicati/Server/UpdatePollThread.cs rename to Duplicati.Library.RestAPI/UpdatePollThread.cs index a4f6c0404..c82bd8d2d 100644 --- a/Duplicati/Server/UpdatePollThread.cs +++ b/Duplicati.Library.RestAPI/UpdatePollThread.cs @@ -1,240 +1,237 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Linq; -using System.Threading; -using Duplicati.Server.Serialization; - -namespace Duplicati.Server -{ - /// - /// The thread that checks on the update server if new versions are available - /// - public class UpdatePollThread - { - 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; - private double m_downloadProgress; - - public bool IsUpdateRequested { get; private set; } = false; - - public UpdatePollerStates ThreadState { get; private set; } - public double DownloadProgess - { - get { return m_downloadProgress ; } - - private set - { - var oldv = m_downloadProgress; - m_downloadProgress = value; - if ((int)(oldv * 100) != (int)(value * 100)) - Program.StatusEventNotifyer.SignalNewEvent(); - } - } - - public UpdatePollThread() - { - m_waitSignal = new AutoResetEvent(false); - ThreadState = UpdatePollerStates.Waiting; - m_thread = new Thread(Run); - m_thread.IsBackground = true; - m_thread.Name = "UpdatePollThread"; - m_thread.Start(); - } - - public void CheckNow() - { - lock(m_lock) - { - m_forceCheck = true; - m_waitSignal.Set(); - } - } - - public void 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) - { - m_terminated = true; - m_waitSignal.Set(); - } - } - - public void Reschedule() - { - m_waitSignal.Set(); - } - - private void Run() - { - // Wait on startup - m_waitSignal.WaitOne(TimeSpan.FromMinutes(1), true); - - while (!m_terminated) - { - var nextCheck = Program.DataConnection.ApplicationSettings.NextUpdateCheck; - - var maxcheck = TimeSpan.FromDays(7); - try - { - maxcheck = Library.Utility.Timeparser.ParseTimeSpan(Program.DataConnection.ApplicationSettings.UpdateCheckInterval); - } - catch - { - } - - // If we have some weirdness, just check now - if (nextCheck - DateTime.UtcNow > maxcheck) - nextCheck = DateTime.UtcNow - TimeSpan.FromSeconds(1); - - if (nextCheck < DateTime.UtcNow || m_forceCheck) - { - lock(m_lock) - m_forceCheck = false; - - ThreadState = UpdatePollerStates.Checking; - Program.StatusEventNotifyer.SignalNewEvent(); - - DateTime started = DateTime.UtcNow; - Program.DataConnection.ApplicationSettings.LastUpdateCheck = started; - nextCheck = Program.DataConnection.ApplicationSettings.NextUpdateCheck; - - Library.AutoUpdater.ReleaseType rt; - if (!Enum.TryParse(Program.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; - } - catch - { - } - - // It could be that we have registered an update from a more unstable channel, - // but the user has switched to a more stable channel. - // In that case we discard the old update to avoid offering it. - if (Program.DataConnection.ApplicationSettings.UpdatedVersion != null) - { - Library.AutoUpdater.ReleaseType updatert; - var updatertstring = Program.DataConnection.ApplicationSettings.UpdatedVersion.ReleaseType; - if (string.Equals(updatertstring, "preview", StringComparison.OrdinalIgnoreCase)) - updatertstring = Library.AutoUpdater.ReleaseType.Experimental.ToString(); - - if (!Enum.TryParse(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; - } - - if (Program.DataConnection.ApplicationSettings.UpdatedVersion != null && Duplicati.Library.AutoUpdater.UpdaterManager.TryParseVersion(Program.DataConnection.ApplicationSettings.UpdatedVersion.Version) > System.Reflection.Assembly.GetExecutingAssembly().GetName().Version) - { - Program.DataConnection.RegisterNotification( - NotificationType.Information, - "Found update", - Program.DataConnection.ApplicationSettings.UpdatedVersion.Displayname, - null, - null, - "update:new", - null, - "NewUpdateFound", - null, - (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(); - } - - var waitTime = nextCheck - DateTime.UtcNow; - - // Guard against spin-loop - if (waitTime.TotalSeconds < 5) - waitTime = TimeSpan.FromSeconds(5); - - // Guard against year-long waits - // A re-check does not cause an update check - if (waitTime.TotalDays > 1) - waitTime = TimeSpan.FromDays(1); - - m_waitSignal.WaitOne(waitTime, true); - } - } - } -} - +// Copyright (C) 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 +{ + /// + /// The thread that checks on the update server if new versions are available + /// + public class UpdatePollThread + { + 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; + private double m_downloadProgress; + + public bool IsUpdateRequested { get; private set; } = false; + + public UpdatePollerStates ThreadState { get; private set; } + public double DownloadProgess + { + get { return m_downloadProgress ; } + + private set + { + var oldv = m_downloadProgress; + m_downloadProgress = value; + if ((int)(oldv * 100) != (int)(value * 100)) + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + } + + public UpdatePollThread() + { + m_waitSignal = new AutoResetEvent(false); + ThreadState = UpdatePollerStates.Waiting; + m_thread = new Thread(Run); + m_thread.IsBackground = true; + m_thread.Name = "UpdatePollThread"; + m_thread.Start(); + } + + public void CheckNow() + { + lock(m_lock) + { + m_forceCheck = true; + m_waitSignal.Set(); + } + } + + public void InstallUpdate() + { + lock(m_lock) + { + m_forceCheck = true; + m_download = true; + m_waitSignal.Set(); + } + } + + public void ActivateUpdate() + { + if (Duplicati.Library.AutoUpdater.UpdaterManager.SetRunUpdate()) + { + IsUpdateRequested = true; + FIXMEGlobal.ApplicationExitEvent.Set(); + } + } + + public void Terminate() + { + lock(m_lock) + { + m_terminated = true; + m_waitSignal.Set(); + } + } + + public void Reschedule() + { + m_waitSignal.Set(); + } + + private void Run() + { + // Wait on startup + m_waitSignal.WaitOne(TimeSpan.FromMinutes(1), true); + + while (!m_terminated) + { + var nextCheck = FIXMEGlobal.DataConnection.ApplicationSettings.NextUpdateCheck; + + var maxcheck = TimeSpan.FromDays(7); + try + { + maxcheck = Library.Utility.Timeparser.ParseTimeSpan(FIXMEGlobal.DataConnection.ApplicationSettings.UpdateCheckInterval); + } + catch + { + } + + // If we have some weirdness, just check now + if (nextCheck - DateTime.UtcNow > maxcheck) + nextCheck = DateTime.UtcNow - TimeSpan.FromSeconds(1); + + if (nextCheck < DateTime.UtcNow || m_forceCheck) + { + lock(m_lock) + m_forceCheck = false; + + ThreadState = UpdatePollerStates.Checking; + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + DateTime started = DateTime.UtcNow; + FIXMEGlobal.DataConnection.ApplicationSettings.LastUpdateCheck = started; + nextCheck = FIXMEGlobal.DataConnection.ApplicationSettings.NextUpdateCheck; + + Library.AutoUpdater.ReleaseType rt; + if (!Enum.TryParse(FIXMEGlobal.DataConnection.ApplicationSettings.UpdateChannel, true, out rt)) + rt = Duplicati.Library.AutoUpdater.ReleaseType.Unknown; + + // Choose the default channel in case we have unknown + rt = rt == Duplicati.Library.AutoUpdater.ReleaseType.Unknown ? Duplicati.Library.AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel : rt; + + try + { + var update = Duplicati.Library.AutoUpdater.UpdaterManager.CheckForUpdate(rt); + if (update != null) + FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion = update; + } + catch + { + } + + // It could be that we have registered an update from a more unstable channel, + // but the user has switched to a more stable channel. + // In that case we discard the old update to avoid offering it. + if (FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion != null) + { + Library.AutoUpdater.ReleaseType updatert; + var updatertstring = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion.ReleaseType; + if (string.Equals(updatertstring, "preview", StringComparison.OrdinalIgnoreCase)) + updatertstring = Library.AutoUpdater.ReleaseType.Experimental.ToString(); + + if (!Enum.TryParse(updatertstring, true, out updatert)) + updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly; + + if (updatert == Duplicati.Library.AutoUpdater.ReleaseType.Unknown) + updatert = Duplicati.Library.AutoUpdater.ReleaseType.Nightly; + + if (updatert > rt) + FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion = null; + } + + if (FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion != null && Duplicati.Library.AutoUpdater.UpdaterManager.TryParseVersion(FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion.Version) > System.Reflection.Assembly.GetExecutingAssembly().GetName().Version) + { + FIXMEGlobal.DataConnection.RegisterNotification( + NotificationType.Information, + "Found update", + FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion.Displayname, + null, + null, + "update:new", + null, + "NewUpdateFound", + null, + (self, all) => { + return all.FirstOrDefault(x => x.Action == "update:new") ?? self; + } + ); + } + } + + if (m_download) + { + lock(m_lock) + m_download = false; + + var v = FIXMEGlobal.DataConnection.ApplicationSettings.UpdatedVersion; + if (v != null) + { + ThreadState = UpdatePollerStates.Downloading; + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + + if (Duplicati.Library.AutoUpdater.UpdaterManager.DownloadAndUnpackUpdate(v, (pg) => { DownloadProgess = pg; })) + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + } + + DownloadProgess = 0; + + if (ThreadState != UpdatePollerStates.Waiting) + { + ThreadState = UpdatePollerStates.Waiting; + FIXMEGlobal.StatusEventNotifyer.SignalNewEvent(); + } + + var waitTime = nextCheck - DateTime.UtcNow; + + // Guard against spin-loop + if (waitTime.TotalSeconds < 5) + waitTime = TimeSpan.FromSeconds(5); + + // Guard against year-long waits + // A re-check does not cause an update check + if (waitTime.TotalDays > 1) + waitTime = TimeSpan.FromDays(1); + + m_waitSignal.WaitOne(waitTime, true); + } + } + } +} + diff --git a/Duplicati/Server/WebServer/AuthenticationHandler.cs b/Duplicati.Library.RestAPI/WebServer/AuthenticationHandler.cs similarity index 94% rename from Duplicati/Server/WebServer/AuthenticationHandler.cs rename to Duplicati.Library.RestAPI/WebServer/AuthenticationHandler.cs index d7a22111f..a7ad72356 100644 --- a/Duplicati/Server/WebServer/AuthenticationHandler.cs +++ b/Duplicati.Library.RestAPI/WebServer/AuthenticationHandler.cs @@ -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 m_activeTokens = new ConcurrentDictionary(); - private readonly ConcurrentDictionary> m_activeNonces = new ConcurrentDictionary>(); - private readonly ConcurrentDictionary m_activexsrf = new ConcurrentDictionary(); - - 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(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 m_activeTokens = new ConcurrentDictionary(); + private readonly ConcurrentDictionary> m_activeNonces = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary m_activexsrf = new ConcurrentDictionary(); + + 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(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; + } + } +} + diff --git a/Duplicati/Server/WebServer/BodyWriter.cs b/Duplicati.Library.RestAPI/WebServer/BodyWriter.cs similarity index 55% rename from Duplicati/Server/WebServer/BodyWriter.cs rename to Duplicati.Library.RestAPI/WebServer/BodyWriter.cs index 44d72a096..a411d98ac 100644 --- a/Duplicati/Server/WebServer/BodyWriter.cs +++ b/Duplicati.Library.RestAPI/WebServer/BodyWriter.cs @@ -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(); + } + } + +} + diff --git a/Duplicati/Server/WebServer/CaptchaUtil.cs b/Duplicati.Library.RestAPI/WebServer/CaptchaUtil.cs similarity index 98% rename from Duplicati/Server/WebServer/CaptchaUtil.cs rename to Duplicati.Library.RestAPI/WebServer/CaptchaUtil.cs index 7cba33300..4146e2768 100644 --- a/Duplicati/Server/WebServer/CaptchaUtil.cs +++ b/Duplicati.Library.RestAPI/WebServer/CaptchaUtil.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Linq; using System.Drawing; diff --git a/Duplicati/Server/WebServer/IndexHtmlHandler.cs b/Duplicati.Library.RestAPI/WebServer/IndexHtmlHandler.cs similarity index 97% rename from Duplicati/Server/WebServer/IndexHtmlHandler.cs rename to Duplicati.Library.RestAPI/WebServer/IndexHtmlHandler.cs index 06b3a5249..fe6c70478 100644 --- a/Duplicati/Server/WebServer/IndexHtmlHandler.cs +++ b/Duplicati.Library.RestAPI/WebServer/IndexHtmlHandler.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Linq; using HttpServer; diff --git a/Duplicati/Server/WebServer/RESTHandler.cs b/Duplicati.Library.RestAPI/WebServer/RESTHandler.cs similarity index 95% rename from Duplicati/Server/WebServer/RESTHandler.cs rename to Duplicati.Library.RestAPI/WebServer/RESTHandler.cs index 3f1384046..10c147b76 100644 --- a/Duplicati/Server/WebServer/RESTHandler.cs +++ b/Duplicati.Library.RestAPI/WebServer/RESTHandler.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Collections.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); } } } diff --git a/Duplicati/Server/WebServer/Server.cs b/Duplicati.Library.RestAPI/WebServer/Server.cs similarity index 88% rename from Duplicati/Server/WebServer/Server.cs rename to Duplicati.Library.RestAPI/WebServer/Server.cs index 726279aba..c8e4408f3 100644 --- a/Duplicati/Server/WebServer/Server.cs +++ b/Duplicati.Library.RestAPI/WebServer/Server.cs @@ -1,474 +1,455 @@ -// 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 - { - /// - /// The tag used for logging - /// - private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(); - - /// - /// Option for changing the webroot folder - /// - public const string OPTION_WEBROOT = "webservice-webroot"; - - /// - /// Option for changing the webservice listen port - /// - public const string OPTION_PORT = "webservice-port"; - - /// - /// Option for changing the webservice listen interface - /// - public const string OPTION_INTERFACE = "webservice-interface"; - - /// - /// The default path to the web root - /// - public const string DEFAULT_OPTION_WEBROOT = "webroot"; - - /// - /// The default listening port - /// - public const int DEFAULT_OPTION_PORT = 8200; - - /// - /// Option for setting the webservice SSL certificate - /// - public const string OPTION_SSLCERTIFICATEFILE = "webservice-sslcertificatefile"; - - /// - /// Option for setting the webservice SSL certificate key - /// - public const string OPTION_SSLCERTIFICATEFILEPASSWORD = "webservice-sslcertificatepassword"; - - /// - /// The default listening interface - /// - public const string DEFAULT_OPTION_INTERFACE = "loopback"; - - /// - /// The single webserver instance - /// - private readonly HttpServer.HttpServer m_server; - - /// - /// The webserver listening port - /// - public readonly int Port; - - /// - /// A string that is sent out instead of password values - /// - public const string PASSWORD_PLACEHOLDER = "**********"; - - /// - /// Sets up the webserver and starts it - /// - /// A set of options - public Server(IDictionary options) - { - string portstring; - IEnumerable 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 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); - } - } - - /// - /// Module for injecting host header verification - /// - private class HostHeaderChecker : HttpModule - { - /// - /// The hostnames that we allow - /// - private string[] m_lastSplitNames; - - /// - /// The string used to generate m_lastSplitNames; - /// - private string m_lastAllowed; - - /// - /// A regex to detect potential IPv4 addresses. - /// Note that this also detects things that are not valid IPv4. - /// - private static readonly System.Text.RegularExpressions.Regex IPV4 = new System.Text.RegularExpressions.Regex(@"((\d){1,3}\.){3}(\d){1,3}"); - /// - /// A regex to detect potential IPv6 addresses. - /// Note that this also detects things that are not valid IPv6. - /// - private static readonly System.Text.RegularExpressions.Regex IPV6 = new System.Text.RegularExpressions.Regex(@"(\:)?(\:?[A-Fa-f0-9]{1,4}\:?){1,8}(\:)?"); - - /// - /// The hostnames that are always allowed - /// - private static readonly string[] DEFAULT_ALLOWED = new string[] { "localhost", "127.0.0.1", "::1", "localhost.localdomain" }; - - /// - /// Process the received request - /// - /// A flag indicating if the request is handled. - /// The received request. - /// The response object. - /// The session state. - 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 + { + /// + /// The tag used for logging + /// + private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType(); + + /// + /// Option for changing the webroot folder + /// + public const string OPTION_WEBROOT = "webservice-webroot"; + + /// + /// Option for changing the webservice listen port + /// + public const string OPTION_PORT = "webservice-port"; + + /// + /// Option for changing the webservice listen interface + /// + public const string OPTION_INTERFACE = "webservice-interface"; + + /// + /// The default path to the web root + /// + public const string DEFAULT_OPTION_WEBROOT = "webroot"; + + /// + /// The default listening port + /// + public const int DEFAULT_OPTION_PORT = 8200; + + /// + /// Option for setting the webservice SSL certificate + /// + public const string OPTION_SSLCERTIFICATEFILE = "webservice-sslcertificatefile"; + + /// + /// Option for setting the webservice SSL certificate key + /// + public const string OPTION_SSLCERTIFICATEFILEPASSWORD = "webservice-sslcertificatepassword"; + + /// + /// The default listening interface + /// + public const string DEFAULT_OPTION_INTERFACE = "loopback"; + + /// + /// The single webserver instance + /// + private readonly HttpServer.HttpServer m_server; + + /// + /// The webserver listening port + /// + public readonly int Port; + + /// + /// A string that is sent out instead of password values + /// + public const string PASSWORD_PLACEHOLDER = "**********"; + + /// + /// Sets up the webserver and starts it + /// + /// A set of options + public Server(IDictionary options) + { + string portstring; + IEnumerable 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 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(Duplicati.Library.AutoUpdater.UpdaterManager.InstalledBaseDir), 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); + } + } + + /// + /// Module for injecting host header verification + /// + private class HostHeaderChecker : HttpModule + { + /// + /// The hostnames that we allow + /// + private string[] m_lastSplitNames; + + /// + /// The string used to generate m_lastSplitNames; + /// + private string m_lastAllowed; + + /// + /// A regex to detect potential IPv4 addresses. + /// Note that this also detects things that are not valid IPv4. + /// + private static readonly System.Text.RegularExpressions.Regex IPV4 = new System.Text.RegularExpressions.Regex(@"((\d){1,3}\.){3}(\d){1,3}"); + /// + /// A regex to detect potential IPv6 addresses. + /// Note that this also detects things that are not valid IPv6. + /// + private static readonly System.Text.RegularExpressions.Regex IPV6 = new System.Text.RegularExpressions.Regex(@"(\:)?(\:?[A-Fa-f0-9]{1,4}\:?){1,8}(\:)?"); + + /// + /// The hostnames that are always allowed + /// + private static readonly string[] DEFAULT_ALLOWED = new string[] { "localhost", "127.0.0.1", "::1", "localhost.localdomain" }; + + /// + /// Process the received request + /// + /// A flag indicating if the request is handled. + /// The received request. + /// The response object. + /// The session state. + 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; + + } + } + } +} diff --git a/Duplicati/Server/WebServer/SynologyAuthenticationHandler.cs b/Duplicati.Library.RestAPI/WebServer/SynologyAuthenticationHandler.cs similarity index 97% rename from Duplicati/Server/WebServer/SynologyAuthenticationHandler.cs rename to Duplicati.Library.RestAPI/WebServer/SynologyAuthenticationHandler.cs index 2a536f03c..cf16695e5 100644 --- a/Duplicati/Server/WebServer/SynologyAuthenticationHandler.cs +++ b/Duplicati.Library.RestAPI/WebServer/SynologyAuthenticationHandler.cs @@ -18,7 +18,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. - using System; using System.Collections.Concurrent; using System.Collections.Generic; diff --git a/Duplicati/Server/newbackup.json b/Duplicati.Library.RestAPI/newbackup.json similarity index 96% rename from Duplicati/Server/newbackup.json rename to Duplicati.Library.RestAPI/newbackup.json index 97c35f4fa..3fbb3c65b 100644 --- a/Duplicati/Server/newbackup.json +++ b/Duplicati.Library.RestAPI/newbackup.json @@ -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"] + } } \ No newline at end of file diff --git a/Duplicati.sln b/Duplicati.sln index fb688daf6..832185a9b 100644 --- a/Duplicati.sln +++ b/Duplicati.sln @@ -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 @@ -30,6 +30,9 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.DynamicLoader", "Duplicati\Library\DynamicLoader\Duplicati.Library.DynamicLoader.csproj", "{0CA86ECF-5BEC-4909-B4F6-110A03B30B92}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Snapshots", "Duplicati\Library\Snapshots\Duplicati.Library.Snapshots.csproj", "{D63E53E4-A458-4C2F-914D-92F715F58ACE}" + ProjectSection(ProjectDependencies) = postProject + {2B605B81-353F-43CD-BF3A-29B3926A204F} = {2B605B81-353F-43CD-BF3A-29B3926A204F} + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.License", "Duplicati\License\Duplicati.License.csproj", "{4D012CB1-4B92-47F4-89B7-BF80A73A2E99}" EndProject @@ -79,8 +82,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 +100,61 @@ 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.Tardigrade", "Duplicati\Library\Backend\Tardigrade\Duplicati.Library.Backend.Tardigrade.csproj", "{11A8D03B-8CAE-4AD6-A5C6-E15D34DA2BDE}" + ProjectSection(ProjectDependencies) = postProject + {E9AB8491-BD4C-4E4F-84C3-0BD551CC7489} = {E9AB8491-BD4C-4E4F-84C3-0BD551CC7489} + EndProjectSection 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}") = "net5", "net5", "{6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.AutoUpdater", "Executables\net5\Duplicati.CommandLine.AutoUpdater\Duplicati.CommandLine.AutoUpdater.csproj", "{95B7DD83-2C5A-4F1E-8EA7-39654B2B236A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Service", "Executables\net5\Duplicati_Service\Duplicati.Service.csproj", "{34149709-F3ED-4FB5-A087-43EB195C948B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.BackendTester", "Executables\net5\Duplicati.CommandLine.BackendTester\Duplicati.CommandLine.BackendTester.csproj", "{2F1C0C8D-5C15-4BC0-811F-87F2C98D9790}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.BackendTool", "Executables\net5\Duplicati.CommandLine.BackendTool\Duplicati.CommandLine.BackendTool.csproj", "{31FA5B9B-4CD6-4BF3-B7A9-12C1E30DCAF6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.ConfigurationImporter", "Executables\net5\Duplicati.CommandLine.ConfigurationImporter\Duplicati.CommandLine.ConfigurationImporter.csproj", "{156B9B77-C2B7-4169-8FE7-1171D221446C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.RecoveryTool", "Executables\net5\Duplicati.CommandLine.RecoveryTool\Duplicati.CommandLine.RecoveryTool.csproj", "{0FFC557E-1B84-46A2-B6E8-06064FA7EA58}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.GUI.TrayIcon", "Executables\net5\Duplicati.GUI.TrayIcon\Duplicati.GUI.TrayIcon.csproj", "{AF32C621-30DC-40F5-8CE6-DD69053068E9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Server", "Executables\net5\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\net5\Duplicati.WindowsService\Duplicati.WindowsService.csproj", "{5D20B150-C445-47BE-8CE8-C9F74F19A4F2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine", "Executables\net5\Duplicati.CommandLine\Duplicati.CommandLine.csproj", "{0F5A1F4E-25FA-4D02-920D-CA2138498081}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnixSupport", "Duplicati\UnixSupport\UnixSupport.csproj", "{2B605B81-353F-43CD-BF3A-29B3926A204F}" 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -265,10 +314,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 +350,142 @@ 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 + {11A8D03B-8CAE-4AD6-A5C6-E15D34DA2BDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11A8D03B-8CAE-4AD6-A5C6-E15D34DA2BDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11A8D03B-8CAE-4AD6-A5C6-E15D34DA2BDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11A8D03B-8CAE-4AD6-A5C6-E15D34DA2BDE}.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 + {2B605B81-353F-43CD-BF3A-29B3926A204F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B605B81-353F-43CD-BF3A-29B3926A204F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B605B81-353F-43CD-BF3A-29B3926A204F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B605B81-353F-43CD-BF3A-29B3926A204F}.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} + {11A8D03B-8CAE-4AD6-A5C6-E15D34DA2BDE} = {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} + {2B605B81-353F-43CD-BF3A-29B3926A204F} = {566EBBDA-19A4-4056-A615-D901D57D2439} + {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 +501,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 diff --git a/Duplicati/CommandLine/AutoUpdater/Duplicati.CommandLine.AutoUpdater.csproj b/Duplicati/CommandLine/AutoUpdater/Duplicati.CommandLine.AutoUpdater.csproj new file mode 100644 index 000000000..5a2137134 --- /dev/null +++ b/Duplicati/CommandLine/AutoUpdater/Duplicati.CommandLine.AutoUpdater.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + Duplicati.CommandLine.AutoUpdater.Implementation + Duplicati.CommandLine.AutoUpdater + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Duplicati/Library/AutoUpdater/Program.cs b/Duplicati/CommandLine/AutoUpdater/Program.cs similarity index 98% rename from Duplicati/Library/AutoUpdater/Program.cs rename to Duplicati/CommandLine/AutoUpdater/Program.cs index ad75a2ecc..2283cddc1 100644 --- a/Duplicati/Library/AutoUpdater/Program.cs +++ b/Duplicati/CommandLine/AutoUpdater/Program.cs @@ -1,147 +1,147 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; -using System.Linq; -using System.Collections.Generic; -using Duplicati.Library.Common.IO; - -namespace Duplicati.Library.AutoUpdater -{ - public static class Program - { - public static int Main(string[] args) - { - // Ignore webroot during startup verification - Duplicati.Library.AutoUpdater.UpdaterManager.IgnoreWebrootFolder = true; - return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args, AutoUpdateStrategy.Never); - } - - public static int RealMain(string[] _args) - { - // Enable webroot checks for the verifier - Duplicati.Library.AutoUpdater.UpdaterManager.IgnoreWebrootFolder = false; - - var args = new List(_args); - Duplicati.Library.Utility.CommandLineParser.ExtractOptions(args); - - if (args.Count == 0) - { - WriteUsage(); - return 100; - } - else if (args.Count != 1) - { - Console.WriteLine("Invalid number of arguments, got {0}:{1}{1}{2}{1}{1}", args.Count, Environment.NewLine, string.Join(Environment.NewLine, args)); - return 100; - } - - UpdaterManager.OnError += (ex) => Console.WriteLine("Error detected: {0}", ex); - - var cmd = args[0].ToLowerInvariant().Trim(); - switch (cmd) - { - case "help": - WriteUsage(); - return 0; - case "list": - { - var versions = UpdaterManager.FindInstalledVersions(); - var selfdir = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); - if (string.Equals(Util.AppendDirSeparator(selfdir), Util.AppendDirSeparator(UpdaterManager.InstalledBaseDir))) - versions = versions.Union(new KeyValuePair[] { new KeyValuePair(selfdir, UpdaterManager.SelfVersion) }); - Console.WriteLine(string.Join(Environment.NewLine, versions.Select(x => string.Format(" {0} {1} ({2})", (x.Value.Version == UpdaterManager.SelfVersion.Version ? "*" : "-"), x.Value.Displayname, x.Value.Version)))); - return 0; - } - - case "verify": - { - var versions = UpdaterManager.FindInstalledVersions(); - var selfdir = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); - if (string.Equals(Util.AppendDirSeparator(selfdir), Util.AppendDirSeparator(UpdaterManager.InstalledBaseDir))) - versions = versions.Union(new KeyValuePair[] { new KeyValuePair(selfdir, UpdaterManager.SelfVersion) }); - - Console.WriteLine(string.Join(Environment.NewLine, versions.Select(x => string.Format(" {0} {1} ({2}): {3}", (x.Value.Version == UpdaterManager.SelfVersion.Version ? "*" : "-"), x.Value.Displayname, x.Value.Version, UpdaterManager.VerifyUnpackedFolder(x.Key, x.Value) ? "Valid" : "*** Modified ***")))); - return 0; - } - case "check": - { - var update = UpdaterManager.CheckForUpdate(); - if (update == null) - Console.WriteLine("No updates found"); - else if (update.Version == UpdaterManager.SelfVersion.Version) - Console.WriteLine("You are running the latest version: {0}", update.Displayname); - else - Console.WriteLine("New version is available: {0}", update.Displayname); - - return 0; - } - case "install": - { - var update = UpdaterManager.CheckForUpdate(); - if (update == null || update.Version == UpdaterManager.SelfVersion.Version) - { - Console.WriteLine("You are running the latest version: {0} ({1})", UpdaterManager.SelfVersion.Displayname, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version); - return 0; - } - Console.WriteLine("Downloading update \"{0}\" ...", update.Displayname); - - long lastpg = 0; - UpdaterManager.DownloadAndUnpackUpdate(update, f => { - var npg = (long)(f*100); - if (Math.Abs(npg - lastpg) >= 5 || (npg == 100 && lastpg != 100)) - { - lastpg = npg; - Console.WriteLine("Downloading {0}% ...", npg); - } - }); - - Console.WriteLine("New version \"{0}\" installed!", update.Displayname); - return 0; - } - default: - Console.WriteLine("Unknown command: \"{0}\"", args[0]); - Console.WriteLine(); - Console.WriteLine("Try the command \"{0}\" instead", "help"); - return 100; - } - } - - private static void WriteUsage() - { - Console.WriteLine("Usage:{0}\t{1}{2} [LIST|VERIFY|CHECK|INSTALL|HELP]", Environment.NewLine, Duplicati.Library.Utility.Utility.IsMono ? "mono " : "", System.IO.Path.GetFileName(System.Reflection.Assembly.GetExecutingAssembly().Location)); - Console.WriteLine(); - Console.WriteLine("Environment variables:"); - Console.WriteLine(); - Console.WriteLine("{0} - Disables updates completely", string.Format(UpdaterManager.SKIPUPDATE_ENVNAME_TEMPLATE, AutoUpdateSettings.AppName)); - Console.WriteLine("{0} - Choose how to handle updates, valid settings: {1}", string.Format(UpdaterManager.UPDATE_STRATEGY_ENVNAME_TEMPLATE, AutoUpdateSettings.AppName), string.Join(", ", Enum.GetNames(typeof(AutoUpdateStrategy)))); - Console.WriteLine("{0} - Use alternate updates urls", string.Format(AutoUpdateSettings.UPDATEURL_ENVNAME_TEMPLATE, AutoUpdateSettings.AppName)); - Console.WriteLine("{0} - Choose different channel than the default {1}, valid settings: {2}", string.Format(AutoUpdateSettings.UPDATECHANNEL_ENVNAME_TEMPLATE, AutoUpdateSettings.AppName), AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel, string.Join(",", Enum.GetNames(typeof(ReleaseType)).Where( x => x != ReleaseType.Unknown.ToString()))); - Console.WriteLine(); - Console.WriteLine("Updates are downloaded from: {0}", string.Join(";", AutoUpdateSettings.URLs)); - Console.WriteLine("Updates are installed in: {0}", UpdaterManager.INSTALLDIR); - Console.WriteLine("The base version is \"{0}\" ({1}) and is installed in: {2}", UpdaterManager.BaseVersion.Displayname, UpdaterManager.BaseVersion.Version, UpdaterManager.InstalledBaseDir); - Console.WriteLine("This version is \"{0}\" ({1}) and is installed in: {2}", UpdaterManager.SelfVersion.Displayname, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version, System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)); - Console.WriteLine(); - } - } -} - +// Copyright (C) 2024, The Duplicati Team +// https://duplicati.com, hello@duplicati.com +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Linq; +using System.Collections.Generic; +using Duplicati.Library.Common.IO; + +namespace Duplicati.Library.AutoUpdater +{ + public static class Program + { + public static int Main(string[] args) + { + // Ignore webroot during startup verification + Duplicati.Library.AutoUpdater.UpdaterManager.IgnoreWebrootFolder = true; + return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args, AutoUpdateStrategy.Never); + } + + public static int RealMain(string[] _args) + { + // Enable webroot checks for the verifier + Duplicati.Library.AutoUpdater.UpdaterManager.IgnoreWebrootFolder = false; + + var args = new List(_args); + Duplicati.Library.Utility.CommandLineParser.ExtractOptions(args); + + if (args.Count == 0) + { + WriteUsage(); + return 100; + } + else if (args.Count != 1) + { + Console.WriteLine("Invalid number of arguments, got {0}:{1}{1}{2}{1}{1}", args.Count, Environment.NewLine, string.Join(Environment.NewLine, args)); + return 100; + } + + UpdaterManager.OnError += (ex) => Console.WriteLine("Error detected: {0}", ex); + + var cmd = args[0].ToLowerInvariant().Trim(); + switch (cmd) + { + case "help": + WriteUsage(); + return 0; + case "list": + { + var versions = UpdaterManager.FindInstalledVersions(); + var selfdir = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + if (string.Equals(Util.AppendDirSeparator(selfdir), Util.AppendDirSeparator(UpdaterManager.InstalledBaseDir))) + versions = versions.Union(new KeyValuePair[] { new KeyValuePair(selfdir, UpdaterManager.SelfVersion) }); + Console.WriteLine(string.Join(Environment.NewLine, versions.Select(x => string.Format(" {0} {1} ({2})", (x.Value.Version == UpdaterManager.SelfVersion.Version ? "*" : "-"), x.Value.Displayname, x.Value.Version)))); + return 0; + } + + case "verify": + { + var versions = UpdaterManager.FindInstalledVersions(); + var selfdir = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + if (string.Equals(Util.AppendDirSeparator(selfdir), Util.AppendDirSeparator(UpdaterManager.InstalledBaseDir))) + versions = versions.Union(new KeyValuePair[] { new KeyValuePair(selfdir, UpdaterManager.SelfVersion) }); + + Console.WriteLine(string.Join(Environment.NewLine, versions.Select(x => string.Format(" {0} {1} ({2}): {3}", (x.Value.Version == UpdaterManager.SelfVersion.Version ? "*" : "-"), x.Value.Displayname, x.Value.Version, UpdaterManager.VerifyUnpackedFolder(x.Key, x.Value) ? "Valid" : "*** Modified ***")))); + return 0; + } + case "check": + { + var update = UpdaterManager.CheckForUpdate(); + if (update == null) + Console.WriteLine("No updates found"); + else if (update.Version == UpdaterManager.SelfVersion.Version) + Console.WriteLine("You are running the latest version: {0}", update.Displayname); + else + Console.WriteLine("New version is available: {0}", update.Displayname); + + return 0; + } + case "install": + { + var update = UpdaterManager.CheckForUpdate(); + if (update == null || update.Version == UpdaterManager.SelfVersion.Version) + { + Console.WriteLine("You are running the latest version: {0} ({1})", UpdaterManager.SelfVersion.Displayname, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version); + return 0; + } + Console.WriteLine("Downloading update \"{0}\" ...", update.Displayname); + + long lastpg = 0; + UpdaterManager.DownloadAndUnpackUpdate(update, f => { + var npg = (long)(f*100); + if (Math.Abs(npg - lastpg) >= 5 || (npg == 100 && lastpg != 100)) + { + lastpg = npg; + Console.WriteLine("Downloading {0}% ...", npg); + } + }); + + Console.WriteLine("New version \"{0}\" installed!", update.Displayname); + return 0; + } + default: + Console.WriteLine("Unknown command: \"{0}\"", args[0]); + Console.WriteLine(); + Console.WriteLine("Try the command \"{0}\" instead", "help"); + return 100; + } + } + + private static void WriteUsage() + { + Console.WriteLine("Usage:{0}\t{1}{2} [LIST|VERIFY|CHECK|INSTALL|HELP]", Environment.NewLine, Duplicati.Library.Utility.Utility.IsMono ? "mono " : "", System.IO.Path.GetFileName(System.Reflection.Assembly.GetExecutingAssembly().Location)); + Console.WriteLine(); + Console.WriteLine("Environment variables:"); + Console.WriteLine(); + Console.WriteLine("{0} - Disables updates completely", string.Format(UpdaterManager.SKIPUPDATE_ENVNAME_TEMPLATE, AutoUpdateSettings.AppName)); + Console.WriteLine("{0} - Choose how to handle updates, valid settings: {1}", string.Format(UpdaterManager.UPDATE_STRATEGY_ENVNAME_TEMPLATE, AutoUpdateSettings.AppName), string.Join(", ", Enum.GetNames(typeof(AutoUpdateStrategy)))); + Console.WriteLine("{0} - Use alternate updates urls", string.Format(AutoUpdateSettings.UPDATEURL_ENVNAME_TEMPLATE, AutoUpdateSettings.AppName)); + Console.WriteLine("{0} - Choose different channel than the default {1}, valid settings: {2}", string.Format(AutoUpdateSettings.UPDATECHANNEL_ENVNAME_TEMPLATE, AutoUpdateSettings.AppName), AutoUpdater.AutoUpdateSettings.DefaultUpdateChannel, string.Join(",", Enum.GetNames(typeof(ReleaseType)).Where( x => x != ReleaseType.Unknown.ToString()))); + Console.WriteLine(); + Console.WriteLine("Updates are downloaded from: {0}", string.Join(";", AutoUpdateSettings.URLs)); + Console.WriteLine("Updates are installed in: {0}", UpdaterManager.INSTALLDIR); + Console.WriteLine("The base version is \"{0}\" ({1}) and is installed in: {2}", UpdaterManager.BaseVersion.Displayname, UpdaterManager.BaseVersion.Version, UpdaterManager.InstalledBaseDir); + Console.WriteLine("This version is \"{0}\" ({1}) and is installed in: {2}", UpdaterManager.SelfVersion.Displayname, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version, System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)); + Console.WriteLine(); + } + } +} + diff --git a/Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj b/Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj index 5e0ecff02..f15eb653a 100644 --- a/Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj +++ b/Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj @@ -1,44 +1,45 @@  + net8.0 Exe + A backend debugging tool for Duplicati + Duplicati.CommandLine.BackendTester.Implementation + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Duplicati/CommandLine/BackendTester/Program.cs b/Duplicati/CommandLine/BackendTester/Program.cs index a4cd59bb7..5671d7647 100644 --- a/Duplicati/CommandLine/BackendTester/Program.cs +++ b/Duplicati/CommandLine/BackendTester/Program.cs @@ -29,7 +29,7 @@ using System.Threading; namespace Duplicati.CommandLine.BackendTester { - class Program + public class Program { /// diff --git a/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj b/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj index 6952361de..aa642fd86 100644 --- a/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj +++ b/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj @@ -1,37 +1,30 @@  + net8.0 Exe + Duplicati.CommandLine.BackendTool.Implementation + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + - \ No newline at end of file + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Duplicati/CommandLine/Commands.cs b/Duplicati/CommandLine/CLI/Commands.cs similarity index 96% rename from Duplicati/CommandLine/Commands.cs rename to Duplicati/CommandLine/CLI/Commands.cs index c5e81c8c3..c8f5c78ee 100644 --- a/Duplicati/CommandLine/Commands.cs +++ b/Duplicati/CommandLine/CLI/Commands.cs @@ -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; } } } diff --git a/Duplicati/CommandLine/ConsoleOutput.cs b/Duplicati/CommandLine/CLI/ConsoleOutput.cs similarity index 100% rename from Duplicati/CommandLine/ConsoleOutput.cs rename to Duplicati/CommandLine/CLI/ConsoleOutput.cs diff --git a/Duplicati/CommandLine/CLI/Duplicati.CommandLine.csproj b/Duplicati/CommandLine/CLI/Duplicati.CommandLine.csproj new file mode 100644 index 000000000..66db07b96 --- /dev/null +++ b/Duplicati/CommandLine/CLI/Duplicati.CommandLine.csproj @@ -0,0 +1,59 @@ + + + + net8.0 + A commandline version of Duplicati + Duplicati.CommandLine.Implementation + TrayWarning.ico + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Duplicati/CommandLine/Help.cs b/Duplicati/CommandLine/CLI/Help.cs similarity index 100% rename from Duplicati/CommandLine/Help.cs rename to Duplicati/CommandLine/CLI/Help.cs diff --git a/Duplicati/CommandLine/Program.cs b/Duplicati/CommandLine/CLI/Program.cs similarity index 97% rename from Duplicati/CommandLine/Program.cs rename to Duplicati/CommandLine/CLI/Program.cs index 7e8559d14..57c82a57a 100644 --- a/Duplicati/CommandLine/Program.cs +++ b/Duplicati/CommandLine/CLI/Program.cs @@ -38,7 +38,6 @@ namespace Duplicati.CommandLine /// /// The main entry point for the application. /// - [STAThread] public static int Main(string[] args) { Duplicati.Library.AutoUpdater.UpdaterManager.IgnoreWebrootFolder = true; diff --git a/Duplicati/CommandLine/Strings.cs b/Duplicati/CommandLine/CLI/Strings.cs similarity index 100% rename from Duplicati/CommandLine/Strings.cs rename to Duplicati/CommandLine/CLI/Strings.cs diff --git a/Duplicati/CommandLine/TrayWarning.ico b/Duplicati/CommandLine/CLI/TrayWarning.ico similarity index 100% rename from Duplicati/CommandLine/TrayWarning.ico rename to Duplicati/CommandLine/CLI/TrayWarning.ico diff --git a/Duplicati/CommandLine/help.txt b/Duplicati/CommandLine/CLI/help.txt similarity index 100% rename from Duplicati/CommandLine/help.txt rename to Duplicati/CommandLine/CLI/help.txt diff --git a/Duplicati/CommandLine/Checkout DSMCBE Data.sh b/Duplicati/CommandLine/Checkout DSMCBE Data.sh old mode 100644 new mode 100755 diff --git a/Duplicati/CommandLine/ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj b/Duplicati/CommandLine/ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj index cc647ddbf..4a26154f8 100644 --- a/Duplicati/CommandLine/ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj +++ b/Duplicati/CommandLine/ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj @@ -1,10 +1,21 @@  + net8.0 Exe + Duplicati.CommandLine.ConfigurationImporter.Implementation + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/CommandLine/ConfigurationImporter/Program.cs b/Duplicati/CommandLine/ConfigurationImporter/Program.cs index 02185446f..eed399d9d 100644 --- a/Duplicati/CommandLine/ConfigurationImporter/Program.cs +++ b/Duplicati/CommandLine/ConfigurationImporter/Program.cs @@ -31,7 +31,7 @@ namespace Duplicati.CommandLine.ConfigurationImporter { private static readonly string usageString = $"Usage: {nameof(ConfigurationImporter)}.exe --import-metadata=(true | false) --server-datafolder="; - public static void Main(string[] args) + public static int Main(string[] args) { if (args.Length != 3) { @@ -58,6 +58,8 @@ namespace Duplicati.CommandLine.ConfigurationImporter ImportExportStructure importedStructure = Backups.ImportBackup(configurationFile, importMetadata, () => ConfigurationImporter.ReadPassword($"Password for {configurationFile}: "), advancedOptions); Console.WriteLine($"Imported \"{importedStructure.Backup.Name}\" with ID {importedStructure.Backup.ID} and local database at {importedStructure.Backup.DBPath}."); + + return 0; } private static string ReadPassword(string prompt) diff --git a/Duplicati/CommandLine/Duplicati.CommandLine.csproj b/Duplicati/CommandLine/Duplicati.CommandLine.csproj deleted file mode 100644 index 9f0705abf..000000000 --- a/Duplicati/CommandLine/Duplicati.CommandLine.csproj +++ /dev/null @@ -1,75 +0,0 @@ - - - net8.0 - Exe - TrayWarning.ico - - - app.manifest - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Designer - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj b/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj index f008e8dfd..5d3fc9af7 100644 --- a/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj +++ b/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj @@ -1,47 +1,38 @@  + net8.0 - Exe + RecoveryTool for Duplicati + Duplicati.CommandLine.RecoveryTool.Implementation + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - \ No newline at end of file + + diff --git a/Duplicati/CommandLine/RecoveryTool/Properties/launchSettings.json b/Duplicati/CommandLine/RecoveryTool/Properties/launchSettings.json new file mode 100644 index 000000000..a33873917 --- /dev/null +++ b/Duplicati/CommandLine/RecoveryTool/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Duplicati.CommandLine.RecoveryTool": { + "commandName": "Project", + "environmentVariables": { + "AUTOUPDATER_Duplicati_SKIP_UPDATE": "1" + } + } + } +} \ No newline at end of file diff --git a/Duplicati/CommandLine/RecoveryTool/Recompress.cs b/Duplicati/CommandLine/RecoveryTool/Recompress.cs index c8ca47826..3af041237 100644 --- a/Duplicati/CommandLine/RecoveryTool/Recompress.cs +++ b/Duplicati/CommandLine/RecoveryTool/Recompress.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -29,6 +29,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; +using System.Security.Cryptography; namespace Duplicati.CommandLine.RecoveryTool { @@ -228,8 +229,7 @@ namespace Duplicati.CommandLine.RecoveryTool textJSON = sourceStreamReader.ReadToEnd(); JToken token = JObject.Parse(textJSON); var fileInfoBlocks = new FileInfo(Path.Combine(targetfolder, cmfileNew.Replace("vol/", ""))); - var filehasher = HashAlgorithmHelper.Create(m_Options.FileHashAlgorithm); - + using (var filehasher = HashFactory.CreateHasher(m_Options.FileHashAlgorithm)) using (var fileStream = fileInfoBlocks.Open(FileMode.Open)) { fileStream.Position = 0; diff --git a/Duplicati/CommandLine/RecoveryTool/Restore.cs b/Duplicati/CommandLine/RecoveryTool/Restore.cs index 20a5f764a..1cbf9bd4b 100644 --- a/Duplicati/CommandLine/RecoveryTool/Restore.cs +++ b/Duplicati/CommandLine/RecoveryTool/Restore.cs @@ -26,6 +26,9 @@ using System.Collections.Generic; using Duplicati.Library.Common.IO; using Duplicati.Library.Common; +using System.Security.Cryptography; +using Duplicati.Library.Utility; + namespace Duplicati.CommandLine.RecoveryTool { public static class Restore @@ -96,18 +99,19 @@ namespace Duplicati.CommandLine.RecoveryTool return 100; } - var blockhasher = string.IsNullOrWhiteSpace(blockhash_str) ? null : Library.Utility.HashAlgorithmHelper.Create(blockhash_str); - var filehasher = string.IsNullOrWhiteSpace(filehash_str) ? null : Library.Utility.HashAlgorithmHelper.Create(filehash_str); - - if (blockhasher == null) + + if (string.IsNullOrWhiteSpace(blockhash_str)) throw new Duplicati.Library.Interface.UserInformationException(string.Format("Block hash algorithm not valid: {0}", blockhash_str), "BlockHashAlgorithmNotSupported"); - if (filehasher == null) + if (string.IsNullOrWhiteSpace(filehash_str)) throw new Duplicati.Library.Interface.UserInformationException(string.Format("File hash algorithm not valid: {0}", filehash_str), "FileHashAlgorithmNotSupported"); - var hashesprblock = blocksize / (blockhasher.HashSize / 8); + using (var blockhasher = HashFactory.CreateHasher(blockhash_str)) + using (var filehasher = HashFactory.CreateHasher(filehash_str)) using (var mru = new CompressedFileMRUCache(options)) { + var hashesprblock = blocksize / (blockhasher.HashSize / 8); + Console.WriteLine("Building lookup table for file hashes"); using (HashLookupHelper lookup = new HashLookupHelper(ixfile, mru, (int) blocksize, blockhasher.HashSize / 8)) { diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/AppIndicatorRunner.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/AppIndicatorRunner.cs deleted file mode 100644 index bfe1a33f8..000000000 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/AppIndicatorRunner.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -#if __WindowsGTK__ || ENABLE_GTK -using System; -using AppIndicator; -using Gtk; -using System.Collections.Generic; - -using Duplicati.Library.Common.IO; -using Duplicati.Library.Utility; -using Duplicati.Server.Serialization; - - -namespace Duplicati.GUI.TrayIcon -{ - public class AppIndicatorRunner : GtkRunner - { - protected ApplicationIndicator m_appIndicator; - private string m_themeFolder; - - protected override void CreateTrayInstance() - { - m_themeFolder = SystemIO.IO_OS.PathCombine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "SVGIcons"); - m_themeFolder = SystemIO.IO_OS.PathCombine(m_themeFolder, "dark"); - - m_appIndicator = new ApplicationIndicator("duplicati", "normal", Category.ApplicationStatus, m_themeFolder); - } - - protected override void Run(string[] args) - { - m_appIndicator.Menu.ShowAll(); - m_appIndicator.Status = Status.Active; - Gtk.Application.Run(); - } - - protected override void SetMenu (IEnumerable items) - { - base.SetMenu(items); - m_appIndicator.Menu = m_popupMenu; - } - - protected override void SetIcon(TrayIcons icon) - { - m_appIndicator.IconName = GetTrayIconFilename(icon); - - switch (icon) - { - case TrayIcons.Paused: - m_appIndicator.IconDesc = "Paused"; - break; - case TrayIcons.Running: - m_appIndicator.IconDesc = "Running"; - break; - case TrayIcons.IdleWarning: - m_appIndicator.IconDesc = "Warning"; - break; - case TrayIcons.IdleError: - m_appIndicator.IconDesc = "Error"; - break; - case TrayIcons.RunningError: - m_appIndicator.IconDesc = "Running"; - break; - case TrayIcons.PausedError: - m_appIndicator.IconDesc = "Paused"; - break; - case TrayIcons.Idle: - default: - m_appIndicator.IconDesc = "Ready"; - break; - } - } - } -} -#endif diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/OSX Icons/normal-error.png b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/normal-error.png similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/OSX Icons/normal-error.png rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/normal-error.png diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/OSX Icons/normal-pause.png b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/normal-pause.png similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/OSX Icons/normal-pause.png rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/normal-pause.png diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/OSX Icons/normal-running.png b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/normal-running.png similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/OSX Icons/normal-running.png rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/normal-running.png diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/OSX Icons/normal-warning.png b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/normal-warning.png similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/OSX Icons/normal-warning.png rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/normal-warning.png diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/OSX Icons/normal.png b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/normal.png similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/OSX Icons/normal.png rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/Assets/icons/normal.png diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/AvaloniaRunner.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/AvaloniaRunner.cs new file mode 100644 index 000000000..c8a40f692 --- /dev/null +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/AvaloniaRunner.cs @@ -0,0 +1,295 @@ +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml.Styling; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Themes.Fluent; +using Duplicati.Server.Serialization.Interface; + +namespace Duplicati.GUI.TrayIcon +{ + public class AvaloniaRunner : TrayIconBase + { + private AvaloniaApp application; + private IEnumerable menuItems = Enumerable.Empty(); + + public override void Init(string[] args) + { + //Init + + //Initialize and Run is called next + base.Init(args); + } + + + + protected override void UpdateUIState(Action action) + { + action.Invoke(); + } + + protected override void RegisterStatusUpdateCallback() + { + Program.Connection.OnStatusUpdated += delegate (IServerStatus status) + { + this.OnStatusUpdated(status); + }; + } + + #region implemented abstract members of Duplicati.GUI.TrayIcon.TrayIconBase + protected override void Run(string[] args) + { + var lifetime = new ClassicDesktopStyleApplicationLifetime() + { + Args = args, + ShutdownMode = ShutdownMode.OnExplicitShutdown + }; + + var builder = Avalonia.Controls.AppBuilderBase.Configure() + .UsePlatformDetect() + .LogToTrace().SetupWithLifetime(lifetime); + + application = builder.Instance as AvaloniaApp; + application.SetMenu(menuItems); + + lifetime.Start(args); + } + + protected override IMenuItem CreateMenuItem(string text, MenuIcons icon, Action callback, IList subitems) + { + return new AvaloniaMenuItem(text, icon, callback, subitems); + } + + protected override void NotifyUser(string title, string message, NotificationType type) + { + //var icon = Win32NativeNotifyIcon.InfoFlags.NIIF_INFO; + + switch (type) + { + case NotificationType.Information: + //icon = Win32NativeNotifyIcon.InfoFlags.NIIF_INFO; + break; + case NotificationType.Warning: + //icon = Win32NativeNotifyIcon.InfoFlags.NIIF_WARNING; + break; + case NotificationType.Error: + //icon = Win32NativeNotifyIcon.InfoFlags.NIIF_ERROR; + break; + } + + //m_ntfIcon.ShowBalloonTip(title, message, icon); + } + + protected override void Exit() + { + this.application?.Shutdown(); + } + + protected override void SetIcon(TrayIcons icon) + { + this.application?.SetIcon(icon); + } + + protected override void SetMenu(IEnumerable items) + { + this.menuItems = items.Select(i => (AvaloniaMenuItem)i); + this.application?.SetMenu(menuItems); + } + + public override void Dispose() + { + } + #endregion + } + + public class AvaloniaMenuItem : IMenuItem + { + + public string Text { get; private set; } + public Action Callback { get; private set; } + public IList SubItems { get; private set; } + public bool Enabled { get; private set; } + + public AvaloniaMenuItem(string text, MenuIcons icon, Action callback, IList subitems) + { + if (subitems != null && subitems.Count > 0) + throw new NotImplementedException("So far not needed."); + + this.Text = text; + this.Callback = callback; + this.SubItems = subitems; + this.Enabled = true; + SetIcon(icon); + } + + #region IMenuItem implementation + public void SetText(string text) + { + this.Text = text; + } + + public void SetIcon(MenuIcons icon) + { + switch (icon) + { + case MenuIcons.Pause: + //this.Icon = Win32IconLoader.MenuPauseIcon; + break; + case MenuIcons.Quit: + //this.Icon = Win32IconLoader.MenuQuitIcon; + break; + case MenuIcons.Resume: + //this.Icon = Win32IconLoader.MenuPlayIcon; + break; + case MenuIcons.Status: + //this.Icon = Win32IconLoader.MenuOpenIcon; + break; + case MenuIcons.None: + default: + //this.Icon = new SafeIconHandle(IntPtr.Zero); + break; + } + } + + public void SetEnabled(bool isEnabled) + { + this.Enabled = isEnabled; + } + + public void SetDefault(bool value) + { + //TODO-DNC Cosmetic, not needed + } + #endregion + + public NativeMenuItem GetNativeItem() + { + var item = new NativeMenuItem(Text); + item.IsEnabled = Enabled; + item.Click += (_sender, _args) => + { + Callback(); + }; + return item; + } + } + + public class AvaloniaApp : Application + { + private Avalonia.Controls.TrayIcon trayIcon; + + public override void Initialize() + { + //AvaloniaXamlLoader.Load(this); + } + + public void SetIcon(TrayIcons icon) + { + //There are calls before the icon is created + if (this.trayIcon == null) + return; + + switch (icon) + { + case TrayIcons.IdleError: + this.trayIcon.Icon = LoadIcon("normal-error.png"); + break; + case TrayIcons.Paused: + case TrayIcons.PausedError: + this.trayIcon.Icon = LoadIcon("normal-pause.png"); + break; + case TrayIcons.Running: + case TrayIcons.RunningError: + this.trayIcon.Icon = LoadIcon("normal-running.png"); + break; + case TrayIcons.Idle: + default: + this.trayIcon.Icon = LoadIcon("normal.png"); + break; + } + } + + public void SetMenu(IEnumerable menuItems) + { + // Reuse the menu on Mac + var menu = trayIcon.Menu ?? new NativeMenu(); + menu.Items.Clear(); + foreach (var item in menuItems) + { + menu.Add(item.GetNativeItem()); + } + trayIcon.Menu = menu; + } + + public void Shutdown() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.Shutdown(); + } + else + { + throw new Exception("Unsupported Lifetime"); + } + } + + private WindowIcon LoadIcon(string iconName) + { + var assets = AvaloniaLocator.Current.GetService(); + var bitmap = new Bitmap(assets.Open(new Uri($"avares://{Assembly.GetExecutingAssembly().FullName}/Assets/icons/" + iconName))); + return new WindowIcon(bitmap); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var light = new FluentTheme(new Uri($"avares://{Assembly.GetExecutingAssembly().GetName()}")) { Mode = FluentThemeMode.Light }; + Styles.Add(light); + + var icon = LoadIcon("normal.png"); + var trayIcon = new Avalonia.Controls.TrayIcon(); + this.trayIcon = trayIcon; + trayIcon.Icon = icon; + trayIcon.ToolTipText = "Test"; + //The menu already exists on Mac but is nullable... + var menu = trayIcon.Menu ?? new NativeMenu(); + var item = new NativeMenuItem("Quit"); + item.Click += (_sender, _args) => + { + desktop.Shutdown(); + }; + menu.Add(item); + trayIcon.Menu = menu; + + // Register tray icons to be removed on application shutdown + var icons = new Avalonia.Controls.TrayIcons(); + icons.Add(trayIcon); + Avalonia.Controls.TrayIcon.SetIcons(Application.Current, icons); + } + + base.OnFrameworkInitializationCompleted(); + } + } +} diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/CocoaRunner.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/CocoaRunner.cs index 422a3f0dd..0c0ecbfb5 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/CocoaRunner.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/CocoaRunner.cs @@ -81,7 +81,7 @@ namespace Duplicati.GUI.TrayIcon } private static readonly System.Reflection.Assembly ASSEMBLY = System.Reflection.Assembly.GetExecutingAssembly(); - private static readonly string ICON_PATH = ASSEMBLY.GetName().Name + ".OSX_Icons."; + private static readonly string ICON_PATH = ASSEMBLY.GetName().Name + ".TrayResources.OSXIcons."; private static readonly string ICON_NORMAL = ICON_PATH + "normal.png"; private static readonly string ICON_PAUSED = ICON_PATH + "normal-pause.png"; diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj index a0b5e06e0..a3734e3d5 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj @@ -1,256 +1,117 @@  + - net8.0-windows - WinExe - false - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - true + net8.0 + The Duplicati Tray implementation + Duplicati.GUI.TrayIcon.Implementation + Duplicati.GUI.TrayIcon Duplicati.ico - true - true - true - - - bin\Debug - true - true - true - - - - - - - - bin\Release - false - none - true - true - true - - - XAMARIN_MAC - - - Duplicati.GUI.TrayIcon.Program - - - app.manifest + + copyused + true + - - ..\..\..\thirdparty\appindicator-sharp\gdk-sharp.dll - gtk-sharp-2.0 - - - ..\..\..\thirdparty\appindicator-sharp\gtk-sharp.dll - gtk-sharp-2.0 - - - glib-sharp-2.0 - - - gtk-sharp-2.0 - - - ..\..\..\thirdparty\MonoMac\MonoMac.dll - MonoMac - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - - - libxammac.dylib - PreserveNewest - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - False - Microsoft .NET Framework 4 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 Client Profile - false - - - False - .NET Framework 3.5 SP1 - false - - - False - Windows Installer 3.1 - true - - - - + PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + PreserveNewest OSXTrayHost\rumps.py PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - + - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + ..\..\..\thirdparty\MonoMac\MonoMac.dll + - + CocoaRunner.cs - - - copy /Y '%USERPROFILE%\.nuget\packages\alphavss\1.4.0\build\net45\AlphaVSS.*.dll' '$(TargetDir)\alphavss\' -copy /Y '%USERPROFILE%\.nuget\packages\alphavss\1.4.0\lib\net45\AlphaVSS.*.dll' '$(TargetDir)\alphavss\' - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/GtkRunner.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/GtkRunner.cs index e615ddde5..741d5b177 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/GtkRunner.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/GtkRunner.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -37,7 +37,7 @@ namespace Duplicati.GUI.TrayIcon /// static GtkRunner() { - m_svgfolder = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "SVGIcons"); + m_svgfolder = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "TrayResources", "SVGIcons"); m_svgfolder = System.IO.Path.Combine(m_svgfolder, "dark"); Gtk.Application.Init(); @@ -348,8 +348,11 @@ namespace Duplicati.GUI.TrayIcon break; } + /* + FIXME: Someone would need to port notify-sharp to use Tmds.DBus var notification = new Notifications.Notification(title, message, icon); notification.Show(); + */ } protected void StatusUpdateEvent(object sender, EventArgs a) diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HostedInstanceKeeper.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HostedInstanceKeeper.cs index 93d3cf19a..8097d56fb 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HostedInstanceKeeper.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HostedInstanceKeeper.cs @@ -84,7 +84,7 @@ namespace Duplicati.GUI.TrayIcon Duplicati.Server.Program.ApplicationExitEvent.Set(); if (!m_runner.Join(TimeSpan.FromSeconds(10))) { - m_runner.Abort(); + m_runner.Interrupt(); m_runner.Join(TimeSpan.FromSeconds(10)); } } diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs index eb4bf7894..28dd95d6b 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/HttpServerConnection.cs @@ -232,11 +232,11 @@ namespace Duplicati.GUI.TrayIcon { m_shutdown = true; m_waitLock.Set(); - m_pollThread.Abort(); + m_pollThread.Interrupt(); m_pollThread.Join(TimeSpan.FromSeconds(10)); if (!m_requestThread.Join(TimeSpan.FromSeconds(10))) { - m_requestThread.Abort(); + m_requestThread.Interrupt(); m_requestThread.Join(TimeSpan.FromSeconds(10)); } } diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/ImageLoader.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/ImageLoader.cs index 224c23078..1b8accb5d 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/ImageLoader.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/ImageLoader.cs @@ -80,16 +80,17 @@ namespace Duplicati.GUI.TrayIcon return ICONS[cachename]; } - public const string NormalIcon = "Resources.TrayNormal.ico"; - public const string WarningIcon = "Resources.TrayNormalWarning.ico"; - public const string ErrorIcon = "Resources.TrayNormalError.ico"; - public const string PauseIcon = "Resources.TrayNormalPause.ico"; - public const string WorkingIcon = "Resources.TrayWorking.ico"; - public static Bitmap Pause { get { return LoadImage("Resources.context_menu_pause.ico"); } } - public static Bitmap Play { get { return LoadImage("Resources.context_menu_play.ico"); } } - public static Bitmap CloseMenuIcon { get { return LoadImage("Resources.context_menu_quit.ico"); } } - public static Bitmap StatusMenuIcon { get { return LoadImage("Resources.context_menu_open.ico"); } } + public const string WarningIcon = "TrayResources.WinIcons.TrayNormalWarning.ico"; + public const string NormalIcon = "TrayResources.WinIcons.TrayNormal.ico"; + public const string ErrorIcon = "TrayResources.WinIcons.TrayNormalError.ico"; + public const string PauseIcon = "TrayResources.WinIcons.TrayNormalPause.ico"; + public const string WorkingIcon = "TrayResources.WinIcons.TrayWorking.ico"; + + public static Bitmap Pause { get { return LoadImage("TrayResources.WinIcons.context_menu_pause.ico"); } } + public static Bitmap Play { get { return LoadImage("TrayResources.WinIcons.context_menu_play.ico"); } } + public static Bitmap CloseMenuIcon { get { return LoadImage("TrayResources.WinIcons.context_menu_quit.ico"); } } + public static Bitmap StatusMenuIcon { get { return LoadImage("TrayResources.WinIcons.context_menu_open.ico"); } } } } diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Info.plist b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Info.plist deleted file mode 100644 index 6f1f9676b..000000000 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Info.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - LSApplicationCategoryType - public.app-category.productivity - CFBundleDevelopmentRegion - English - CFBundleIdentifier - com.duplicati.Duplicati - CFBundleDisplayName - Duplicati - CFBundleName - Duplicati - CFBundleExecutable - Duplicati - CFBundleIconFile - Duplicati.icns - CFBundleInfoDictionaryVersion - 6.0 - CFBundleShortVersionString - 1.3 - CFBundleVersion - 1.3.0.973 - CFBundleSignature - hand - NSAppleScriptEnabled - NO - - diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs index e078e6b0b..b980f8c56 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs @@ -40,9 +40,9 @@ namespace Duplicati.GUI.TrayIcon private const string TOOLKIT_OPTION = "toolkit"; private const string TOOLKIT_WINDOWS_FORMS = "winforms"; private const string TOOLKIT_GTK = "gtk"; - private const string TOOLKIT_GTK_APP_INDICATOR = "gtk-appindicator"; private const string TOOLKIT_COCOA = "cocoa"; private const string TOOLKIT_RUMPS = "rumps"; + private const string TOOLKIT_AVALONIA = "avalonia"; private const string HOSTURL_OPTION = "hosturl"; private const string NOHOSTEDSERVER_OPTION = "no-hosted-server"; @@ -64,15 +64,16 @@ namespace Duplicati.GUI.TrayIcon private static string GetDefaultToolKit() { +#if !__LEGACY_TOOLKITS__ + return TOOLKIT_AVALONIA; +#endif + // No longer using Cocoa directly as it fails on 32bit as well if (Platform.IsClientOSX) return TOOLKIT_RUMPS; #if __WindowsGTK__ || ENABLE_GTK if (Platform.IsClientPosix) { - if (SupportsAppIndicator) - return TOOLKIT_GTK_APP_INDICATOR; - else return TOOLKIT_GTK; } else @@ -148,8 +149,6 @@ namespace Duplicati.GUI.TrayIcon #if __WindowsGTK__ || ENABLE_GTK else if (TOOLKIT_GTK.Equals(toolkit, StringComparison.OrdinalIgnoreCase)) toolkit = TOOLKIT_GTK; - else if (TOOLKIT_GTK_APP_INDICATOR.Equals(toolkit, StringComparison.OrdinalIgnoreCase)) - toolkit = TOOLKIT_GTK_APP_INDICATOR; #endif else if (TOOLKIT_COCOA.Equals(toolkit, StringComparison.OrdinalIgnoreCase)) toolkit = TOOLKIT_COCOA; @@ -323,19 +322,22 @@ namespace Duplicati.GUI.TrayIcon private static TrayIconBase RunTrayIcon(string toolkit) { + //https://medium.com/@k.l.mueller/cross-platform-tray-bar-applications-with-xamarin-forms-bbd6c1b7f17a + //https://docs.microsoft.com/en-us/xamarin/xamarin-forms/platform/other/gtk?tabs=windows + if (toolkit == TOOLKIT_WINDOWS_FORMS) return GetWinformsInstance(); #if __WindowsGTK__ || ENABLE_GTK else if (toolkit == TOOLKIT_GTK) return GetGtkInstance(); - else if (toolkit == TOOLKIT_GTK_APP_INDICATOR) - return GetAppIndicatorInstance(); #endif else if (toolkit == TOOLKIT_COCOA) return GetCocoaRunnerInstance(); else if (toolkit == TOOLKIT_RUMPS) return GetRumpsRunnerInstance(); - else + else if (toolkit == TOOLKIT_AVALONIA) + return GetAvaloniaRunnerInstance(); + else throw new UserInformationException(string.Format("The selected toolkit '{0}' is invalid", toolkit), "TrayIconInvalidToolKit"); } @@ -347,24 +349,23 @@ namespace Duplicati.GUI.TrayIcon // attempt to load non-existing assemblies [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - private static TrayIconBase GetWinformsInstance() { return new Windows.WinFormsRunner(); } -#if __WindowsGTK__ || ENABLE_GTK + private static TrayIconBase GetWinformsInstance() { return new Win32Runner(); } +#if __MonoCS__ || __WindowsGTK__ || ENABLE_GTK [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] private static TrayIconBase GetGtkInstance() { return new GtkRunner(); } - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - private static TrayIconBase GetAppIndicatorInstance() { return new AppIndicatorRunner(); } #endif [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - private static TrayIconBase GetCocoaRunnerInstance() - { -#if XAMARIN_MAC_ENABLE + private static TrayIconBase GetCocoaRunnerInstance() { +#if XAMARIN_MAC_ENABLE && __LEGACY_TOOLKITS__ return new CocoaRunner(); #else throw new UserInformationException("Xamarin.Mac framework not found", "TrayIconMissingXamarinMac"); #endif - } + } + + private static TrayIconBase GetRumpsRunnerInstance() { return new RumpsRunner(); } + private static TrayIconBase GetAvaloniaRunnerInstance() { return new AvaloniaRunner(); } - private static TrayIconBase GetRumpsRunnerInstance() { return new RumpsRunner(); } //The functions below simply load the requested type, // and if the type is not present, calling the function will result in an exception. @@ -376,7 +377,7 @@ namespace Duplicati.GUI.TrayIcon private static bool TryGetGtk() { #if __WindowsGTK__ || ENABLE_GTK - return typeof(Gtk.StatusIcon) != null && typeof(Gdk.Image) != null; + return typeof(Gtk.StatusIcon) != null; #else return false; #endif @@ -385,17 +386,7 @@ namespace Duplicati.GUI.TrayIcon [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] private static bool TryGetWinforms() { - return typeof(System.Windows.Forms.NotifyIcon) != null; - } - - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - private static bool TryGetAppIndicator() - { -#if __WindowsGTK__ || ENABLE_GTK - return typeof(AppIndicator.ApplicationIndicator) != null; -#else - return false; -#endif + return Platform.IsClientWindows; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] @@ -422,17 +413,6 @@ namespace Duplicati.GUI.TrayIcon } } - private static bool SupportsAppIndicator - { - get - { - try { return TryGetAppIndicator(); } - catch {} - - return false; - } - } - private static bool SupportsCocoa { get @@ -476,8 +456,6 @@ namespace Duplicati.GUI.TrayIcon toolkits.Add(TOOLKIT_WINDOWS_FORMS); if (SupportsGtk) toolkits.Add(TOOLKIT_GTK); - if (SupportsAppIndicator) - toolkits.Add(TOOLKIT_GTK_APP_INDICATOR); if (SupportsCocoa) toolkits.Add(TOOLKIT_COCOA); if (SupportsRumps) diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Properties/Settings.Designer.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Properties/Settings.Designer.cs deleted file mode 100644 index 5183921aa..000000000 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Properties/Settings.Designer.cs +++ /dev/null @@ -1,26 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Duplicati.GUI.TrayIcon.Properties { - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.1.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default { - get { - return defaultInstance; - } - } - } -} diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Properties/Settings.settings b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Properties/Settings.settings deleted file mode 100644 index abf36c5d3..000000000 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Properties/Settings.settings +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/RumpsRunner.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/RumpsRunner.cs index 525a573f7..94206db09 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/RumpsRunner.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/RumpsRunner.cs @@ -92,7 +92,7 @@ namespace Duplicati.GUI.TrayIcon } private static readonly System.Reflection.Assembly ASSEMBLY = System.Reflection.Assembly.GetExecutingAssembly(); - private static readonly string ICON_PATH = ASSEMBLY.GetName().Name + ".OSX_Icons."; + private static readonly string ICON_PATH = ASSEMBLY.GetName().Name + ".TrayResources.OSXIcons."; private static readonly string ICON_NORMAL = ICON_PATH + "normal.png"; private static readonly string ICON_PAUSED = ICON_PATH + "normal-pause.png"; diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/OSXIcons/normal-error.png b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/OSXIcons/normal-error.png new file mode 100644 index 000000000..6662de009 Binary files /dev/null and b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/OSXIcons/normal-error.png differ diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/OSXIcons/normal-pause.png b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/OSXIcons/normal-pause.png new file mode 100644 index 000000000..bd63b2404 Binary files /dev/null and b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/OSXIcons/normal-pause.png differ diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/OSXIcons/normal-running.png b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/OSXIcons/normal-running.png new file mode 100644 index 000000000..26802de48 Binary files /dev/null and b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/OSXIcons/normal-running.png differ diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/OSXIcons/normal.png b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/OSXIcons/normal.png new file mode 100644 index 000000000..7dc583635 Binary files /dev/null and b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/OSXIcons/normal.png differ diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/context_menu_open.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/context_menu_open.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/context_menu_open.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/context_menu_open.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/context_menu_pause.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/context_menu_pause.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/context_menu_pause.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/context_menu_pause.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/context_menu_quit.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/context_menu_quit.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/context_menu_quit.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/context_menu_quit.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/context_menu_resume.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/context_menu_resume.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/context_menu_resume.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/context_menu_resume.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/normal-error.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/normal-error.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/normal-error.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/normal-error.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/normal-pause.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/normal-pause.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/normal-pause.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/normal-pause.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/normal-running.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/normal-running.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/normal-running.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/normal-running.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/normal.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/normal.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/normal.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/dark/normal.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/context_menu_open.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/context_menu_open.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/context_menu_open.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/context_menu_open.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/context_menu_pause.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/context_menu_pause.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/context_menu_pause.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/context_menu_pause.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/context_menu_quit.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/context_menu_quit.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/context_menu_quit.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/context_menu_quit.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/context_menu_resume.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/context_menu_resume.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/context_menu_resume.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/context_menu_resume.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/normal-error.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/normal-error.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/normal-error.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/normal-error.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/normal-pause.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/normal-pause.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/normal-pause.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/normal-pause.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/normal-running.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/normal-running.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/normal-running.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/normal-running.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/normal.svg b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/normal.svg similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/normal.svg rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/SVGIcons/light/normal.svg diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/TrayNormal.ico b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/TrayNormal.ico similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/TrayNormal.ico rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/TrayNormal.ico diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/TrayNormalError.ico b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/TrayNormalError.ico similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/TrayNormalError.ico rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/TrayNormalError.ico diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/TrayNormalPause.ico b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/TrayNormalPause.ico similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/TrayNormalPause.ico rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/TrayNormalPause.ico diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/TrayNormalWarning.ico b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/TrayNormalWarning.ico similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/TrayNormalWarning.ico rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/TrayNormalWarning.ico diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/TrayWorking.ico b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/TrayWorking.ico similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/TrayWorking.ico rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/TrayWorking.ico diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/context_menu_open.ico b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/context_menu_open.ico similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/context_menu_open.ico rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/context_menu_open.ico diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/context_menu_pause.ico b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/context_menu_pause.ico similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/context_menu_pause.ico rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/context_menu_pause.ico diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/context_menu_play.ico b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/context_menu_play.ico similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/context_menu_play.ico rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/context_menu_play.ico diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/context_menu_quit.ico b/Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/context_menu_quit.ico similarity index 100% rename from Duplicati/GUI/Duplicati.GUI.TrayIcon/Resources/context_menu_quit.ico rename to Duplicati/GUI/Duplicati.GUI.TrayIcon/TrayResources/WinIcons/context_menu_quit.ico diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32IconLoader.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32IconLoader.cs new file mode 100644 index 000000000..afd7b1322 --- /dev/null +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32IconLoader.cs @@ -0,0 +1,95 @@ +// 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.Drawing; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; +using System.Runtime.ConstrainedExecution; +using System.IO; + +namespace Duplicati.GUI.TrayIcon +{ + public class SafeIconHandle : SafeHandleZeroOrMinusOneIsInvalid + { + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + private static extern bool DestroyIcon(IntPtr hIcon); + + public SafeIconHandle(IntPtr hIcon) : base(true) + { + SetHandle(hIcon); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + protected override bool ReleaseHandle() + { + return DestroyIcon(handle); + } + } + public static class Win32IconLoader + { + private const uint IMAGE_ICON = 1; + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern IntPtr LoadImage(IntPtr hInstance, string lpszName, uint uType, int cxDesired, int cyDesired, LR fuLoad); + + [Flags] + private enum LR : uint + { + LR_DEFAULTCOLOR = 0x00000000, + LR_MONOCHROME = 0x00000001, + LR_COLOR = 0x00000002, + LR_COPYRETURNORG = 0x00000004, + LR_COPYDELETEORG = 0x00000008, + LR_LOADFROMFILE = 0x00000010, + LR_LOADTRANSPARENT = 0x00000020, + LR_DEFAULTSIZE = 0x00000040, + LR_VGACOLOR = 0x00000080, + LR_LOADMAP3DCOLORS = 0x00001000, + LR_CREATEDIBSECTION = 0x00002000, + LR_COPYFROMRESOURCE = 0x00004000, + LR_SHARED = 0x00008000 + } + + private static readonly Dictionary ICONS = new Dictionary(); + private static readonly object LOCK = new object(); + private static readonly string m_icofolder = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), @"TrayResources\WinIcons"); + + private static SafeIconHandle LoadIcon(string filename) + { + lock (LOCK) + { + if (ICONS.TryGetValue(filename, out SafeIconHandle ico)) + return ico; + else + return new SafeIconHandle(LoadImage(IntPtr.Zero, Path.Combine(m_icofolder, filename), IMAGE_ICON, 16, 16, LR.LR_LOADFROMFILE)); + } + } + + public static SafeIconHandle TrayNormalIcon { get { return LoadIcon("TrayNormal.ico"); } } + public static SafeIconHandle TrayErrorIcon { get { return LoadIcon("TrayNormalError.ico"); } } + public static SafeIconHandle TrayPauseIcon { get { return LoadIcon("TrayNormalPause.ico"); } } + public static SafeIconHandle TrayWorkingIcon { get { return LoadIcon("TrayWorking.ico"); } } + public static SafeIconHandle MenuPauseIcon { get { return LoadIcon("context_menu_pause.ico"); } } + public static SafeIconHandle MenuPlayIcon { get { return LoadIcon("context_menu_play.ico"); } } + public static SafeIconHandle MenuQuitIcon { get { return LoadIcon("context_menu_quit.ico"); } } + public static SafeIconHandle MenuOpenIcon { get { return LoadIcon("context_menu_open.ico"); } } + } +} \ No newline at end of file diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32Menu.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32Menu.cs new file mode 100644 index 000000000..41ad1b48f --- /dev/null +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32Menu.cs @@ -0,0 +1,329 @@ +// 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.Drawing; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; +using System.Runtime.ConstrainedExecution; +using System.IO; + +namespace Duplicati.GUI.TrayIcon +{ + public class SafeObjectHandle : SafeHandleZeroOrMinusOneIsInvalid + { + [DllImport("gdi32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + private static extern bool DeleteObject(IntPtr hObject); + public SafeObjectHandle(IntPtr hObject) : base(true) + { + SetHandle(hObject); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + protected override bool ReleaseHandle() + { + return DeleteObject(handle); + } + } + + public class SafeMenuHandle : SafeHandleZeroOrMinusOneIsInvalid + { + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + private static extern bool DestroyMenu(IntPtr hMenu); + public SafeMenuHandle(IntPtr hMenu) : base(true) + { + SetHandle(hMenu); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + protected override bool ReleaseHandle() + { + return DestroyMenu(handle); + } + } + + public static class Win32NativeMenu + { + [DllImport("user32.dll")] + private static extern IntPtr CreatePopupMenu(); + + [DllImport("user32.dll")] + private static extern int TrackPopupMenuEx(SafeMenuHandle hMenu, TPM wFlags, int x, int y, IntPtr hWnd, IntPtr tpmParams); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetCursorPos(out POINT lpPoint); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern bool AppendMenu(SafeMenuHandle hMenu, MenuFlags uFlags, uint uIDNewItem, string lpNewItem); + + [DllImport("user32.dll")] + private static extern bool EnableMenuItem(SafeMenuHandle hMenu, uint uIDEnableItem, MenuFlags uEnable); + + [DllImport("user32.dll")] + private static extern bool SetMenuItemBitmaps(SafeMenuHandle hMenu, uint uPosition, MFT uFlags, IntPtr hBitmapUnchecked, IntPtr hBitmapChecked); + + [DllImport("user32.dll")] + private static extern bool GetIconInfo(SafeIconHandle hIcon, out ICONINFO piconinfo); + + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(IntPtr hWnd); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + private class MENUITEMINFO + { + public Int32 cbSize = Marshal.SizeOf(typeof(MENUITEMINFO)); + public MIIM fMask; + public MFT fType; + public MFS fState; + public UInt32 wID; + public IntPtr hSubMenu; + public IntPtr hbmpChecked; + public IntPtr hbmpUnchecked; + public IntPtr dwItemData; + public string dwTypeData = null; + public UInt32 cch; // length of dwTypeData + public IntPtr hbmpItem; + + public MENUITEMINFO() { } + public MENUITEMINFO(MIIM pfMask) + { + fMask = pfMask; + } + } + + [Flags] + private enum MIIM + { + BITMAP = 0x00000080, + CHECKMARKS = 0x00000008, + DATA = 0x00000020, + FTYPE = 0x00000100, + ID = 0x00000002, + STATE = 0x00000001, + STRING = 0x00000040, + SUBMENU = 0x00000004, + TYPE = 0x00000010 + } + + [Flags] + public enum TPM : uint + { + TPM_LEFTBUTTON = 0x0000, + TPM_RIGHTBUTTON = 0x0002, + TPM_LEFTALIGN = 0x0000, + TPM_CENTERALIGN = 0x0004, + TPM_RIGHTALIGN = 0x0008, + TPM_TOPALIGN = 0x0000, + TPM_VCENTERALIGN = 0x0010, + TPM_BOTTOMALIGN = 0x0020, + TPM_HORIZONTAL = 0x0000, + TPM_VERTICAL = 0x0040, + TPM_NONOTIFY = 0x0080, + TPM_RETURNCMD = 0x0100, + TPM_RECURSE = 0x0001, + TPM_HORPOSANIMATION = 0x0400, + TPM_HORNEGANIMATION = 0x0800, + TPM_VERPOSANIMATION = 0x1000, + TPM_VERNEGANIMATION = 0x2000, + TPM_NOANIMATION = 0x4000, + TPM_LAYOUTRTL = 0x8000, + TPM_WORKAREA = 0x10000 + } + + [Flags] + private enum MFT : uint + { + + GRAYED = 0x00000003, + DISABLED = 0x00000003, + CHECKED = 0x00000008, + SEPARATOR = 0x00000800, + RADIOCHECK = 0x00000200, + BITMAP = 0x00000004, + OWNERDRAW = 0x00000100, + MENUBARBREAK = 0x00000020, + MENUBREAK = 0x00000040, + RIGHTORDER = 0x00002000, + BYCOMMAND = 0x00000000, + BYPOSITION = 0x00000400, + POPUP = 0x00000010, + ////Added + MFT_STRING = 0x000, + MF_BYPOSITION = 0x400, + MF_REMOVE = 0x1000 + } + + private enum MFS : uint + { + MFS_ENABLED = 0x00000000, + MFS_UNCHECKED = 0x00000000, + MFS_UNHILITE = 0x00000000, + MFS_GRAYED = 0x00000003, + MFS_DISABLED = 0x00000003, + MFS_CHECKED = 0x00000008, + MFS_HILITE = 0x00000080, + MFS_DEFAULT = 0x00001000 + } + + [StructLayout(LayoutKind.Sequential)] + private struct POINT + { + public int x; + public int y; + } + + [Flags] + private enum MenuFlags : uint + { + MF_STRING = 0, + MF_BYCOMMAND = 0, + MF_BYPOSITION = 0x400, + MF_SEPARATOR = 0x800, + MF_REMOVE = 0x1000, + MF_DISABLED = 0x00000002, + MF_ENABLED = 0x00000000, + MF_GRAYED = 0x00000001 + } + + [StructLayout(LayoutKind.Sequential)] + private struct ICONINFO + { + public bool fIcon; + public Int32 xHotspot; + public Int32 yHotspot; + public IntPtr hbmMask; + public IntPtr hbmColor; + } + + public static Win32MenuItem ShowContextMenu(IntPtr hMainWindow, IList listMenu) + { + if (!GetCursorPos(out POINT pt)) + throw new System.ComponentModel.Win32Exception(); + + var hMenu = new SafeMenuHandle(CreatePopupMenu()); + + if (hMenu.IsInvalid) + throw new System.ComponentModel.Win32Exception(); + + uint menuID = 1; + + //Keep reference until end of function. Then handles are safely closed. + var listSupressDispose = new List(); + + foreach (var menu in listMenu) + { + if (menu.Text == "-") + { + if (!AppendMenu(hMenu, MenuFlags.MF_SEPARATOR, 0, null)) + throw new System.ComponentModel.Win32Exception(); + } + else + { + if (!AppendMenu(hMenu, MenuFlags.MF_STRING, menuID, menu.Text)) + throw new System.ComponentModel.Win32Exception(); + + if (!menu.Enabled) + EnableMenuItem(hMenu, menuID, MenuFlags.MF_BYCOMMAND | MenuFlags.MF_DISABLED); + + ICONINFO ii = new ICONINFO(); + //TODO-DNC Icons in Tray contect menu are not transparent - cosmetic + GetIconInfo(menu.Icon, out ii); + listSupressDispose.Add(new SafeObjectHandle(ii.hbmColor)); + listSupressDispose.Add(new SafeObjectHandle(ii.hbmMask)); + SetMenuItemBitmaps(hMenu, menuID, MFT.BYCOMMAND, ii.hbmColor, ii.hbmColor); + } + + menuID++; + } + + //must set window to the foreground or the menu won't disappear when it should + SetForegroundWindow(hMainWindow); + + var selectedMenu = TrackPopupMenuEx(hMenu, TPM.TPM_RIGHTALIGN | TPM.TPM_BOTTOMALIGN | + TPM.TPM_RETURNCMD | TPM.TPM_RIGHTBUTTON, pt.x, pt.y, hMainWindow, IntPtr.Zero); + + return selectedMenu == 0 ? null : listMenu[selectedMenu - 1]; + } + } + + public class Win32MenuItem : IMenuItem + { + public string Text { get; private set; } + public SafeIconHandle Icon { get; private set; } + public Action Callback { get; private set; } + public IList SubItems { get; private set; } + public bool Enabled { get; private set; } + + public Win32MenuItem(string text, MenuIcons icon, Action callback, IList subitems) + { + if (subitems != null && subitems.Count > 0) + throw new NotImplementedException("So far not needed."); + + this.Text = text; + this.Callback = callback; + this.SubItems = subitems; + this.Enabled = true; + SetIcon(icon); + } + + #region IMenuItem implementation + public void SetText(string text) + { + this.Text = text; + } + + public void SetIcon(MenuIcons icon) + { + switch (icon) + { + case MenuIcons.Pause: + this.Icon = Win32IconLoader.MenuPauseIcon; + break; + case MenuIcons.Quit: + this.Icon = Win32IconLoader.MenuQuitIcon; + break; + case MenuIcons.Resume: + this.Icon = Win32IconLoader.MenuPlayIcon; + break; + case MenuIcons.Status: + this.Icon = Win32IconLoader.MenuOpenIcon; + break; + case MenuIcons.None: + default: + this.Icon = new SafeIconHandle(IntPtr.Zero); + break; + } + } + + public void SetEnabled(bool isEnabled) + { + this.Enabled = isEnabled; + } + + public void SetDefault(bool value) + { + //TODO-DNC Cosmetic, not needed + } + #endregion + } +} \ No newline at end of file diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32NotifyIcon.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32NotifyIcon.cs new file mode 100644 index 000000000..be2ebccb8 --- /dev/null +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32NotifyIcon.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +namespace Duplicati.GUI.TrayIcon +{ + public static class Win32NativeNotifyIcon + { + public const int WM_APP = 0x8000; + + public enum NotifyFlags : int + { + NIF_MESSAGE = 0x00000001, + NIF_ICON = 0x00000002, + NIF_TIP = 0x00000004, + NIF_STATE = 0x00000008, + NIF_INFO = 0x00000010, + NIF_GUID = 0x00000020, + NIF_REALTIME = 0x00000040, + NIF_SHOWTIP = 0x00000080 + } + + public enum NotifyIconMessage : int + { + NIM_ADD = 0x00000000, + NIM_MODIFY = 0x00000001, + NIM_DELETE = 0x00000002, + NIM_SETFOCUS = 0x00000003, + NIM_SETVERSION = 0x00000004 + } + + public enum InfoFlags : int + { + NIIF_NONE = 0x00000000, + NIIF_INFO = 0x00000001, + NIIF_WARNING = 0x00000002, + NIIF_ERROR = 0x00000003, + NIIF_USER = 0x00000004, + NIIF_NOSOUND = 0x00000010, + NIIF_LARGE_ICON = 0x00000020, + NIIF_RESPECT_QUIET_TIME = 0x00000080 + } + + public const int NOTIFYICON_VERSION_4 = 4; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public class NOTIFYICONDATA + { + public int cbSize; + public IntPtr hWnd; + public int uID; + public NotifyFlags uFlags; + public int uCallbackMessage; + public SafeIconHandle hIcon; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string szTip; + public int dwState; + public int dwStateMask; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string szInfo; + public int uTimeoutOrVersion; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + public string szInfoTitle; + public InfoFlags dwInfoFlags; + public Guid guidItem; + public IntPtr hBalloonicon; + } + + [DllImport("shell32.dll", CharSet = CharSet.Auto)] + public static extern int Shell_NotifyIcon(NotifyIconMessage dwMessage, NOTIFYICONDATA pNID); + } + + public class Win32NotifyIcon + { + public int ID { get; private set; } + + public int MessageID { get; private set; } + + public IntPtr MainWindowHandle { get; private set; } + + public SafeIconHandle Icon { get; private set; } + + public string ToolTip { get; private set; } + + public Win32NotifyIcon(IntPtr MainWindowHandle, int ID, int MessageID, SafeIconHandle Icon, string ToolTip) + { + if (MainWindowHandle == IntPtr.Zero | ID == 0 | MessageID == 0) + throw new ArgumentException("Bad input arguments Win32NotifyIcon::ctor"); + + this.MainWindowHandle = MainWindowHandle; + this.ID = ID; + this.MessageID = MessageID; + this.Icon = Icon; + this.ToolTip = ToolTip; + } + + public void Create() + { + var nid = new Win32NativeNotifyIcon.NOTIFYICONDATA + { + hIcon = Icon, + hWnd = MainWindowHandle, + uID = ID, + szTip = ToolTip, + uFlags = Win32NativeNotifyIcon.NotifyFlags.NIF_ICON | Win32NativeNotifyIcon.NotifyFlags.NIF_TIP | + Win32NativeNotifyIcon.NotifyFlags.NIF_MESSAGE | Win32NativeNotifyIcon.NotifyFlags.NIF_SHOWTIP, + uCallbackMessage = MessageID, + cbSize = Marshal.SizeOf(typeof(Win32NativeNotifyIcon.NOTIFYICONDATA)) + }; + + if (Win32NativeNotifyIcon.Shell_NotifyIcon(Win32NativeNotifyIcon.NotifyIconMessage.NIM_ADD, nid) == 0) + throw new System.ComponentModel.Win32Exception(); + + nid.uTimeoutOrVersion = Win32NativeNotifyIcon.NOTIFYICON_VERSION_4; + + if (Win32NativeNotifyIcon.Shell_NotifyIcon(Win32NativeNotifyIcon.NotifyIconMessage.NIM_SETVERSION, nid) == 0) + throw new System.ComponentModel.Win32Exception(); + } + + public void Delete() + { + var nid = new Win32NativeNotifyIcon.NOTIFYICONDATA + { + hWnd = MainWindowHandle, + uID = ID, + hIcon = new SafeIconHandle(IntPtr.Zero), + cbSize = Marshal.SizeOf(typeof(Win32NativeNotifyIcon.NOTIFYICONDATA)) + }; + + if (Win32NativeNotifyIcon.Shell_NotifyIcon(Win32NativeNotifyIcon.NotifyIconMessage.NIM_DELETE, nid) == 0) + throw new System.ComponentModel.Win32Exception(); + } + + public void SetIcon(SafeIconHandle hIcon) + { + var nid = new Win32NativeNotifyIcon.NOTIFYICONDATA + { + hIcon = hIcon, + hWnd = MainWindowHandle, + uID = ID, + uFlags = Win32NativeNotifyIcon.NotifyFlags.NIF_ICON, + cbSize = Marshal.SizeOf(typeof(Win32NativeNotifyIcon.NOTIFYICONDATA)) + }; + + if (Win32NativeNotifyIcon.Shell_NotifyIcon(Win32NativeNotifyIcon.NotifyIconMessage.NIM_MODIFY, nid) == 0) + throw new System.ComponentModel.Win32Exception(); + + Icon = hIcon; + } + + public void ShowBalloonTip(string BallonTitle, string BallonMessage, Win32NativeNotifyIcon.InfoFlags NotificationType) + { + var nid = new Win32NativeNotifyIcon.NOTIFYICONDATA + { + hIcon = Icon, + hWnd = MainWindowHandle, + uID = ID, + cbSize = Marshal.SizeOf(typeof(Win32NativeNotifyIcon.NOTIFYICONDATA)), + szInfoTitle = BallonTitle, + szInfo = BallonMessage, + dwInfoFlags = NotificationType, + uFlags = Win32NativeNotifyIcon.NotifyFlags.NIF_INFO + }; + + if (Win32NativeNotifyIcon.Shell_NotifyIcon(Win32NativeNotifyIcon.NotifyIconMessage.NIM_MODIFY, nid) == 0) + throw new System.ComponentModel.Win32Exception(); + } + } +} \ No newline at end of file diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32Runner.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32Runner.cs new file mode 100644 index 000000000..bbca442d2 --- /dev/null +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32Runner.cs @@ -0,0 +1,170 @@ +// Copyright (C) 2015, The Duplicati Team +// http://www.duplicati.com, info@duplicati.com +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Drawing; +using Duplicati.Server.Serialization.Interface; + +namespace Duplicati.GUI.TrayIcon +{ + public class Win32Runner : TrayIconBase + { + private const string WINDOW_CLASSNAME = "DuplicatiMessageMonitorWindowTrayIcon"; + private const int TRAY_ICON_ID_1 = 1; + private const int WM_TRAYICON_1 = Win32NativeNotifyIcon.WM_APP + 1; + private readonly Win32Window m_window = new Win32Window(); + private Win32NotifyIcon m_ntfIcon; + private bool m_TrayIconCreated; + List m_TrayContextMenu; + + public override void Init(string[] args) + { + base.Init(args); + + m_window.MessageReceived += MessageMonitor_MessageReceived; + m_window.CreateWindow(WINDOW_CLASSNAME); + } + + private void MessageMonitor_MessageReceived(IntPtr hwnd, Win32NativeWindow.WindowsMessage message, IntPtr wParam, IntPtr lParam) + { + if (message == Win32NativeWindow.WindowsMessage.WM_SHOWWINDOW) + { + if (!m_TrayIconCreated) + { + m_ntfIcon = new Win32NotifyIcon(m_window.Handle, TRAY_ICON_ID_1, WM_TRAYICON_1, + Win32IconLoader.TrayNormalIcon, Duplicati.Library.AutoUpdater.AutoUpdateSettings.AppName); + m_ntfIcon.Create(); + m_TrayIconCreated = true; + } + } + + if (message == (Win32NativeWindow.WindowsMessage)WM_TRAYICON_1) + { + var llpTray = (Win32NativeWindow.WindowsMessage)((uint)lParam.ToInt32() & 0x0000FFFF); + + switch (llpTray) + { + case Win32NativeWindow.WindowsMessage.WM_LBUTTONDBLCLK: + m_onDoubleClick?.Invoke(); + break; + case Win32NativeWindow.WindowsMessage.WM_LBUTTONUP: + m_onSingleClick?.Invoke(); + break; + case Win32NativeWindow.WindowsMessage.WM_CONTEXTMENU: + Win32NativeMenu.ShowContextMenu(m_window.Handle, m_TrayContextMenu)?.Callback?.Invoke(); + break; + case Win32NativeWindow.WindowsMessage.NIN_BALLOONUSERCLICK: + m_onNotificationClick?.Invoke(); + break; + default: + break; + } + } + } + + protected override void UpdateUIState(Action action) + { + action.Invoke(); + } + + protected override void RegisterStatusUpdateCallback() + { + Program.Connection.OnStatusUpdated += delegate (IServerStatus status) + { + this.OnStatusUpdated(status); + }; + } + + #region implemented abstract members of Duplicati.GUI.TrayIcon.TrayIconBase + protected override void Run(string[] args) + { + } + + protected override IMenuItem CreateMenuItem(string text, MenuIcons icon, Action callback, IList subitems) + { + return new Win32MenuItem(text, icon, callback, subitems); + } + + protected override void NotifyUser(string title, string message, NotificationType type) + { + var icon = Win32NativeNotifyIcon.InfoFlags.NIIF_INFO; + + switch (type) + { + case NotificationType.Information: + icon = Win32NativeNotifyIcon.InfoFlags.NIIF_INFO; + break; + case NotificationType.Warning: + icon = Win32NativeNotifyIcon.InfoFlags.NIIF_WARNING; + break; + case NotificationType.Error: + icon = Win32NativeNotifyIcon.InfoFlags.NIIF_ERROR; + break; + } + + m_ntfIcon.ShowBalloonTip(title, message, icon); + } + + protected override void Exit() + { + m_ntfIcon.Delete(); + m_window.DestroyWindow(); + } + + protected override void SetIcon(TrayIcons icon) + { + //There are calls before NotifyIcons is created + if (m_ntfIcon == null) + return; + + switch (icon) + { + case TrayIcons.IdleError: + m_ntfIcon?.SetIcon(Win32IconLoader.TrayErrorIcon); + break; + case TrayIcons.Paused: + case TrayIcons.PausedError: + m_ntfIcon?.SetIcon(Win32IconLoader.TrayPauseIcon); + break; + case TrayIcons.Running: + case TrayIcons.RunningError: + m_ntfIcon?.SetIcon(Win32IconLoader.TrayWorkingIcon); + break; + case TrayIcons.Idle: + m_ntfIcon?.SetIcon(Win32IconLoader.TrayNormalIcon); + break; + default: + m_ntfIcon?.SetIcon(Win32IconLoader.TrayNormalIcon); + break; + } + } + + protected override void SetMenu(System.Collections.Generic.IEnumerable items) + { + m_TrayContextMenu = new List(); + + foreach(var item in items) + m_TrayContextMenu.Add((Win32MenuItem)item); + } + + public override void Dispose() + { + } + #endregion + } +} diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32Window.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32Window.cs new file mode 100644 index 000000000..427e0c353 --- /dev/null +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Win32Window.cs @@ -0,0 +1,453 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +namespace Duplicati.GUI.TrayIcon +{ + public class Win32Window : IDisposable + { + private bool m_disposed; + private IntPtr m_classAtom = IntPtr.Zero; + private readonly IntPtr m_hInstance = Win32NativeWindow.GetModuleHandle(null); + public delegate void MessageMonitorEventHandler(IntPtr hwnd, Win32NativeWindow.WindowsMessage message, IntPtr wParam, IntPtr lParam); + public event MessageMonitorEventHandler MessageReceived; + public IntPtr Handle { get; private set; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!m_disposed) + { + if (disposing) + { + // free other managed objects that implement IDisposable only + } + + // Dispose unmanaged resources + DestroyWindow(); + m_disposed = true; + } + } + + public Win32Window() + { + Handle = IntPtr.Zero; + } + + public void CreateWindow(string ClassName) + { + if (string.IsNullOrEmpty(ClassName)) + throw new ArgumentException("ClassName is null or empty"); + + if (Handle != IntPtr.Zero | m_classAtom != IntPtr.Zero) + return; + + Win32NativeWindow.WNDCLASSEX wc = new Win32NativeWindow.WNDCLASSEX + { + cbSize = Marshal.SizeOf(typeof(Win32NativeWindow.WNDCLASSEX)), + lpfnWndProc = CustomWndProc, + lpszClassName = ClassName, + hInstance = m_hInstance + }; + + var atom = Win32NativeWindow.RegisterClassEx(ref wc); + + if (atom == 0) + throw new System.ComponentModel.Win32Exception(); + + m_classAtom = new IntPtr(atom); + + Handle = Win32NativeWindow.CreateWindowEx(0, m_classAtom, + null, 0, 0, 0, 0, 0, IntPtr.Zero, IntPtr.Zero, m_hInstance, IntPtr.Zero); + + if (Handle == IntPtr.Zero) + throw new System.ComponentModel.Win32Exception(); + + //Send WM_SHOWWINDOW to create NotifyIcon + Win32NativeWindow.ShowWindow(Handle, 1); + Win32NativeWindow.ShowWindow(Handle, 0); + + var msg = new Win32NativeWindow.MSG(); + + while (Win32NativeWindow.GetMessage(out msg, IntPtr.Zero, 0, 0)) + { + Win32NativeWindow.TranslateMessage(ref msg); + Win32NativeWindow.DispatchMessage(ref msg); + } + } + + public void DestroyWindow() + { + Win32NativeWindow.PostQuitMessage(0); + + if (Handle != IntPtr.Zero) + { + Win32NativeWindow.DestroyWindow(Handle); + Handle = IntPtr.Zero; + } + + if (m_classAtom != IntPtr.Zero) + { + Win32NativeWindow.UnregisterClass(m_classAtom, m_hInstance); + m_classAtom = IntPtr.Zero; + } + } + + private IntPtr CustomWndProc(IntPtr hWnd, Win32NativeWindow.WindowsMessage msg, IntPtr wParam, IntPtr lParam) + { + MessageReceived?.Invoke(hWnd, msg, wParam, lParam); + return Win32NativeWindow.DefWindowProc(hWnd, msg, wParam, lParam); + } + } + public static class Win32NativeWindow + { + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr CreateWindowEx(uint dwExStyle, IntPtr lpszClassName, + string lpszWindowName, uint dwStyle, int x, int y, int nWidth, int nHeight, + IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr DefWindowProc(IntPtr hWnd, WindowsMessage msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr DispatchMessage([In] ref MSG lpMsg); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern bool TranslateMessage([In] ref MSG lpMsg); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetMessage(out MSG msg, IntPtr hWnd, uint uMsgFilterMin, uint uMsgFilterMax); + + [DllImport("user32.dll", SetLastError = true)] + public static extern UInt16 RegisterClassEx(ref WNDCLASSEX lpWndClass); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool UnregisterClass(IntPtr lpClassName, IntPtr hInstance); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool DestroyWindow(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr GetModuleHandle(string lpModuleName); + + [DllImport("user32.dll")] + public static extern void PostQuitMessage(int nExitCode); + + public delegate IntPtr WndProc(IntPtr hWnd, WindowsMessage msg, IntPtr wParam, IntPtr lParam); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct WNDCLASSEX + { + public int cbSize; + public uint style; + public WndProc lpfnWndProc; + public int cbClsExtra; + public int cbWndExtra; + public IntPtr hInstance; + public IntPtr hIcon; + public IntPtr hCursor; + public IntPtr hbrBackground; + public string lpszMenuName; + public string lpszClassName; + public IntPtr hIconSm; + } + + [StructLayout(LayoutKind.Sequential)] + public struct MSG + { + public IntPtr hwnd; + public uint message; + public IntPtr wParam; + public IntPtr lParam; + public int time; + public POINT pt; + } + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int x; + public int y; + } + + public enum WindowsMessage + { + WM_NULL = 0x0000, + WM_CREATE = 0x0001, + WM_DESTROY = 0x0002, + WM_MOVE = 0x0003, + WM_SIZE = 0x0005, + WM_ACTIVATE = 0x0006, + WM_SETFOCUS = 0x0007, + WM_KILLFOCUS = 0x0008, + WM_ENABLE = 0x000A, + WM_SETREDRAW = 0x000B, + WM_SETTEXT = 0x000C, + WM_GETTEXT = 0x000D, + WM_GETTEXTLENGTH = 0x000E, + WM_PAINT = 0x000F, + WM_CLOSE = 0x0010, + WM_QUERYENDSESSION = 0x0011, + WM_QUERYOPEN = 0x0013, + WM_ENDSESSION = 0x0016, + WM_QUIT = 0x0012, + WM_ERASEBKGND = 0x0014, + WM_SYSCOLORCHANGE = 0x0015, + WM_SHOWWINDOW = 0x0018, + WM_WININICHANGE = 0x001A, + WM_SETTINGCHANGE = WM_WININICHANGE, + WM_DEVMODECHANGE = 0x001B, + WM_ACTIVATEAPP = 0x001C, + WM_FONTCHANGE = 0x001D, + WM_TIMECHANGE = 0x001E, + WM_CANCELMODE = 0x001F, + WM_SETCURSOR = 0x0020, + WM_MOUSEACTIVATE = 0x0021, + WM_CHILDACTIVATE = 0x0022, + WM_QUEUESYNC = 0x0023, + WM_GETMINMAXINFO = 0x0024, + WM_PAINTICON = 0x0026, + WM_ICONERASEBKGND = 0x0027, + WM_NEXTDLGCTL = 0x0028, + WM_SPOOLERSTATUS = 0x002A, + WM_DRAWITEM = 0x002B, + WM_MEASUREITEM = 0x002C, + WM_DELETEITEM = 0x002D, + WM_VKEYTOITEM = 0x002E, + WM_CHARTOITEM = 0x002F, + WM_SETFONT = 0x0030, + WM_GETFONT = 0x0031, + WM_SETHOTKEY = 0x0032, + WM_GETHOTKEY = 0x0033, + WM_QUERYDRAGICON = 0x0037, + WM_COMPAREITEM = 0x0039, + WM_GETOBJECT = 0x003D, + WM_COMPACTING = 0x0041, + WM_COMMNOTIFY = 0x0044, + WM_WINDOWPOSCHANGING = 0x0046, + WM_WINDOWPOSCHANGED = 0x0047, + WM_POWER = 0x0048, + WM_COPYDATA = 0x004A, + WM_CANCELJOURNAL = 0x004B, + WM_NOTIFY = 0x004E, + WM_INPUTLANGCHANGEREQUEST = 0x0050, + WM_INPUTLANGCHANGE = 0x0051, + WM_TCARD = 0x0052, + WM_HELP = 0x0053, + WM_USERCHANGED = 0x0054, + WM_NOTIFYFORMAT = 0x0055, + WM_CONTEXTMENU = 0x007B, + WM_STYLECHANGING = 0x007C, + WM_STYLECHANGED = 0x007D, + WM_DISPLAYCHANGE = 0x007E, + WM_GETICON = 0x007F, + WM_SETICON = 0x0080, + WM_NCCREATE = 0x0081, + WM_NCDESTROY = 0x0082, + WM_NCCALCSIZE = 0x0083, + WM_NCHITTEST = 0x0084, + WM_NCPAINT = 0x0085, + WM_NCACTIVATE = 0x0086, + WM_GETDLGCODE = 0x0087, + WM_SYNCPAINT = 0x0088, + + + WM_NCMOUSEMOVE = 0x00A0, + WM_NCLBUTTONDOWN = 0x00A1, + WM_NCLBUTTONUP = 0x00A2, + WM_NCLBUTTONDBLCLK = 0x00A3, + WM_NCRBUTTONDOWN = 0x00A4, + WM_NCRBUTTONUP = 0x00A5, + WM_NCRBUTTONDBLCLK = 0x00A6, + WM_NCMBUTTONDOWN = 0x00A7, + WM_NCMBUTTONUP = 0x00A8, + WM_NCMBUTTONDBLCLK = 0x00A9, + WM_NCXBUTTONDOWN = 0x00AB, + WM_NCXBUTTONUP = 0x00AC, + WM_NCXBUTTONDBLCLK = 0x00AD, + + WM_INPUT_DEVICE_CHANGE = 0x00FE, + WM_INPUT = 0x00FF, + + WM_KEYFIRST = 0x0100, + WM_KEYDOWN = 0x0100, + WM_KEYUP = 0x0101, + WM_CHAR = 0x0102, + WM_DEADCHAR = 0x0103, + WM_SYSKEYDOWN = 0x0104, + WM_SYSKEYUP = 0x0105, + WM_SYSCHAR = 0x0106, + WM_SYSDEADCHAR = 0x0107, + WM_UNICHAR = 0x0109, + WM_KEYLAST = 0x0109, + + WM_IME_STARTCOMPOSITION = 0x010D, + WM_IME_ENDCOMPOSITION = 0x010E, + WM_IME_COMPOSITION = 0x010F, + WM_IME_KEYLAST = 0x010F, + + WM_INITDIALOG = 0x0110, + WM_COMMAND = 0x0111, + WM_SYSCOMMAND = 0x0112, + WM_TIMER = 0x0113, + WM_HSCROLL = 0x0114, + WM_VSCROLL = 0x0115, + WM_INITMENU = 0x0116, + WM_INITMENUPOPUP = 0x0117, + WM_MENUSELECT = 0x011F, + WM_MENUCHAR = 0x0120, + WM_ENTERIDLE = 0x0121, + WM_MENURBUTTONUP = 0x0122, + WM_MENUDRAG = 0x0123, + WM_MENUGETOBJECT = 0x0124, + WM_UNINITMENUPOPUP = 0x0125, + WM_MENUCOMMAND = 0x0126, + + WM_CHANGEUISTATE = 0x0127, + WM_UPDATEUISTATE = 0x0128, + WM_QUERYUISTATE = 0x0129, + + WM_CTLCOLORMSGBOX = 0x0132, + WM_CTLCOLOREDIT = 0x0133, + WM_CTLCOLORLISTBOX = 0x0134, + WM_CTLCOLORBTN = 0x0135, + WM_CTLCOLORDLG = 0x0136, + WM_CTLCOLORSCROLLBAR = 0x0137, + WM_CTLCOLORSTATIC = 0x0138, + MN_GETHMENU = 0x01E1, + + WM_MOUSEFIRST = 0x0200, + WM_MOUSEMOVE = 0x0200, + WM_LBUTTONDOWN = 0x0201, + WM_LBUTTONUP = 0x0202, + WM_LBUTTONDBLCLK = 0x0203, + WM_RBUTTONDOWN = 0x0204, + WM_RBUTTONUP = 0x0205, + WM_RBUTTONDBLCLK = 0x0206, + WM_MBUTTONDOWN = 0x0207, + WM_MBUTTONUP = 0x0208, + WM_MBUTTONDBLCLK = 0x0209, + WM_MOUSEWHEEL = 0x020A, + WM_XBUTTONDOWN = 0x020B, + WM_XBUTTONUP = 0x020C, + WM_XBUTTONDBLCLK = 0x020D, + WM_MOUSEHWHEEL = 0x020E, + + WM_PARENTNOTIFY = 0x0210, + WM_ENTERMENULOOP = 0x0211, + WM_EXITMENULOOP = 0x0212, + + WM_NEXTMENU = 0x0213, + WM_SIZING = 0x0214, + WM_CAPTURECHANGED = 0x0215, + WM_MOVING = 0x0216, + + WM_POWERBROADCAST = 0x0218, + + WM_DEVICECHANGE = 0x0219, + + WM_MDICREATE = 0x0220, + WM_MDIDESTROY = 0x0221, + WM_MDIACTIVATE = 0x0222, + WM_MDIRESTORE = 0x0223, + WM_MDINEXT = 0x0224, + WM_MDIMAXIMIZE = 0x0225, + WM_MDITILE = 0x0226, + WM_MDICASCADE = 0x0227, + WM_MDIICONARRANGE = 0x0228, + WM_MDIGETACTIVE = 0x0229, + + + WM_MDISETMENU = 0x0230, + WM_ENTERSIZEMOVE = 0x0231, + WM_EXITSIZEMOVE = 0x0232, + WM_DROPFILES = 0x0233, + WM_MDIREFRESHMENU = 0x0234, + + WM_IME_SETCONTEXT = 0x0281, + WM_IME_NOTIFY = 0x0282, + WM_IME_CONTROL = 0x0283, + WM_IME_COMPOSITIONFULL = 0x0284, + WM_IME_SELECT = 0x0285, + WM_IME_CHAR = 0x0286, + WM_IME_REQUEST = 0x0288, + WM_IME_KEYDOWN = 0x0290, + WM_IME_KEYUP = 0x0291, + + WM_MOUSEHOVER = 0x02A1, + WM_MOUSELEAVE = 0x02A3, + WM_NCMOUSEHOVER = 0x02A0, + WM_NCMOUSELEAVE = 0x02A2, + + WM_WTSSESSION_CHANGE = 0x02B1, + + WM_TABLET_FIRST = 0x02c0, + WM_TABLET_LAST = 0x02df, + + WM_CUT = 0x0300, + WM_COPY = 0x0301, + WM_PASTE = 0x0302, + WM_CLEAR = 0x0303, + WM_UNDO = 0x0304, + WM_RENDERFORMAT = 0x0305, + WM_RENDERALLFORMATS = 0x0306, + WM_DESTROYCLIPBOARD = 0x0307, + WM_DRAWCLIPBOARD = 0x0308, + WM_PAINTCLIPBOARD = 0x0309, + WM_VSCROLLCLIPBOARD = 0x030A, + WM_SIZECLIPBOARD = 0x030B, + WM_ASKCBFORMATNAME = 0x030C, + WM_CHANGECBCHAIN = 0x030D, + WM_HSCROLLCLIPBOARD = 0x030E, + WM_QUERYNEWPALETTE = 0x030F, + WM_PALETTEISCHANGING = 0x0310, + WM_PALETTECHANGED = 0x0311, + WM_HOTKEY = 0x0312, + + WM_PRINT = 0x0317, + WM_PRINTCLIENT = 0x0318, + + WM_APPCOMMAND = 0x0319, + + WM_THEMECHANGED = 0x031A, + + WM_CLIPBOARDUPDATE = 0x031D, + + WM_DWMCOMPOSITIONCHANGED = 0x031E, + WM_DWMNCRENDERINGCHANGED = 0x031F, + WM_DWMCOLORIZATIONCOLORCHANGED = 0x0320, + WM_DWMWINDOWMAXIMIZEDCHANGE = 0x0321, + + WM_GETTITLEBARINFOEX = 0x033F, + + WM_HANDHELDFIRST = 0x0358, + WM_HANDHELDLAST = 0x035F, + + WM_AFXFIRST = 0x0360, + WM_AFXLAST = 0x037F, + + WM_PENWINFIRST = 0x0380, + WM_PENWINLAST = 0x038F, + + WM_APP = 0x8000, + + WM_USER = 0x0400, + + NIN_BALLOONUSERCLICK = 1029, + + WM_REFLECT = WM_USER + 0x1C00, + } + } +} diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/WinFormsRunner.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/WinFormsRunner.cs deleted file mode 100644 index 0de1d10dc..000000000 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/WinFormsRunner.cs +++ /dev/null @@ -1,266 +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.Windows.Forms; -using Duplicati.Server.Serialization; -using Duplicati.Server.Serialization.Interface; - -namespace Duplicati.GUI.TrayIcon.Windows -{ - //We use a separate class to start the runner to avoid attempts to load WinForms from Program.cs - public class WinFormsRunner : TrayIconBase - { - private class MenuItemWrapper : IMenuItem - { - private readonly ToolStripItem m_menu; - private readonly Action m_callback; - - public ToolStripItem MenuItem { get { return m_menu; } } - - public MenuItemWrapper(string text, MenuIcons icon, Action callback, IList subitems) - { - if (text == "-") - m_menu = new ToolStripSeparator(); - else - { - m_menu = new ToolStripMenuItem(text); - if (icon != MenuIcons.None) - m_menu.Image = GetIcon(icon); - - if (callback != null) - { - m_callback = callback; - m_menu.Click += OnMenuClick; - } - - if (subitems != null && subitems.Count > 0) - foreach(var itm in subitems) - ((ToolStripMenuItem)m_menu).DropDownItems.Add(((MenuItemWrapper)itm).MenuItem); - } - } - - private void OnMenuClick (object sender, EventArgs e) - { - m_callback(); - } - - private System.Drawing.Image GetIcon(MenuIcons icon) - { - switch(icon) - { - case MenuIcons.Pause: - return ImageLoader.Pause; - case MenuIcons.Quit: - return ImageLoader.CloseMenuIcon; - case MenuIcons.Resume: - return ImageLoader.Play; - case MenuIcons.Status: - return ImageLoader.StatusMenuIcon; - case MenuIcons.None: - default: - return null; - } - } - - #region IMenuItem implementation - public void SetText(string text) - { - m_menu.Text = text; - } - - public void SetIcon(MenuIcons icon) - { - m_menu.Image = GetIcon(icon); - } - - public void SetDefault(bool value) - { - m_menu.Font = new System.Drawing.Font(m_menu.Font, System.Drawing.FontStyle.Bold); - } - #endregion - } - - private Form m_handleProvider; - private NotifyIcon m_trayIcon; - - public override void Init (string[] args) - { - //We need this ugly hack to get a handle that we can call Invoke on, - // and sadly the TrayIcon does not expose one, and forcing the context menu - // to create one causes weird "lost clicks" - m_handleProvider = new Form() - { - FormBorderStyle = FormBorderStyle.None, - Width = 10, - Height = 10, - Top = 0, - Left = 0 - }; - m_handleProvider.Show(); - m_handleProvider.Hide(); - - m_trayIcon = new NotifyIcon(); - m_trayIcon.DoubleClick += new EventHandler(m_trayIcon_DoubleClick); - m_trayIcon.Click += new EventHandler(m_trayIcon_Click); - m_trayIcon.BalloonTipClicked += m_BalloonTipClicked; - m_trayIcon.Text = Duplicati.Library.AutoUpdater.AutoUpdateSettings.AppName; - base.Init(args); - } - - protected override void UpdateUIState(Action action) - { - m_handleProvider.Invoke(action); - } - - private void m_trayIcon_Click(object sender, EventArgs e) - { - if (m_onSingleClick != null) - m_onSingleClick(); - else if (m_trayIcon != null && m_trayIcon.ContextMenuStrip != null) - { - // Show context menu on left-click as we have nothing happening otherwise - try - { - typeof(NotifyIcon).GetMethod("ShowContextMenu", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).Invoke(m_trayIcon, null); - } - catch { } - } - } - - private void m_trayIcon_DoubleClick(object sender, EventArgs e) - { - if (m_onDoubleClick != null) - m_onDoubleClick(); - } - - private void m_BalloonTipClicked(object sender, EventArgs e) - { - m_onNotificationClick?.Invoke(); - } - - protected override void RegisterStatusUpdateCallback () - { - Program.Connection.OnStatusUpdated += delegate(IServerStatus status) { - m_handleProvider.Invoke(new Action(OnStatusUpdated), status); - }; - } - - #region implemented abstract members of Duplicati.GUI.TrayIcon.TrayIconBase - protected override void Run (string[] args) - { - m_trayIcon.Visible = true; - Application.Run(); - } - - protected override IMenuItem CreateMenuItem (string text, MenuIcons icon, Action callback, IList subitems) - { - return new MenuItemWrapper(text, icon, callback, subitems); - } - - protected override void NotifyUser(string title, string message, NotificationType type) - { - var icon = ToolTipIcon.None; - - switch (type) - { - case NotificationType.Information: - icon = ToolTipIcon.Info; - break; - case NotificationType.Warning: - icon = ToolTipIcon.Warning; - break; - case NotificationType.Error: - icon = ToolTipIcon.Error; - break; - } - - m_trayIcon.ShowBalloonTip((int)TimeSpan.FromSeconds(60).TotalMilliseconds, title, message, icon); - } - - protected override void Exit () - { - Application.Exit(); - } - - protected override void SetIcon(TrayIcons icon) - { - switch (icon) - { - case TrayIcons.IdleError: - m_trayIcon.Icon = ImageLoader.LoadIcon(ImageLoader.ErrorIcon, System.Windows.Forms.SystemInformation.SmallIconSize); - break; - case TrayIcons.IdleWarning: - m_trayIcon.Icon = ImageLoader.LoadIcon(ImageLoader.WarningIcon, System.Windows.Forms.SystemInformation.SmallIconSize); - break; - case TrayIcons.Paused: - m_trayIcon.Icon = ImageLoader.LoadIcon(ImageLoader.PauseIcon, System.Windows.Forms.SystemInformation.SmallIconSize); - break; - case TrayIcons.PausedError: - m_trayIcon.Icon = ImageLoader.LoadIcon(ImageLoader.PauseIcon, System.Windows.Forms.SystemInformation.SmallIconSize); - break; - case TrayIcons.Running: - m_trayIcon.Icon = ImageLoader.LoadIcon(ImageLoader.WorkingIcon, System.Windows.Forms.SystemInformation.SmallIconSize); - break; - case TrayIcons.RunningError: - m_trayIcon.Icon = ImageLoader.LoadIcon(ImageLoader.WorkingIcon, System.Windows.Forms.SystemInformation.SmallIconSize); - break; - case TrayIcons.Idle: - default: - m_trayIcon.Icon = ImageLoader.LoadIcon(ImageLoader.NormalIcon, System.Windows.Forms.SystemInformation.SmallIconSize); - break; - } - } - - protected override void SetMenu (System.Collections.Generic.IEnumerable items) - { - m_trayIcon.ContextMenuStrip = new ContextMenuStrip(); - foreach(var itm in items) - m_trayIcon.ContextMenuStrip.Items.Add(((MenuItemWrapper)itm).MenuItem); - } - - public override void Dispose () - { - if (m_handleProvider != null) - { - m_handleProvider.Dispose(); - m_handleProvider = null; - } - - if (m_trayIcon != null) - { - m_trayIcon.Visible = false; - m_trayIcon.Dispose(); - m_trayIcon = null; - } - } - #endregion - - /*public override IBrowserWindow ShowUrlInWindow(string url) - { - var v = new WindowsBrowser(url); - v.Show(); - return v; - }*/ - } -} - diff --git a/Duplicati/Library/AutoUpdater/AutoUpdateSettings.cs b/Duplicati/Library/AutoUpdater/AutoUpdateSettings.cs index fdee0a45e..70febbec9 100644 --- a/Duplicati/Library/AutoUpdater/AutoUpdateSettings.cs +++ b/Duplicati/Library/AutoUpdater/AutoUpdateSettings.cs @@ -42,8 +42,8 @@ namespace Duplicati.Library.AutoUpdater private const string OEM_UPDATE_README = "oem-update-readme.txt"; private const string OEM_UPDATE_INSTALL_FILE = "oem-update-installid.txt"; - internal const string UPDATEURL_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_URLS"; - internal const string UPDATECHANNEL_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_CHANNEL"; + public const string UPDATEURL_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_URLS"; + public const string UPDATECHANNEL_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_CHANNEL"; internal const string MATCH_UPDATE_URL_PREFIX_GROUP = "prefix"; internal const string MATCH_UPDATE_URL_CHANNEL_GROUP = "channel"; diff --git a/Duplicati/Library/AutoUpdater/Duplicati.Library.AutoUpdater.csproj b/Duplicati/Library/AutoUpdater/Duplicati.Library.AutoUpdater.csproj index 6621c4c5c..b778d32e8 100644 --- a/Duplicati/Library/AutoUpdater/Duplicati.Library.AutoUpdater.csproj +++ b/Duplicati/Library/AutoUpdater/Duplicati.Library.AutoUpdater.csproj @@ -1,24 +1,21 @@  + net8.0 Exe - - bin\Debug - DEBUG; - false - - - bin\Release - false - + - + + + + + - - + + @@ -27,7 +24,12 @@ + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + \ No newline at end of file diff --git a/Duplicati/Library/AutoUpdater/UpdaterManager.cs b/Duplicati/Library/AutoUpdater/UpdaterManager.cs index a6346ee79..593ced18e 100644 --- a/Duplicati/Library/AutoUpdater/UpdaterManager.cs +++ b/Duplicati/Library/AutoUpdater/UpdaterManager.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -28,7 +28,7 @@ using System.Threading.Tasks; using Duplicati.Library.Interface; using Duplicati.Library.Common.IO; using Duplicati.Library.Common; -using System.Diagnostics; +using Duplicati.Library.Utility; namespace Duplicati.Library.AutoUpdater { @@ -61,7 +61,8 @@ namespace Duplicati.Library.AutoUpdater ? System.IO.Path.GetDirectoryName(Duplicati.Library.Utility.Utility.getEntryAssembly().Location) : Environment.ExpandEnvironmentVariables(System.Environment.GetEnvironmentVariable(string.Format(BASEINSTALLDIR_ENVNAME_TEMPLATE, APPNAME))); - private static readonly bool DISABLE_UPDATE_DOMAIN = Debugger.IsAttached || Utility.Utility.ParseBool(Environment.GetEnvironmentVariable(string.Format(SKIPUPDATE_ENVNAME_TEMPLATE, APPNAME)), false); + //FIXME: Auto Update doesn't work with the .net5 version + private static readonly bool DISABLE_UPDATE_DOMAIN = true;//Debugger.IsAttached || Utility.Utility.ParseBool(Environment.GetEnvironmentVariable(string.Format(SKIPUPDATE_ENVNAME_TEMPLATE, APPNAME)), false); public static bool RequiresRespawn { get; set; } @@ -78,10 +79,10 @@ namespace Duplicati.Library.AutoUpdater private const string DATETIME_FORMAT = "yyyymmddhhMMss"; private const string BASEINSTALLDIR_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_INSTALL_ROOT"; private const string UPDATEINSTALLDIR_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_UPDATE_ROOT"; - internal const string SKIPUPDATE_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_SKIP_UPDATE"; + public const string SKIPUPDATE_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_SKIP_UPDATE"; private const string RUN_UPDATED_FOLDER_PATH = "AUTOUPDATER_LOAD_UPDATE"; private const string SLEEP_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_SLEEP"; - internal const string UPDATE_STRATEGY_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_POLICY"; + public const string UPDATE_STRATEGY_ENVNAME_TEMPLATE = "AUTOUPDATER_{0}_POLICY"; private const string UPDATE_MANIFEST_FILENAME = "autoupdate.manifest"; private const string README_FILE = "README.txt"; private const string INSTALL_FILE = "installation.txt"; @@ -1047,7 +1048,8 @@ namespace Duplicati.Library.AutoUpdater m_hasUpdateInstalled = null; // Check if there are updates installed, otherwise use current - KeyValuePair best = new KeyValuePair(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, SelfVersion); + KeyValuePair best = new KeyValuePair(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), SelfVersion); + if (HasUpdateInstalled) best = m_hasUpdateInstalled.Value; @@ -1078,7 +1080,9 @@ namespace Duplicati.Library.AutoUpdater public static int RunFromMostRecent(System.Reflection.MethodInfo method, string[] cmdargs, AutoUpdateStrategy defaultstrategy = AutoUpdateStrategy.CheckDuring) { - return RunFromMostRecentSpawn(method, cmdargs, defaultstrategy); + // TODO: Disabled auto-updater as it does not currently work + return RunMethod(method, cmdargs); + //return RunFromMostRecentSpawn(method, cmdargs, defaultstrategy); } public static int RunFromMostRecentSpawn(System.Reflection.MethodInfo method, string[] cmdargs, AutoUpdateStrategy defaultstrategy = AutoUpdateStrategy.CheckDuring) @@ -1102,7 +1106,7 @@ namespace Duplicati.Library.AutoUpdater return r; } - var app = Environment.GetCommandLineArgs().First(); + var app = Environment.GetCommandLineArgs().First().Substring(0, Environment.GetCommandLineArgs().First().Length - 3) + "exe"; var args = Library.Utility.Utility.WrapAsCommandLine(Environment.GetCommandLineArgs().Skip(1), false); if (!Path.IsPathRooted(app)) diff --git a/Duplicati/Library/Backend/AlternativeFTP/Duplicati.Library.Backend.AlternativeFTP.csproj b/Duplicati/Library/Backend/AlternativeFTP/Duplicati.Library.Backend.AlternativeFTP.csproj index 1856e5aa8..b5ce13532 100644 --- a/Duplicati/Library/Backend/AlternativeFTP/Duplicati.Library.Backend.AlternativeFTP.csproj +++ b/Duplicati/Library/Backend/AlternativeFTP/Duplicati.Library.Backend.AlternativeFTP.csproj @@ -1,4 +1,5 @@  + net8.0 Library @@ -8,12 +9,13 @@ - + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/AzureBlob/Duplicati.Library.Backend.AzureBlob.csproj b/Duplicati/Library/Backend/AzureBlob/Duplicati.Library.Backend.AzureBlob.csproj index 4c018de75..f05fd5dbf 100644 --- a/Duplicati/Library/Backend/AzureBlob/Duplicati.Library.Backend.AzureBlob.csproj +++ b/Duplicati/Library/Backend/AzureBlob/Duplicati.Library.Backend.AzureBlob.csproj @@ -1,14 +1,9 @@  + net8.0 Library - - - - - - @@ -22,4 +17,18 @@ + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/Backblaze/B2.cs b/Duplicati/Library/Backend/Backblaze/B2.cs index 1078ea46e..2673d7116 100644 --- a/Duplicati/Library/Backend/Backblaze/B2.cs +++ b/Duplicati/Library/Backend/Backblaze/B2.cs @@ -27,6 +27,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; @@ -212,8 +213,9 @@ namespace Duplicati.Library.Backend.Backblaze { var p = measure.Position; - using(var hashalg = HashAlgorithmHelper.Create("sha1")) - sha1 = Utility.Utility.ByteArrayAsHexString(hashalg.ComputeHash(measure)); + // Compute the hash + using(var hashalg = SHA1.Create()) + sha1 = Library.Utility.Utility.ByteArrayAsHexString(hashalg.ComputeHash(measure)); measure.Position = p; } @@ -222,7 +224,8 @@ namespace Duplicati.Library.Backend.Backblaze // No seeking possible, use a temp file tmp = new TempFile(); using(var sr = System.IO.File.OpenWrite(tmp)) - using(var hc = new HashCalculatingStream(measure, "sha1")) + using(var hasher = HashFactory.CreateHasher("SHA1")) + using(var hc = new HashCalculatingStream(measure, hasher)) { await Utility.Utility.CopyStreamAsync(hc, sr, cancelToken).ConfigureAwait(false); sha1 = hc.GetFinalHashString(); diff --git a/Duplicati/Library/Backend/Backblaze/Duplicati.Library.Backend.Backblaze.csproj b/Duplicati/Library/Backend/Backblaze/Duplicati.Library.Backend.Backblaze.csproj index 86676f436..1064a35b5 100644 --- a/Duplicati/Library/Backend/Backblaze/Duplicati.Library.Backend.Backblaze.csproj +++ b/Duplicati/Library/Backend/Backblaze/Duplicati.Library.Backend.Backblaze.csproj @@ -1,16 +1,25 @@  + net8.0 Library - - - - - - - + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/Box/Duplicati.Library.Backend.Box.csproj b/Duplicati/Library/Backend/Box/Duplicati.Library.Backend.Box.csproj index dc7db0624..e2062d2f5 100644 --- a/Duplicati/Library/Backend/Box/Duplicati.Library.Backend.Box.csproj +++ b/Duplicati/Library/Backend/Box/Duplicati.Library.Backend.Box.csproj @@ -1,17 +1,27 @@  + net8.0 Library + + + + + - - + + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/CloudFiles/CloudFiles.cs b/Duplicati/Library/Backend/CloudFiles/CloudFiles.cs index 2a76d01ad..a5612268a 100644 --- a/Duplicati/Library/Backend/CloudFiles/CloudFiles.cs +++ b/Duplicati/Library/Backend/CloudFiles/CloudFiles.cs @@ -24,6 +24,7 @@ using Duplicati.Library.Interface; using System; using System.Collections.Generic; using System.Net; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; @@ -301,7 +302,8 @@ namespace Duplicati.Library.Backend var areq = new Utility.AsyncHttpRequest(req); using (var resp = areq.GetResponse()) using (var s = areq.GetResponseStream()) - using (var mds = new Utility.MD5CalculatingStream(s)) + using (var hasher = MD5.Create()) + using (var mds = new Utility.HashCalculatingStream(s, hasher)) { string md5Hash = resp.Headers["ETag"]; Utility.Utility.CopyStream(mds, stream, true, m_copybuffer); @@ -353,7 +355,8 @@ namespace Duplicati.Library.Backend Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req); using (System.IO.Stream s = areq.GetRequestStream(streamLen)) - using (var mds = new Utility.MD5CalculatingStream(s)) + using (var hasher = MD5.Create()) + using (var mds = new Utility.HashCalculatingStream(s, hasher)) { await Utility.Utility.CopyStreamAsync(stream, mds, tryRewindSource: true, cancelToken: cancelToken); fileHash = mds.GetFinalHashString(); diff --git a/Duplicati/Library/Backend/CloudFiles/Duplicati.Library.Backend.CloudFiles.csproj b/Duplicati/Library/Backend/CloudFiles/Duplicati.Library.Backend.CloudFiles.csproj index 857ecb7d8..439448c3a 100644 --- a/Duplicati/Library/Backend/CloudFiles/Duplicati.Library.Backend.CloudFiles.csproj +++ b/Duplicati/Library/Backend/CloudFiles/Duplicati.Library.Backend.CloudFiles.csproj @@ -1,13 +1,22 @@  + net8.0 Library Duplicati.Library.Backend + - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/Dropbox/Duplicati.Library.Backend.Dropbox.csproj b/Duplicati/Library/Backend/Dropbox/Duplicati.Library.Backend.Dropbox.csproj index 4ca51c492..75b9c103b 100644 --- a/Duplicati/Library/Backend/Dropbox/Duplicati.Library.Backend.Dropbox.csproj +++ b/Duplicati/Library/Backend/Dropbox/Duplicati.Library.Backend.Dropbox.csproj @@ -1,20 +1,29 @@  + - Library - false + net8.0 + Dropbox backend for Duplicati net8.0 - - - - - - - + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/FTP/Duplicati.Library.Backend.FTP.csproj b/Duplicati/Library/Backend/FTP/Duplicati.Library.Backend.FTP.csproj index 3462d3cff..c226de937 100644 --- a/Duplicati/Library/Backend/FTP/Duplicati.Library.Backend.FTP.csproj +++ b/Duplicati/Library/Backend/FTP/Duplicati.Library.Backend.FTP.csproj @@ -1,13 +1,22 @@  + net8.0 Library + - - - + + - \ No newline at end of file + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Duplicati/Library/Backend/File/Duplicati.Library.Backend.File.csproj b/Duplicati/Library/Backend/File/Duplicati.Library.Backend.File.csproj index 857ecb7d8..eff2fbe41 100644 --- a/Duplicati/Library/Backend/File/Duplicati.Library.Backend.File.csproj +++ b/Duplicati/Library/Backend/File/Duplicati.Library.Backend.File.csproj @@ -1,13 +1,22 @@  + net8.0 Library Duplicati.Library.Backend + - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/GoogleServices/Duplicati.Library.Backend.GoogleServices.csproj b/Duplicati/Library/Backend/GoogleServices/Duplicati.Library.Backend.GoogleServices.csproj index 3c1f56d6a..e69770ba4 100644 --- a/Duplicati/Library/Backend/GoogleServices/Duplicati.Library.Backend.GoogleServices.csproj +++ b/Duplicati/Library/Backend/GoogleServices/Duplicati.Library.Backend.GoogleServices.csproj @@ -1,16 +1,25 @@  + net8.0 Library - - - - - - - + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/Idrivee2/Duplicati.Library.Backend.Idrivee2.csproj b/Duplicati/Library/Backend/Idrivee2/Duplicati.Library.Backend.Idrivee2.csproj index 53b1620b6..39d0e2859 100644 --- a/Duplicati/Library/Backend/Idrivee2/Duplicati.Library.Backend.Idrivee2.csproj +++ b/Duplicati/Library/Backend/Idrivee2/Duplicati.Library.Backend.Idrivee2.csproj @@ -1,25 +1,23 @@  + net8.0 Library Duplicati.Library.Backend + - - - + + + - - - - - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - \ No newline at end of file + diff --git a/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj b/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj index 81f4e7595..e7d7f37b9 100644 --- a/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj +++ b/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj @@ -1,18 +1,23 @@  + net8.0 Library Duplicati.Library.Backend + - - - - + + + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - \ No newline at end of file + diff --git a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs index 3636bcb79..a633cfba3 100644 --- a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs +++ b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs @@ -25,6 +25,7 @@ using Duplicati.Library.Localization.Short; using System; using System.Collections.Generic; using System.Net; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; namespace Duplicati.Library.Backend @@ -472,7 +473,8 @@ namespace Duplicati.Library.Backend // No seeking possible, use a temp file tmpFile = new Duplicati.Library.Utility.TempFile(); using (var os = System.IO.File.OpenWrite(tmpFile)) - using (var md5 = new Utility.MD5CalculatingStream(baseStream)) + using(var hasher = MD5.Create()) + using (var md5 = new Utility.HashCalculatingStream(baseStream, hasher)) { await Utility.Utility.CopyStreamAsync(md5, os, true, cancelToken, m_copybuffer); md5Hash = md5.GetFinalHashString(); diff --git a/Duplicati/Library/Backend/Mega/Duplicati.Library.Backend.Mega.csproj b/Duplicati/Library/Backend/Mega/Duplicati.Library.Backend.Mega.csproj index d0baf8638..cc383aeaa 100644 --- a/Duplicati/Library/Backend/Mega/Duplicati.Library.Backend.Mega.csproj +++ b/Duplicati/Library/Backend/Mega/Duplicati.Library.Backend.Mega.csproj @@ -1,18 +1,27 @@  + net8.0 Library - - - - - - + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/OAuthHelper/Duplicati.Library.OAuthHelper.csproj b/Duplicati/Library/Backend/OAuthHelper/Duplicati.Library.OAuthHelper.csproj index 17b0bf1d8..62cfaff47 100644 --- a/Duplicati/Library/Backend/OAuthHelper/Duplicati.Library.OAuthHelper.csproj +++ b/Duplicati/Library/Backend/OAuthHelper/Duplicati.Library.OAuthHelper.csproj @@ -1,15 +1,24 @@  + net8.0 Library - - - - - - - \ No newline at end of file + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Duplicati/Library/Backend/OAuthHelper/OAuthHttpClient.cs b/Duplicati/Library/Backend/OAuthHelper/OAuthHttpClient.cs index 8e160d455..11b7e8ca6 100644 --- a/Duplicati/Library/Backend/OAuthHelper/OAuthHttpClient.cs +++ b/Duplicati/Library/Backend/OAuthHelper/OAuthHttpClient.cs @@ -148,6 +148,7 @@ namespace Duplicati.Library { OAuthHttpMessageHandler handler = new OAuthHttpMessageHandler(authid, protocolKey); + /* TODO-DNC - not supported https://github.com/dotnet/corefx/issues/26223 // Set the read/write timeout if (HttpContextSettings.ReadWriteTimeout > TimeSpan.Zero) { @@ -161,7 +162,7 @@ namespace Duplicati.Library // TODO: This is no longer supported, the validation can now be done pr. connection as it should always have been // handler.ServerCertificateValidationCallback = HttpContextSettings.CertificateValidator.ValidateServerCertficate; } - + */ return handler; } } diff --git a/Duplicati/Library/Backend/OneDrive/Duplicati.Library.Backend.OneDrive.csproj b/Duplicati/Library/Backend/OneDrive/Duplicati.Library.Backend.OneDrive.csproj index 5498ef908..146701e93 100644 --- a/Duplicati/Library/Backend/OneDrive/Duplicati.Library.Backend.OneDrive.csproj +++ b/Duplicati/Library/Backend/OneDrive/Duplicati.Library.Backend.OneDrive.csproj @@ -1,21 +1,20 @@  + net8.0 Library Duplicati.Library.Backend - - - - - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/OneDrive/OneDrive.cs b/Duplicati/Library/Backend/OneDrive/OneDrive.cs deleted file mode 100644 index bb948cd18..000000000 --- a/Duplicati/Library/Backend/OneDrive/OneDrive.cs +++ /dev/null @@ -1,693 +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.Text; -using System.Text.RegularExpressions; -using System.Net; -using Duplicati.Library.Interface; -using Duplicati.Library.Common.IO; - -namespace Duplicati.Library.Backend -{ - public class OneDrive : IBackend, IStreamingBackend, IQuotaEnabledBackend, IRenameEnabledBackend - { - private static readonly string LOGTAG = Logging.Log.LogTagFromType(); - - private const string SERVICES_AGREEMENT = "https://www.microsoft.com/en-us/servicesagreement"; - private const string PRIVACY_STATEMENT = "https://privacy.microsoft.com/en-us/privacystatement"; - - private const string AUTHID_OPTION = "authid"; - - private const string WLID_SERVER = "https://apis.live.net/v5.0"; - private const string ROOT_FOLDER_ID = "me/skydrive"; - private const string FOLDER_TEMPLATE = "{0}/files"; - private const string ONEDRIVE_SERVICE_URL = "https://api.onedrive.com/v1.0"; - - private const int FILE_LIST_PAGE_SIZE = 100; - - private const long BITS_FILE_SIZE_LIMIT = 1024 * 1024 * 15; - private const long BITS_CHUNK_SIZE = 1024 * 1024 * 10; - - private static readonly string USER_AGENT = string.Format("Duplicati v{0}", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version); - - private readonly string m_rootfolder; - private readonly string m_prefix; - private string m_userid; - private WLID_FolderItem m_folderid; - - private readonly OAuthHelper m_oauth; - - private readonly Dictionary m_fileidCache = new Dictionary(StringComparer.OrdinalIgnoreCase); - - private readonly byte[] m_copybuffer = new byte[Duplicati.Library.Utility.Utility.DEFAULT_BUFFER_SIZE]; - - public OneDrive() { } - - public OneDrive(string url, Dictionary options) - { - var uri = new Utility.Uri(url); - - m_rootfolder = uri.Host; - m_prefix = Util.AppendDirSeparator("/" + uri.Path, "/"); - - string authid = null; - if (options.ContainsKey(AUTHID_OPTION)) - authid = options[AUTHID_OPTION]; - - m_oauth = new OAuthHelper(authid, this.ProtocolKey); - } - - private class WLID_DataItem - { - public WLID_FolderItem[] data { get; set; } - } - - private class WLID_FolderItem - { - public string id { get; set; } - public string name { get; set; } - public string description { get; set; } - public string parent_id { get; set; } - public string upload_location { get; set; } - public string type { get; set; } - [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] - public DateTime? created_time { get; set; } - [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] - public DateTime? updated_time { get; set; } - [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] - public long? size { get; set; } - } - - private class WLID_CreateFolderData - { - public string name; - public string description; - } - - private class WLID_UserInfo - { - public string id { get; set; } - public string first_name { get; set; } - public string last_name { get; set; } - public string name { get; set; } - public string gender { get; set; } - public string locale { get; set; } - } - - private class WLID_QuotaInfo - { - [Newtonsoft.Json.JsonProperty("quota")] - public long? Quota { get; set; } - [Newtonsoft.Json.JsonProperty("available")] - public long? Available { get; set; } - } - - private WLID_FolderItem FindFolder(string folder, string parentfolder = null) - { - if (string.IsNullOrWhiteSpace(parentfolder)) - parentfolder = ROOT_FOLDER_ID; - - var url = string.Format("{0}/{1}?access_token={2}", WLID_SERVER, string.Format(FOLDER_TEMPLATE, parentfolder), Library.Utility.Uri.UrlEncode(m_oauth.AccessToken)); - var res = m_oauth.GetJSONData(url, x => x.UserAgent = USER_AGENT); - - if (res == null || res.data == null) - return null; - - foreach(var r in res.data) - if (string.Equals(r.name, folder, StringComparison.OrdinalIgnoreCase)) - return r; - - return null; - } - - private WLID_FolderItem FindFolders(bool autocreate) - { - var folders = (m_rootfolder + '/' + m_prefix).Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - if (folders.Length == 0) - { - var url = string.Format("{0}/{1}?access_token={2}", WLID_SERVER, ROOT_FOLDER_ID, Library.Utility.Uri.UrlEncode(m_oauth.AccessToken)); - return m_oauth.GetJSONData(url, x => x.UserAgent = USER_AGENT); - } - - WLID_FolderItem cur = null; - foreach (var f in folders) - { - var n = FindFolder(f, cur == null ? null : cur.id); - if (n == null) - { - if (autocreate) - { - var url = string.Format("{0}/{1}?access_token={2}", WLID_SERVER, cur == null ? ROOT_FOLDER_ID : cur.id, Library.Utility.Uri.UrlEncode(m_oauth.AccessToken)); - var req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = USER_AGENT; - req.Method = "POST"; - - var areq = new Utility.AsyncHttpRequest(req); - - using (var ms = new System.IO.MemoryStream()) - using (var sw = new System.IO.StreamWriter(ms)) - { - new Newtonsoft.Json.JsonSerializer().Serialize(sw, new WLID_CreateFolderData() { - name = f, - description = Strings.OneDrive.AutoCreatedFolderLabel - }); - - sw.Flush(); - ms.Position = 0; - - req.ContentLength = ms.Length; - req.ContentType = "application/json"; - - using (var reqs = areq.GetRequestStream()) - Utility.Utility.CopyStream(ms, reqs, true, m_copybuffer); - } - - using (var resp = (HttpWebResponse)areq.GetResponse()) - using (var rs = areq.GetResponseStream()) - using (var tr = new System.IO.StreamReader(rs)) - using (var jr = new Newtonsoft.Json.JsonTextReader(tr)) - { - if ((int)resp.StatusCode < 200 || (int)resp.StatusCode > 299) - throw new ProtocolViolationException(Strings.OneDrive.UnexpectedError(resp.StatusCode, resp.StatusDescription)); - cur = new Newtonsoft.Json.JsonSerializer().Deserialize(jr); - } - } - else - throw new FolderMissingException(Strings.OneDrive.MissingFolderError(f)); - } - else - cur = n; - } - - return cur; - } - - private string FolderID - { - get - { - if (m_folderid == null) - m_folderid = FindFolders(false); - - return m_folderid.id; - } - } - - private string GetFileID(string name, bool throwIfMissing = true) - { - string id; - m_fileidCache.TryGetValue(name, out id); - if (string.IsNullOrWhiteSpace(id)) - { - // Refresh the list of files, just in case - foreach (IFileEntry file in List()) { /* We just need to iterate the whole list */ } - m_fileidCache.TryGetValue(name, out id); - - if (string.IsNullOrWhiteSpace(id) && throwIfMissing) - throw new FileMissingException(Strings.OneDrive.FileNotFoundError(name)); - } - - return id; - } - - private WLID_QuotaInfo GetQuotaInfo() - { - var url = string.Format("{0}/me/skydrive/quota?access_token={1}", WLID_SERVER, Library.Utility.Uri.UrlEncode(m_oauth.AccessToken)); - return m_oauth.GetJSONData(url, x => x.UserAgent = USER_AGENT); - } - - #region IBackend Members - - public void Test() - { - this.TestList(); - } - - public void CreateFolder() - { - FindFolders(true); - } - - public string DisplayName - { - get { return Strings.OneDrive.DisplayName; } - } - - public string ProtocolKey - { - get { return "onedrive"; } - } - - public IEnumerable List() - { - int offset = 0; - int count = FILE_LIST_PAGE_SIZE; - int numFiles = 0; - int filesOk = 0; - int filesRepeated = 0; - int iteration = 0; - - var files = new List(); - - m_fileidCache.Clear(); - - do - { - - while (count == FILE_LIST_PAGE_SIZE) - { - var url = string.Format("{0}/{1}?access_token={2}&limit={3}&offset={4}", WLID_SERVER, string.Format(FOLDER_TEMPLATE, FolderID), Library.Utility.Uri.UrlEncode(m_oauth.AccessToken), FILE_LIST_PAGE_SIZE, offset); - var res = m_oauth.GetJSONData(url); - - if (res != null && res.data != null) - { - count = res.data.Length; - - // log - Library.Logging.Log.WriteProfilingMessage(LOGTAG, "OneDriveListStats", "Iteration: {0:D} Offset: {1:D} Count: {2:D} TotalOK: {3:D} TotalRep: {4:D} TotalFiles: {5:D}", iteration, offset, count, filesOk, filesRepeated, numFiles); - - foreach (var r in res.data) - { - - if (m_fileidCache.ContainsKey(r.name)) - { - filesRepeated++; - } - else - { - m_fileidCache.Add(r.name, r.id); - - var fe = new FileEntry(r.name, r.size.Value, r.updated_time.Value, r.updated_time.Value); - fe.IsFolder = string.Equals(r.type, "folder", StringComparison.OrdinalIgnoreCase); - files.Add(fe); - - filesOk++; - } - } - } - else - { - count = 0; - } - - if (iteration != 0 && filesOk == numFiles) return files; - - offset += count; - - } - - // Save total number of files in the first iteration - if (iteration == 0) - { - numFiles = offset; - } - - filesRepeated = 0; - iteration++; - - offset = 0; - count = FILE_LIST_PAGE_SIZE; - } - while (filesOk != numFiles); - - return files; - } - - public void Put(string remotename, string filename) - { - using (System.IO.FileStream fs = System.IO.File.OpenRead(filename)) - Put(remotename, fs); - } - - public void Get(string remotename, string filename) - { - using (System.IO.FileStream fs = System.IO.File.Create(filename)) - Get(remotename, fs); - } - - public void Delete(string remotename) - { - try - { - var id = GetFileID(remotename); - var url = string.Format("{0}/{1}?access_token={2}", WLID_SERVER, id, Library.Utility.Uri.UrlEncode(m_oauth.AccessToken)); - var req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = USER_AGENT; - req.Method = "DELETE"; - - var areq = new Utility.AsyncHttpRequest(req); - using (var resp = (HttpWebResponse)areq.GetResponse()) - { - if (resp.StatusCode == System.Net.HttpStatusCode.NotFound) - throw new FileMissingException(); - - if ((int)resp.StatusCode < 200 || (int)resp.StatusCode > 299) - throw new ProtocolViolationException(Strings.OneDrive.UnexpectedError(resp.StatusCode, resp.StatusDescription)); - m_fileidCache.Remove(remotename); - } - } - catch (System.Net.WebException wex) - { - if (wex.Response is System.Net.HttpWebResponse && ((System.Net.HttpWebResponse)wex.Response).StatusCode == System.Net.HttpStatusCode.NotFound) - throw new FileMissingException(wex); - else - throw; - } - } - - - public IList SupportedCommands - { - get { - return new List(new ICommandLineArgument[] { - new CommandLineArgument(AUTHID_OPTION, CommandLineArgument.ArgumentType.Password, Strings.OneDrive.AuthidShort, Strings.OneDrive.AuthidLong(OAuthHelper.OAUTH_LOGIN_URL("onedrive"))), - }); - } - } - - public string Description - { - get { return Strings.OneDrive.Description( - "Microsoft Service Agreement", - SERVICES_AGREEMENT, - "Microsoft Online Privacy Statement", - PRIVACY_STATEMENT - ); - } - } - - #endregion - - #region IDisposable Members - - public void Dispose() - { - } - - #endregion - - private string UserID - { - get - { - if (string.IsNullOrEmpty(m_userid)) - { - var url = string.Format("{0}/me?access_token={1}", WLID_SERVER, Library.Utility.Uri.UrlEncode(m_oauth.AccessToken)); - var req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = USER_AGENT; - var areq = new Utility.AsyncHttpRequest(req); - - using(var resp = (HttpWebResponse)areq.GetResponse()) - using(var rs = areq.GetResponseStream()) - using(var tr = new System.IO.StreamReader(rs)) - using(var jr = new Newtonsoft.Json.JsonTextReader(tr)) - m_userid = new Newtonsoft.Json.JsonSerializer().Deserialize(jr).id; - } - - return m_userid; - } - } - - #region IRenameEnabledBackend - - public void Rename(string oldname, string newname) - { - try - { - try - { - var id = GetFileID(oldname); - var url = string.Format("{0}/{1}?access_token={2}", WLID_SERVER, id, Library.Utility.Uri.UrlEncode(m_oauth.AccessToken)); - var req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = USER_AGENT; - req.Method = "PUT"; - - var updateData = new WLID_FolderItem() { name = newname }; - var data = System.Text.Encoding.UTF8.GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(updateData)); - req.ContentLength = data.Length; - req.ContentType = "application/json; charset=UTF-8"; - using (var requestStream = req.GetRequestStream()) - requestStream.Write(data, 0, data.Length); - - var areq = new Utility.AsyncHttpRequest(req); - using (var resp = (HttpWebResponse)areq.GetResponse()) - { - if (resp.StatusCode == System.Net.HttpStatusCode.NotFound) - throw new FileMissingException(); - - if ((int)resp.StatusCode < 200 || (int)resp.StatusCode > 299) - throw new ProtocolViolationException(Strings.OneDrive.UnexpectedError(resp.StatusCode, resp.StatusDescription)); - - m_fileidCache[newname] = id; - m_fileidCache.Remove(oldname); - } - } - catch - { - // Since we don't know the state of file IDs, clear the cache - m_fileidCache.Clear(); - - throw; - } - } - catch (System.Net.WebException wex) - { - if (wex.Response is System.Net.HttpWebResponse && ((System.Net.HttpWebResponse)wex.Response).StatusCode == System.Net.HttpStatusCode.NotFound) - throw new FileMissingException(wex); - else - throw; - } - } - - #endregion - - #region IQuotaEnabledBackend Members - - public IQuotaInfo Quota - { - get - { - WLID_QuotaInfo quota = this.GetQuotaInfo(); - return new QuotaInfo(quota.Quota ?? -1, quota.Available ?? -1); - } - } - - public long TotalQuotaSpace - { - get - { - return this.GetQuotaInfo().Quota ?? -1; - } - } - - public long FreeQuotaSpace - { - get - { - return this.GetQuotaInfo().Available ?? -1; - } - } - - public string[] DNSName - { - get { return new string[] { new Uri(WLID_SERVER).Host, new Uri(ONEDRIVE_SERVICE_URL).Host, string.IsNullOrWhiteSpace(m_userid) ? null : string.Format("cid-{0}.users.storage.live.com", m_userid) }; } - } - - #endregion - - #region IStreamingBackend Members - - public void Put(string remotename, System.IO.Stream stream) - { - if (stream.Length > BITS_FILE_SIZE_LIMIT) - { - // Get extra info for BITS - var uid = UserID; - var fid = FolderID.Split('.')[2]; - - // Create a session - var url = string.Format("https://cid-{0}.users.storage.live.com/items/{1}/{2}?access_token={3}", uid, fid, Utility.Uri.UrlPathEncode(remotename), Library.Utility.Uri.UrlEncode(m_oauth.AccessToken)); - - var req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = USER_AGENT; - req.Method = "POST"; - req.ContentType = "application/json"; - - req.Headers.Add("X-Http-Method-Override", "BITS_POST"); - req.Headers.Add("BITS-Packet-Type", "Create-Session"); - req.Headers.Add("BITS-Supported-Protocols", "{7df0354d-249b-430f-820d-3d2a9bef4931}"); - req.ContentLength = 0; - - var areq = new Utility.AsyncHttpRequest(req); - - string sessionid; - - using(var resp = (HttpWebResponse)areq.GetResponse()) - { - var packtype = resp.Headers["BITS-Packet-Type"]; - if (!packtype.Equals("Ack", StringComparison.OrdinalIgnoreCase)) - throw new Exception(string.Format("Unable to create BITS transfer, got status: {0}", packtype)); - - sessionid = resp.Headers["BITS-Session-Id"]; - } - - if (string.IsNullOrEmpty(sessionid)) - throw new Exception("BITS session-id was missing"); - - // Session is now created, start uploading chunks - - var offset = 0L; - var retries = 0; - - while (offset < stream.Length) - { - try - { - var bytesInChunk = Math.Min(BITS_CHUNK_SIZE, stream.Length - offset); - - req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = USER_AGENT; - req.Method = "POST"; - req.Headers.Add("X-Http-Method-Override", "BITS_POST"); - req.Headers.Add("BITS-Packet-Type", "Fragment"); - req.Headers.Add("BITS-Session-Id", sessionid); - req.Headers.Add("Content-Range", string.Format("bytes {0}-{1}/{2}", offset, offset + bytesInChunk - 1, stream.Length)); - - req.ContentLength = bytesInChunk; - - if (stream.Position != offset) - stream.Position = offset; - - areq = new Utility.AsyncHttpRequest(req); - var remaining = (int)bytesInChunk; - using(var reqs = areq.GetRequestStream()) - { - int read; - while ((read = stream.Read(m_copybuffer, 0, Math.Min(m_copybuffer.Length, remaining))) != 0) - { - reqs.Write(m_copybuffer, 0, read); - remaining -= read; - } - } - - using(var resp = (HttpWebResponse)areq.GetResponse()) - { - if (resp.StatusCode != HttpStatusCode.OK) - throw new WebException("Invalid partial upload response", null, WebExceptionStatus.UnknownError, resp); - } - - offset += bytesInChunk; - retries = 0; - } - catch (Exception ex) - { - var retry = false; - - // If we get a 5xx error, or some network issue, we retry - if (ex is WebException && ((WebException)ex).Response is HttpWebResponse) - { - var code = (int)((HttpWebResponse)((WebException)ex).Response).StatusCode; - retry = code >= 500 && code <= 599; - } - else if (ex is System.Net.Sockets.SocketException || ex is System.IO.IOException || ex.InnerException is System.Net.Sockets.SocketException || ex.InnerException is System.IO.IOException) - { - retry = true; - } - - - // Retry with exponential backoff - if (retry && retries < 5) - { - System.Threading.Thread.Sleep(TimeSpan.FromSeconds(Math.Pow(2, retries))); - retries++; - } - else - throw; - } - } - - // Transfer completed, now commit the upload and close the session - - req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = USER_AGENT; - req.Method = "POST"; - req.Headers.Add("X-Http-Method-Override", "BITS_POST"); - req.Headers.Add("BITS-Packet-Type", "Close-Session"); - req.Headers.Add("BITS-Session-Id", sessionid); - req.ContentLength = 0; - - areq = new Utility.AsyncHttpRequest(req); - using(var resp = (HttpWebResponse)areq.GetResponse()) - { - if (resp.StatusCode != HttpStatusCode.OK) - throw new Exception("Invalid partial upload commit response"); - } - } - else - { - var url = string.Format("{0}/{1}/files/{2}?access_token={3}", WLID_SERVER, FolderID, Utility.Uri.UrlPathEncode(remotename), Library.Utility.Uri.UrlEncode(m_oauth.AccessToken)); - var req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = USER_AGENT; - req.Method = "PUT"; - - try - { - req.ContentLength = stream.Length; - } - catch - { - } - - // Docs says not to set this ? - //req.ContentType = "application/octet-stream"; - - var areq = new Utility.AsyncHttpRequest(req); - using(var reqs = areq.GetRequestStream()) - Utility.Utility.CopyStream(stream, reqs, true, m_copybuffer); - - using(var resp = (HttpWebResponse)areq.GetResponse()) - using(var rs = areq.GetResponseStream()) - using(var tr = new System.IO.StreamReader(rs)) - using(var jr = new Newtonsoft.Json.JsonTextReader(tr)) - { - var nf = new Newtonsoft.Json.JsonSerializer().Deserialize(jr); - m_fileidCache[remotename] = nf.id; - } - } - } - - public void Get(string remotename, System.IO.Stream stream) - { - var id = GetFileID(remotename); - var url = string.Format("{0}/{1}/content?access_token={2}", WLID_SERVER, id, Library.Utility.Uri.UrlEncode(m_oauth.AccessToken)); - var req = (HttpWebRequest)WebRequest.Create(url); - req.UserAgent = USER_AGENT; - - var areq = new Utility.AsyncHttpRequest(req); - using (var resp = (HttpWebResponse)areq.GetResponse()) - using (var rs = areq.GetResponseStream()) - Utility.Utility.CopyStream(rs, stream, true, m_copybuffer); - } - - #endregion - } -} diff --git a/Duplicati/Library/Backend/OpenStack/Duplicati.Library.Backend.OpenStack.csproj b/Duplicati/Library/Backend/OpenStack/Duplicati.Library.Backend.OpenStack.csproj index ce72d4b34..e69770ba4 100644 --- a/Duplicati/Library/Backend/OpenStack/Duplicati.Library.Backend.OpenStack.csproj +++ b/Duplicati/Library/Backend/OpenStack/Duplicati.Library.Backend.OpenStack.csproj @@ -1,16 +1,25 @@  + net8.0 Library - - - - - - - + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/Rclone/Duplicati.Library.Backend.Rclone.csproj b/Duplicati/Library/Backend/Rclone/Duplicati.Library.Backend.Rclone.csproj index 1a6069445..64849a608 100644 --- a/Duplicati/Library/Backend/Rclone/Duplicati.Library.Backend.Rclone.csproj +++ b/Duplicati/Library/Backend/Rclone/Duplicati.Library.Backend.Rclone.csproj @@ -1,15 +1,24 @@  + net8.0 Library - - - - - - + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/S3/Duplicati.Library.Backend.S3.csproj b/Duplicati/Library/Backend/S3/Duplicati.Library.Backend.S3.csproj index 5be8b8694..8f251ca78 100644 --- a/Duplicati/Library/Backend/S3/Duplicati.Library.Backend.S3.csproj +++ b/Duplicati/Library/Backend/S3/Duplicati.Library.Backend.S3.csproj @@ -1,29 +1,31 @@  + net8.0 Library Duplicati.Library.Backend + + + + + + + + + - + - + - - - - - - - - - - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/S3/S3AwsClient.cs b/Duplicati/Library/Backend/S3/S3AwsClient.cs index 54a871668..f3bd4fc53 100644 --- a/Duplicati/Library/Backend/S3/S3AwsClient.cs +++ b/Duplicati/Library/Backend/S3/S3AwsClient.cs @@ -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 Amazon.S3; @@ -49,12 +49,9 @@ namespace Duplicati.Library.Backend public S3AwsClient(string awsID, string awsKey, string locationConstraint, string servername, string storageClass, bool useSSL, bool disableChunkEncoding, Dictionary options) { - var cfg = new AmazonS3Config - { - UseHttp = !useSSL, - ServiceURL = (useSSL ? "https://" : "http://") + servername, - BufferSize = (int) Utility.Utility.DEFAULT_BUFFER_SIZE, - }; + var cfg = S3AwsClient.GetDefaultAmazonS3Config(); + cfg.UseHttp = !useSSL; + cfg.ServiceURL = (useSSL ? "https://" : "http://") + servername; foreach (var opt in options.Keys.Where(x => x.StartsWith("s3-ext-", StringComparison.OrdinalIgnoreCase))) { @@ -99,6 +96,20 @@ namespace Duplicati.Library.Backend m_client.PutBucketAsync(request).GetAwaiter().GetResult(); } + internal static AmazonS3Config GetDefaultAmazonS3Config() + { + return new AmazonS3Config() + { + BufferSize = (int) Utility.Utility.DEFAULT_BUFFER_SIZE, + + // If this is not set, accessing the property will trigger an expensive operation (~30 seconds) + // to get the region endpoint. The use of ARNs (Amazon Resource Names) doesn't appear to be + // critical for our usages. + // See: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html + UseArnRegion = false, + }; + } + public virtual void GetFileStream(string bucketName, string keyName, System.IO.Stream target) { var objectGetRequest = new GetObjectRequest @@ -110,14 +121,8 @@ namespace Duplicati.Library.Backend using (GetObjectResponse objectGetResponse = m_client.GetObjectAsync(objectGetRequest).GetAwaiter().GetResult()) using (System.IO.Stream s = objectGetResponse.ResponseStream) { - try - { - s.ReadTimeout = (int) TimeSpan.FromMinutes(1).TotalMilliseconds; - } - catch - { - // We don't care about this timeout - } + try { s.ReadTimeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds; } + catch { } Utility.Utility.CopyStream(s, target); } diff --git a/Duplicati/Library/Backend/S3/S3Backend.cs b/Duplicati/Library/Backend/S3/S3Backend.cs index 1a9e0e88e..65faaa29f 100644 --- a/Duplicati/Library/Backend/S3/S3Backend.cs +++ b/Duplicati/Library/Backend/S3/S3Backend.cs @@ -393,7 +393,7 @@ namespace Duplicati.Library.Backend foreach (var s in KNOWN_S3_LOCATIONS) locations.AppendLine(string.Format("{0}: {1}", s.Key, s.Value)); - var defaults = new Amazon.S3.AmazonS3Config(); + var defaults = S3AwsClient.GetDefaultAmazonS3Config(); var exts = typeof(Amazon.S3.AmazonS3Config).GetProperties().Where(x => x.CanRead && x.CanWrite && (x.PropertyType == typeof(string) || x.PropertyType == typeof(bool) || x.PropertyType == typeof(int) || x.PropertyType == typeof(long) || x.PropertyType.IsEnum)) diff --git a/Duplicati/Library/Backend/S3/S3IAM.cs b/Duplicati/Library/Backend/S3/S3IAM.cs index edf09b37f..7c34568ad 100644 --- a/Duplicati/Library/Backend/S3/S3IAM.cs +++ b/Duplicati/Library/Backend/S3/S3IAM.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -159,7 +159,6 @@ namespace Duplicati.Library.Backend .GetAwaiter().GetResult() .EvaluationResults.First() .EvalDecision == PolicyEvaluationDecisionType.Allowed; - } private static IDictionary GetCreateUserDict(User user, AmazonIdentityManagementServiceClient cl) diff --git a/Duplicati/Library/Backend/SSHv2/Duplicati.Library.Backend.SSHv2.csproj b/Duplicati/Library/Backend/SSHv2/Duplicati.Library.Backend.SSHv2.csproj index 6320bf54d..1aac8b1c2 100644 --- a/Duplicati/Library/Backend/SSHv2/Duplicati.Library.Backend.SSHv2.csproj +++ b/Duplicati/Library/Backend/SSHv2/Duplicati.Library.Backend.SSHv2.csproj @@ -1,16 +1,27 @@  + net8.0 Library Duplicati.Library.Backend - - - - - - + + - \ No newline at end of file + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Duplicati/Library/Backend/SharePoint/Duplicati.Library.Backend.SharePoint.csproj b/Duplicati/Library/Backend/SharePoint/Duplicati.Library.Backend.SharePoint.csproj index 75333ff8a..d289fc24a 100644 --- a/Duplicati/Library/Backend/SharePoint/Duplicati.Library.Backend.SharePoint.csproj +++ b/Duplicati/Library/Backend/SharePoint/Duplicati.Library.Backend.SharePoint.csproj @@ -1,4 +1,5 @@  + net8.0 Library @@ -7,17 +8,21 @@ - - - - - - + + ..\..\..\..\thirdparty\SharePointPnP-Sites-Core\SharePointPnP.Client.dll + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/Sia/Duplicati.Library.Backend.Sia.csproj b/Duplicati/Library/Backend/Sia/Duplicati.Library.Backend.Sia.csproj index 86676f436..e69770ba4 100644 --- a/Duplicati/Library/Backend/Sia/Duplicati.Library.Backend.Sia.csproj +++ b/Duplicati/Library/Backend/Sia/Duplicati.Library.Backend.Sia.csproj @@ -1,16 +1,25 @@  + net8.0 Library - - - - - - - + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/Storj/Duplicati.Library.Backend.Storj.csproj b/Duplicati/Library/Backend/Storj/Duplicati.Library.Backend.Storj.csproj index 4df583966..260f36447 100644 --- a/Duplicati/Library/Backend/Storj/Duplicati.Library.Backend.Storj.csproj +++ b/Duplicati/Library/Backend/Storj/Duplicati.Library.Backend.Storj.csproj @@ -1,40 +1,27 @@  + net8.0 Library - - - x64 - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - - - PreserveNewest - - - PreserveNewest - - - - - - - - - - - - \ No newline at end of file + Storj backend for Duplicati + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Duplicati/Library/Backend/Storj/Strings.cs b/Duplicati/Library/Backend/Storj/Strings.cs index 48b57b26d..bb8931dfa 100644 --- a/Duplicati/Library/Backend/Storj/Strings.cs +++ b/Duplicati/Library/Backend/Storj/Strings.cs @@ -27,7 +27,7 @@ using System.Threading.Tasks; namespace Duplicati.Library.Backend.Strings { - internal static class Storj + public static class Storj { public static string DisplayName { get { return LC.L(@"Storj DCS (Decentralized Cloud Storage)"); } } public static string Description { get { return LC.L(@"This backend can read and write data to the Storj DCS."); } } diff --git a/Duplicati/Library/Backend/TahoeLAFS/Duplicati.Library.Backend.TahoeLAFS.csproj b/Duplicati/Library/Backend/TahoeLAFS/Duplicati.Library.Backend.TahoeLAFS.csproj index 2d5c02c6e..42ca371e7 100644 --- a/Duplicati/Library/Backend/TahoeLAFS/Duplicati.Library.Backend.TahoeLAFS.csproj +++ b/Duplicati/Library/Backend/TahoeLAFS/Duplicati.Library.Backend.TahoeLAFS.csproj @@ -1,17 +1,27 @@  + net8.0 Library Duplicati.Library.Backend - - - - - - - + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/Tardigrade/Duplicati.Library.Backend.Tardigrade.csproj b/Duplicati/Library/Backend/Tardigrade/Duplicati.Library.Backend.Tardigrade.csproj index d2627e7cd..16612f306 100644 --- a/Duplicati/Library/Backend/Tardigrade/Duplicati.Library.Backend.Tardigrade.csproj +++ b/Duplicati/Library/Backend/Tardigrade/Duplicati.Library.Backend.Tardigrade.csproj @@ -1,28 +1,27 @@  + net8.0 Library - - x64 - + - + + + - + + - - - - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - \ No newline at end of file + diff --git a/Duplicati/Library/Backend/TencentCOS/Duplicati.Library.Backend.TencentCOS.csproj b/Duplicati/Library/Backend/TencentCOS/Duplicati.Library.Backend.TencentCOS.csproj index 262db429e..0787253f2 100644 --- a/Duplicati/Library/Backend/TencentCOS/Duplicati.Library.Backend.TencentCOS.csproj +++ b/Duplicati/Library/Backend/TencentCOS/Duplicati.Library.Backend.TencentCOS.csproj @@ -15,4 +15,12 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/WEBDAV/Duplicati.Library.Backend.WEBDAV.csproj b/Duplicati/Library/Backend/WEBDAV/Duplicati.Library.Backend.WEBDAV.csproj index bee6be5ca..c9cf10e4c 100644 --- a/Duplicati/Library/Backend/WEBDAV/Duplicati.Library.Backend.WEBDAV.csproj +++ b/Duplicati/Library/Backend/WEBDAV/Duplicati.Library.Backend.WEBDAV.csproj @@ -1,14 +1,23 @@  + net8.0 Library Duplicati.Library.Backend + - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Backends/Duplicati.Library.Backends.csproj b/Duplicati/Library/Backends/Duplicati.Library.Backends.csproj new file mode 100644 index 000000000..7548b7996 --- /dev/null +++ b/Duplicati/Library/Backends/Duplicati.Library.Backends.csproj @@ -0,0 +1,33 @@ + + + net8.0 + Central reference to all backends + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Duplicati/Library/Common/Duplicati.Library.Common.csproj b/Duplicati/Library/Common/Duplicati.Library.Common.csproj index c3e7d30fa..afb83a7ca 100644 --- a/Duplicati/Library/Common/Duplicati.Library.Common.csproj +++ b/Duplicati/Library/Common/Duplicati.Library.Common.csproj @@ -1,18 +1,38 @@  + net8.0 Library Duplicati.Library.IO + + + + + ..\..\..\thirdparty\alphafs\AlphaFS.dll + + + ..\..\..\thirdparty\alphavss\AlphaVSS.Common.dll + + + + + + + + + - + + - - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - \ No newline at end of file + + diff --git a/Duplicati/Library/Common/IO/PosixFile.cs b/Duplicati/Library/Common/IO/PosixFile.cs index d3f746adb..73b99240d 100644 --- a/Duplicati/Library/Common/IO/PosixFile.cs +++ b/Duplicati/Library/Common/IO/PosixFile.cs @@ -18,347 +18,347 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using System; -using System.Collections.Generic; -using Mono.Unix.Native; - -namespace Duplicati.Library.Common.IO -{ - public static class PosixFile - { - - private static readonly bool SUPPORTS_LLISTXATTR; - - static PosixFile () - { - bool works = false; - try - { - string[] v; - Mono.Unix.Native.Syscall.llistxattr("/", out v); - works = true; - } - catch (EntryPointNotFoundException e) - { - } - catch - { - } - SUPPORTS_LLISTXATTR = works; - } - - /// - /// Opens the file and honors advisory locking. - /// - /// A open stream that references the file - /// The full path to the file - public static System.IO.Stream OpenExclusive(string path, System.IO.FileAccess mode) - { - return OpenExclusive(path, mode, (int)Mono.Unix.Native.FilePermissions.DEFFILEMODE); - } - - /// - /// Opens the file and honors advisory locking. - /// - /// A open stream that references the file - /// The full path to the file - /// The file create mode - public static System.IO.Stream OpenExclusive(string path, System.IO.FileAccess mode, int filemode) - { - Flock lck; - lck.l_len = 0; - lck.l_pid = Syscall.getpid(); - lck.l_start = 0; - lck.l_type = LockType.F_WRLCK; - lck.l_whence = SeekFlags.SEEK_SET; - - OpenFlags flags = OpenFlags.O_CREAT; - if (mode == System.IO.FileAccess.Read) - { - lck.l_type = LockType.F_RDLCK; - flags |= OpenFlags.O_RDONLY; - } else if (mode == System.IO.FileAccess.Write) { - flags |= OpenFlags.O_WRONLY; - } else { - flags |= OpenFlags.O_RDWR; - } - - int fd = Syscall.open(path, flags, (Mono.Unix.Native.FilePermissions)filemode); - if (fd > 0) - { - //This does not work on OSX, it gives ENOTTY - //int res = Syscall.fcntl(fd, Mono.Unix.Native.FcntlCommand.F_SETLK, ref lck); - - //This is the same (at least for our purpose, and works on OSX) - int res = Syscall.lockf(fd, LockfCommand.F_TLOCK, 0); - - //If we have the lock, return the stream - if (res == 0) - return new Mono.Unix.UnixStream(fd); - else - { - Mono.Unix.Native.Syscall.close(fd); - throw new LockedFileException(path, mode); - } - } - - throw new BadFileException(path); - } - - [Serializable] - private class BadFileException : System.IO.IOException - { - public BadFileException(string filename) - : base(string.Format("Unable to open the file \"{0}\", error: {1} ({2})", filename, Syscall.GetLastError(), (int)Syscall.GetLastError())) - { - } - } - - [Serializable] - private class LockedFileException : System.IO.IOException - { - public LockedFileException(string filename, System.IO.FileAccess mode) - : base(string.Format("Unable to open the file \"{0}\" in mode {1}, error: {2} ({3})", filename, mode, Syscall.GetLastError(), (int)Syscall.GetLastError())) - { - } - } - - [Serializable] - private class FileAccesException : System.IO.IOException - { - public FileAccesException(string filename, string method) - : base(string.Format("Unable to access the file \"{0}\" with method {1}, error: {2} ({3})", filename, method, Syscall.GetLastError(), (int)Syscall.GetLastError())) - { - } - } - - /// - /// Gets the symlink target for the given path - /// - /// The path to get the symlink target for - /// The symlink target - public static string GetSymlinkTarget(string path) - { - System.Text.StringBuilder sb = new System.Text.StringBuilder(2048); //2kb, should cover utf16 * 1023 chars - if (Mono.Unix.Native.Syscall.readlink(path, sb, (ulong)sb.Capacity) >= 0) - return sb.ToString(); - - throw new System.IO.FileLoadException(string.Format("Unable to get symlink for \"{0}\", error: {1} ({2})", path, Syscall.GetLastError(), (int)Syscall.GetLastError())); - } - - /// - /// Creates a new symlink - /// - /// The path to create the symbolic link entry - /// The path the symbolic link points to - public static void CreateSymlink(string path, string target) - { - if (Mono.Unix.Native.Syscall.symlink(target, path) != 0) - throw new System.IO.IOException(string.Format("Unable to create symlink from \"{0}\" to \"{1}\", error: {2} ({3})", path, target, Syscall.GetLastError(), (int)Syscall.GetLastError())); - } - - /// - /// Enum that describes the different filesystem entry types - /// - public enum FileType - { - File, - Directory, - Symlink, - Fifo, - Socket, - CharacterDevice, - BlockDevice, - Unknown - } - - /// - /// Gets the type of the file. - /// - /// The file type - /// The full path to look up - public static FileType GetFileType(string path) - { - - var fse = Mono.Unix.UnixFileInfo.GetFileSystemEntry(path); - if (fse.IsRegularFile) - return FileType.File; - else if (fse.IsDirectory) - return FileType.Directory; - else if (fse.IsSymbolicLink) - return FileType.Symlink; - else if (fse.IsFifo) - return FileType.Fifo; - else if (fse.IsSocket) - return FileType.Socket; - else if (fse.IsCharacterDevice) - return FileType.CharacterDevice; - else if (fse.IsBlockDevice) - return FileType.BlockDevice; - else - return FileType.Unknown; - } - - - /// - /// Gets the extended attributes. - /// - /// The extended attributes. - /// The full path to look up - /// A flag indicating if the target is a symlink - /// A flag indicating if a symlink should be followed - public static Dictionary GetExtendedAttributes(string path, bool isSymlink, bool followSymlink) - { - // If we get a symlink that we should not follow, we need llistxattr support - if (isSymlink && !followSymlink && !SUPPORTS_LLISTXATTR) - return null; - - var use_llistxattr = SUPPORTS_LLISTXATTR && !followSymlink; - - string[] values; - var size = use_llistxattr ? Mono.Unix.Native.Syscall.llistxattr(path, out values) : Mono.Unix.Native.Syscall.listxattr(path, out values); - if (size < 0) - { - // In case the underlying filesystem does not support extended attributes, - // we simply return that there are no attributes - var err = Syscall.GetLastError(); - if (err == Errno.EOPNOTSUPP || err == Errno.ENODATA) - return null; - - throw new FileAccesException(path, use_llistxattr ? "llistxattr" : "listxattr"); - } - - var dict = new Dictionary(); - foreach(var s in values) - { - byte[] v; - var n = SUPPORTS_LLISTXATTR ? Mono.Unix.Native.Syscall.lgetxattr(path, s, out v) : Mono.Unix.Native.Syscall.getxattr(path, s, out v); - if (n > 0) - dict.Add(s, v); - } - - return dict; - } - - /// - /// Sets an extended attribute. - /// - /// The full path to set the values for - /// The extended attribute key - /// The value to set - public static void SetExtendedAttribute(string path, string key, byte[] value) - { - Mono.Unix.Native.Syscall.setxattr(path, key, value); - } - - /// - /// Describes the basic user/group/perm tuplet for a file or folder - /// - public struct FileInfo - { - public readonly long UID; - public readonly long GID; - public readonly long Permissions; - public readonly string OwnerName; - public readonly string GroupName; - - internal FileInfo(Mono.Unix.UnixFileSystemInfo fse) - { - UID = fse.OwnerUserId; - GID = fse.OwnerGroupId; - Permissions = (long)fse.FileAccessPermissions; - - try - { - OwnerName = fse.OwnerUser.UserName; - } - catch (ArgumentException) - { - // Could not retrieve user name, possibly the user is not defined on the local system - OwnerName = null; - } - - try - { - GroupName = fse.OwnerGroup.GroupName; - } - catch (ArgumentException) - { - // Could not retrieve group name, possibly the group is not defined on the local system - GroupName = null; - } - } - } - - /// - /// Gets the basic user/group/perm tuplet for a file or folder - /// - /// The basic user/group/perm tuplet for a file or folder - /// The full path to look up - public static FileInfo GetUserGroupAndPermissions(string path) - { - return new FileInfo(Mono.Unix.UnixFileInfo.GetFileSystemEntry(path)); - } - - /// - /// Sets the basic user/group/perm tuplet for a file or folder - /// - /// The full path to look up - /// The owner user id to set - /// The owner group id to set - /// The file access permissions to set - public static void SetUserGroupAndPermissions(string path, long uid, long gid, long permissions) - { - Mono.Unix.UnixFileInfo.GetFileSystemEntry(path).SetOwner(uid, gid); - Mono.Unix.UnixFileInfo.GetFileSystemEntry(path).FileAccessPermissions = (Mono.Unix.FileAccessPermissions)permissions; - } - - /// - /// Gets the UID from a user name - /// - /// The user ID. - /// The user name. - public static long GetUserID(string name) - { - return new Mono.Unix.UnixUserInfo(name).UserId; - } - - /// - /// Gets the GID from a group name - /// - /// The user ID. - /// The group name. - public static long GetGroupID(string name) - { - return new Mono.Unix.UnixGroupInfo(name).GroupId; - } - - /// - /// Gets the number of hard links for a file - /// - /// The hardlink count - /// The full path to look up - public static long GetHardlinkCount(string path) - { - var fse = Mono.Unix.UnixFileInfo.GetFileSystemEntry(path); - if (fse.IsRegularFile || fse.IsDirectory) - return fse.LinkCount; - else - return 0; - } - - /// - /// Gets a unique ID for the path inode target, - /// which is the device ID and inode ID - /// joined with a ":" - /// - /// The inode target ID. - /// The full path to look up - public static string GetInodeTargetID(string path) - { - var fse = Mono.Unix.UnixFileInfo.GetFileSystemEntry(path); - return fse.Device + ":" + fse.Inode; - } - } -} - +using System; +using System.Collections.Generic; +using Mono.Unix.Native; + +namespace Duplicati.Library.Common.IO +{ + public static class PosixFile + { + + private static readonly bool SUPPORTS_LLISTXATTR; + + static PosixFile () + { + bool works = false; + try + { + string[] v; + Mono.Unix.Native.Syscall.llistxattr("/", out v); + works = true; + } + catch (EntryPointNotFoundException e) + { + } + catch + { + } + SUPPORTS_LLISTXATTR = works; + } + + /// + /// Opens the file and honors advisory locking. + /// + /// A open stream that references the file + /// The full path to the file + public static System.IO.Stream OpenExclusive(string path, System.IO.FileAccess mode) + { + return OpenExclusive(path, mode, (int)Mono.Unix.Native.FilePermissions.DEFFILEMODE); + } + + /// + /// Opens the file and honors advisory locking. + /// + /// A open stream that references the file + /// The full path to the file + /// The file create mode + public static System.IO.Stream OpenExclusive(string path, System.IO.FileAccess mode, int filemode) + { + Flock lck; + lck.l_len = 0; + lck.l_pid = Syscall.getpid(); + lck.l_start = 0; + lck.l_type = LockType.F_WRLCK; + lck.l_whence = SeekFlags.SEEK_SET; + + OpenFlags flags = OpenFlags.O_CREAT; + if (mode == System.IO.FileAccess.Read) + { + lck.l_type = LockType.F_RDLCK; + flags |= OpenFlags.O_RDONLY; + } else if (mode == System.IO.FileAccess.Write) { + flags |= OpenFlags.O_WRONLY; + } else { + flags |= OpenFlags.O_RDWR; + } + + int fd = Syscall.open(path, flags, (Mono.Unix.Native.FilePermissions)filemode); + if (fd > 0) + { + //This does not work on OSX, it gives ENOTTY + //int res = Syscall.fcntl(fd, Mono.Unix.Native.FcntlCommand.F_SETLK, ref lck); + + //This is the same (at least for our purpose, and works on OSX) + int res = Syscall.lockf(fd, LockfCommand.F_TLOCK, 0); + + //If we have the lock, return the stream + if (res == 0) + return new Mono.Unix.UnixStream(fd); + else + { + Mono.Unix.Native.Syscall.close(fd); + throw new LockedFileException(path, mode); + } + } + + throw new BadFileException(path); + } + + [Serializable] + private class BadFileException : System.IO.IOException + { + public BadFileException(string filename) + : base(string.Format("Unable to open the file \"{0}\", error: {1} ({2})", filename, Syscall.GetLastError(), (int)Syscall.GetLastError())) + { + } + } + + [Serializable] + private class LockedFileException : System.IO.IOException + { + public LockedFileException(string filename, System.IO.FileAccess mode) + : base(string.Format("Unable to open the file \"{0}\" in mode {1}, error: {2} ({3})", filename, mode, Syscall.GetLastError(), (int)Syscall.GetLastError())) + { + } + } + + [Serializable] + private class FileAccesException : System.IO.IOException + { + public FileAccesException(string filename, string method) + : base(string.Format("Unable to access the file \"{0}\" with method {1}, error: {2} ({3})", filename, method, Syscall.GetLastError(), (int)Syscall.GetLastError())) + { + } + } + + /// + /// Gets the symlink target for the given path + /// + /// The path to get the symlink target for + /// The symlink target + public static string GetSymlinkTarget(string path) + { + System.Text.StringBuilder sb = new System.Text.StringBuilder(2048); //2kb, should cover utf16 * 1023 chars + if (Mono.Unix.Native.Syscall.readlink(path, sb, (ulong)sb.Capacity) >= 0) + return sb.ToString(); + + throw new System.IO.FileLoadException(string.Format("Unable to get symlink for \"{0}\", error: {1} ({2})", path, Syscall.GetLastError(), (int)Syscall.GetLastError())); + } + + /// + /// Creates a new symlink + /// + /// The path to create the symbolic link entry + /// The path the symbolic link points to + public static void CreateSymlink(string path, string target) + { + if (Mono.Unix.Native.Syscall.symlink(target, path) != 0) + throw new System.IO.IOException(string.Format("Unable to create symlink from \"{0}\" to \"{1}\", error: {2} ({3})", path, target, Syscall.GetLastError(), (int)Syscall.GetLastError())); + } + + /// + /// Enum that describes the different filesystem entry types + /// + public enum FileType + { + File, + Directory, + Symlink, + Fifo, + Socket, + CharacterDevice, + BlockDevice, + Unknown + } + + /// + /// Gets the type of the file. + /// + /// The file type + /// The full path to look up + public static FileType GetFileType(string path) + { + + var fse = Mono.Unix.UnixFileInfo.GetFileSystemEntry(path); + if (fse.IsRegularFile) + return FileType.File; + else if (fse.IsDirectory) + return FileType.Directory; + else if (fse.IsSymbolicLink) + return FileType.Symlink; + else if (fse.IsFifo) + return FileType.Fifo; + else if (fse.IsSocket) + return FileType.Socket; + else if (fse.IsCharacterDevice) + return FileType.CharacterDevice; + else if (fse.IsBlockDevice) + return FileType.BlockDevice; + else + return FileType.Unknown; + } + + + /// + /// Gets the extended attributes. + /// + /// The extended attributes. + /// The full path to look up + /// A flag indicating if the target is a symlink + /// A flag indicating if a symlink should be followed + public static Dictionary GetExtendedAttributes(string path, bool isSymlink, bool followSymlink) + { + // If we get a symlink that we should not follow, we need llistxattr support + if (isSymlink && !followSymlink && !SUPPORTS_LLISTXATTR) + return null; + + var use_llistxattr = SUPPORTS_LLISTXATTR && !followSymlink; + + string[] values; + var size = use_llistxattr ? Mono.Unix.Native.Syscall.llistxattr(path, out values) : Mono.Unix.Native.Syscall.listxattr(path, out values); + if (size < 0) + { + // In case the underlying filesystem does not support extended attributes, + // we simply return that there are no attributes + var err = Syscall.GetLastError(); + if (err == Errno.EOPNOTSUPP || err == Errno.ENODATA) + return null; + + throw new FileAccesException(path, use_llistxattr ? "llistxattr" : "listxattr"); + } + + var dict = new Dictionary(); + foreach(var s in values) + { + byte[] v; + var n = SUPPORTS_LLISTXATTR ? Mono.Unix.Native.Syscall.lgetxattr(path, s, out v) : Mono.Unix.Native.Syscall.getxattr(path, s, out v); + if (n > 0) + dict.Add(s, v); + } + + return dict; + } + + /// + /// Sets an extended attribute. + /// + /// The full path to set the values for + /// The extended attribute key + /// The value to set + public static void SetExtendedAttribute(string path, string key, byte[] value) + { + Mono.Unix.Native.Syscall.setxattr(path, key, value); + } + + /// + /// Describes the basic user/group/perm tuplet for a file or folder + /// + public struct FileInfo + { + public readonly long UID; + public readonly long GID; + public readonly long Permissions; + public readonly string OwnerName; + public readonly string GroupName; + + internal FileInfo(Mono.Unix.UnixFileSystemInfo fse) + { + UID = fse.OwnerUserId; + GID = fse.OwnerGroupId; + Permissions = (long)fse.FileAccessPermissions; + + try + { + OwnerName = fse.OwnerUser.UserName; + } + catch (ArgumentException) + { + // Could not retrieve user name, possibly the user is not defined on the local system + OwnerName = null; + } + + try + { + GroupName = fse.OwnerGroup.GroupName; + } + catch (ArgumentException) + { + // Could not retrieve group name, possibly the group is not defined on the local system + GroupName = null; + } + } + } + + /// + /// Gets the basic user/group/perm tuplet for a file or folder + /// + /// The basic user/group/perm tuplet for a file or folder + /// The full path to look up + public static FileInfo GetUserGroupAndPermissions(string path) + { + return new FileInfo(Mono.Unix.UnixFileInfo.GetFileSystemEntry(path)); + } + + /// + /// Sets the basic user/group/perm tuplet for a file or folder + /// + /// The full path to look up + /// The owner user id to set + /// The owner group id to set + /// The file access permissions to set + public static void SetUserGroupAndPermissions(string path, long uid, long gid, long permissions) + { + Mono.Unix.UnixFileInfo.GetFileSystemEntry(path).SetOwner(uid, gid); + Mono.Unix.UnixFileInfo.GetFileSystemEntry(path).FileAccessPermissions = (Mono.Unix.FileAccessPermissions)permissions; + } + + /// + /// Gets the UID from a user name + /// + /// The user ID. + /// The user name. + public static long GetUserID(string name) + { + return new Mono.Unix.UnixUserInfo(name).UserId; + } + + /// + /// Gets the GID from a group name + /// + /// The user ID. + /// The group name. + public static long GetGroupID(string name) + { + return new Mono.Unix.UnixGroupInfo(name).GroupId; + } + + /// + /// Gets the number of hard links for a file + /// + /// The hardlink count + /// The full path to look up + public static long GetHardlinkCount(string path) + { + var fse = Mono.Unix.UnixFileInfo.GetFileSystemEntry(path); + if (fse.IsRegularFile || fse.IsDirectory) + return fse.LinkCount; + else + return 0; + } + + /// + /// Gets a unique ID for the path inode target, + /// which is the device ID and inode ID + /// joined with a ":" + /// + /// The inode target ID. + /// The full path to look up + public static string GetInodeTargetID(string path) + { + var fse = Mono.Unix.UnixFileInfo.GetFileSystemEntry(path); + return fse.Device + ":" + fse.Inode; + } + } +} + diff --git a/Duplicati/Library/Common/IO/SystemIOLinux.cs b/Duplicati/Library/Common/IO/SystemIOLinux.cs index 570e9d036..66b9c40ac 100644 --- a/Duplicati/Library/Common/IO/SystemIOLinux.cs +++ b/Duplicati/Library/Common/IO/SystemIOLinux.cs @@ -59,6 +59,11 @@ namespace Duplicati.Library.Common.IO public void FileSetLastWriteTimeUtc(string path, DateTime time) { File.SetLastWriteTimeUtc(path, time); + + var gtime = FileGetLastWriteTimeUtc(path); + if(gtime != time) { + Console.Error.WriteLine($"DISS: {path} {gtime.ToFileTimeUtc() - time.ToFileTimeUtc()}"); + } } public void FileSetCreationTimeUtc(string path, DateTime time) diff --git a/Duplicati/Library/Common/IO/SystemIOWindows.cs b/Duplicati/Library/Common/IO/SystemIOWindows.cs index 981596882..c942fa2ca 100644 --- a/Duplicati/Library/Common/IO/SystemIOWindows.cs +++ b/Duplicati/Library/Common/IO/SystemIOWindows.cs @@ -615,11 +615,11 @@ namespace Duplicati.Library.Common.IO if (asDir) { - Alphaleonis.Win32.Filesystem.Directory.CreateSymbolicLink(AddExtendedDevicePathPrefix(symlinkfile), target, AlphaFS.PathFormat.LongFullPath); + AlphaFS.Directory.CreateSymbolicLink(AddExtendedDevicePathPrefix(symlinkfile), target, AlphaFS.PathFormat.LongFullPath); } else { - Alphaleonis.Win32.Filesystem.File.CreateSymbolicLink(AddExtendedDevicePathPrefix(symlinkfile), target, AlphaFS.PathFormat.LongFullPath); + AlphaFS.File.CreateSymbolicLink(AddExtendedDevicePathPrefix(symlinkfile), target, AlphaFS.PathFormat.LongFullPath); } //Sadly we do not get a notification if the creation fails :( diff --git a/Duplicati/Library/Common/IO/VssBackupComponents.cs b/Duplicati/Library/Common/IO/VssBackupComponents.cs index fad050fe5..a06630394 100644 --- a/Duplicati/Library/Common/IO/VssBackupComponents.cs +++ b/Duplicati/Library/Common/IO/VssBackupComponents.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a diff --git a/Duplicati/Library/Compression/Duplicati.Library.Compression.csproj b/Duplicati/Library/Compression/Duplicati.Library.Compression.csproj index d2796d429..bc4f4cb60 100644 --- a/Duplicati/Library/Compression/Duplicati.Library.Compression.csproj +++ b/Duplicati/Library/Compression/Duplicati.Library.Compression.csproj @@ -1,21 +1,31 @@  + net8.0 Library + - - - - - + + - - - - + ..\..\..\thirdparty\ManagedLZMA\managed-lzma.dll + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + \ No newline at end of file diff --git a/Duplicati/Library/Compression/SevenZipCompression.cs b/Duplicati/Library/Compression/SevenZipCompression.cs index 5eb44b2bb..5f244bf91 100644 --- a/Duplicati/Library/Compression/SevenZipCompression.cs +++ b/Duplicati/Library/Compression/SevenZipCompression.cs @@ -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; using System.IO; diff --git a/Duplicati/Library/DynamicLoader/Duplicati.Library.DynamicLoader.csproj b/Duplicati/Library/DynamicLoader/Duplicati.Library.DynamicLoader.csproj index 185b68f7e..8d2fac9c0 100644 --- a/Duplicati/Library/DynamicLoader/Duplicati.Library.DynamicLoader.csproj +++ b/Duplicati/Library/DynamicLoader/Duplicati.Library.DynamicLoader.csproj @@ -1,12 +1,22 @@  + net8.0 Library + - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Encryption/Duplicati.Library.Encryption.csproj b/Duplicati/Library/Encryption/Duplicati.Library.Encryption.csproj index e2c05d60f..12cdf35f5 100644 --- a/Duplicati/Library/Encryption/Duplicati.Library.Encryption.csproj +++ b/Duplicati/Library/Encryption/Duplicati.Library.Encryption.csproj @@ -1,18 +1,25 @@  + net8.0 Library + + + + + - - + - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + \ No newline at end of file diff --git a/Duplicati/Library/Interface/Duplicati.Library.Interface.csproj b/Duplicati/Library/Interface/Duplicati.Library.Interface.csproj index f1815256d..7fbac15eb 100644 --- a/Duplicati/Library/Interface/Duplicati.Library.Interface.csproj +++ b/Duplicati/Library/Interface/Duplicati.Library.Interface.csproj @@ -1,17 +1,19 @@  + net8.0 Library + + - - - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + \ No newline at end of file diff --git a/Duplicati/Library/Interface/IBackendGUI.cs b/Duplicati/Library/Interface/IBackendGUI.cs deleted file mode 100644 index 0a68ece39..000000000 --- a/Duplicati/Library/Interface/IBackendGUI.cs +++ /dev/null @@ -1,37 +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.Text; -using System.Windows.Forms; - -namespace Duplicati.Library.Interface -{ - /// - /// The interface used to display a GUI wizard page for the user. - /// If the backed should be available from the GUI, this interface must be implemented, - /// otherwise a generic interface is displayed, listing the commandline options. - /// - public interface IBackendGUI : IBackend, IGUIControl - { - } -} diff --git a/Duplicati/Library/Interface/ICompressionGUI.cs b/Duplicati/Library/Interface/ICompressionGUI.cs deleted file mode 100644 index 0a0193001..000000000 --- a/Duplicati/Library/Interface/ICompressionGUI.cs +++ /dev/null @@ -1,35 +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.Text; - -namespace Duplicati.Library.Interface -{ - /// - /// An interface used to provide a custom usercontrol for a compression module. - /// This interface is not currently in use. - /// - public interface ICompressionGUI : ICompression, IGUIControl - { - } -} diff --git a/Duplicati/Library/Interface/IEncryptionGUI.cs b/Duplicati/Library/Interface/IEncryptionGUI.cs deleted file mode 100644 index 001f05692..000000000 --- a/Duplicati/Library/Interface/IEncryptionGUI.cs +++ /dev/null @@ -1,37 +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.Text; - -namespace Duplicati.Library.Interface -{ - /// - /// An interface used to provide a custom usercontrol for an encryption module. - /// The control is displayed on the passphrase selection page, and thus has limited space available. - /// If this interface is not implemented, a default options grid will be displayed. - /// To prevent options from being displayed, implement this interface, and return an empty control in the method. - /// - public interface IEncryptionGUI : IEncryption, IGUIControl - { - } -} diff --git a/Duplicati/Library/Interface/IGUIControl.cs b/Duplicati/Library/Interface/IGUIControl.cs deleted file mode 100644 index 6e3c9f17d..000000000 --- a/Duplicati/Library/Interface/IGUIControl.cs +++ /dev/null @@ -1,89 +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.Text; -using System.Windows.Forms; - -namespace Duplicati.Library.Interface -{ - /// - /// The interface that defines a GUI for a plugable component, such as a backend or encryption module. - /// All options are stored in a dictionary, which is persisted to the database. - /// When creating a backup, the option dictionary will be empty, when editing, it will contain the contents from the last edit. - /// The options in the dictionary are free form, and should contain the state of the userinterface. - /// When the backup is invoked, the method is invoked with the options dictionary. - /// At this point the control should setup the Duplicati options, based on the saved userinterface state. - /// This enables the user interface to be decoupled from the actual options. - /// - public interface IGUIControl - { - /// - /// The title of the page, shown as the wizard title or the tab caption - /// - string PageTitle { get; } - - /// - /// The page description, shown as a helptext in the wizard or as a tooltip on a tab - /// - string PageDescription { get; } - - /// - /// A method to retrieve a user interface control. - /// The method should keep a reference to the dictionary. - /// The control will be resized to fit in the container. - /// - /// The set of settings defined by the calling application - /// A set of options, either empty or previously stored by the control - /// The control that represents the user interface - Control GetControl(IDictionary applicationSettings, IDictionary options); - - /// - /// Method that is called when the user leaves the form. - /// This method can be used to store data from the interface in the dictionary, - /// so the user interface can be reconstructed later. - /// Do not throw exceptions to interrupt this method as that will remove the "back" option in the wizard, - /// return false in the method instead. - /// - /// The UI object created by the GetControl call - void Leave(Control control); - - /// - /// A method that is called when the form should be validated. - /// The method should inform the user if the validation fails. - /// - /// The UI object created by the GetControl call - /// True if the data is valid, false otherwise - bool Validate(Control control); - - /// - /// This method sets up the Duplicati options, based on a previous setup. - /// The method should read the saved setup from , and set the appropriate . - /// If the UI represents a backend, the function should return the backend URL, otherwise the return value is ignored. - /// - /// The set of settings defined by the calling application - /// A set of previously saved options for the control - /// A set of commandline options passed to Duplicati - /// The destination path if the control is for a backend, otherwise null - string GetConfiguration(IDictionary applicationSettings, IDictionary guiOptions, IDictionary commandlineOptions); - } -} diff --git a/Duplicati/Library/Interface/IGUIMiniControl.cs b/Duplicati/Library/Interface/IGUIMiniControl.cs deleted file mode 100644 index 23b90a4d9..000000000 --- a/Duplicati/Library/Interface/IGUIMiniControl.cs +++ /dev/null @@ -1,39 +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.Text; - -namespace Duplicati.Library.Interface -{ - /// - /// An interface that indicates that the control does not require a full page. - /// If the host application chooses to do so, it may display a collection of - /// mini controls on a single page, rather than display them on a page each. - /// This makes the UI less cluttered if there are many small custom controls. - /// A control should implement this interface if it requires only one or two - /// lines, eg. less than 40 pixels high. - /// - public interface IGUIMiniControl : IGUIControl - { - } -} diff --git a/Duplicati/Library/Interface/ISettingsControl.cs b/Duplicati/Library/Interface/ISettingsControl.cs deleted file mode 100644 index f23458b10..000000000 --- a/Duplicati/Library/Interface/ISettingsControl.cs +++ /dev/null @@ -1,61 +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.Text; - -namespace Duplicati.Library.Interface -{ - /// - /// An interface for modules that register a control in the settings menu. - /// This control will be placed in a tab page. - /// The method is invoked for each custom control module when generating a backup. - /// When editing/creating a backup job, the and functions are invoked. - /// - public interface ISettingsControl : IGUIControl - { - /// - /// Gets a key that uniquely identifies the control - /// - string Key { get; } - - /// - /// Called when the user is editing or creating a backup. - /// This method can alter the applicationSettings environment and thus communicate with another UI plugin, - /// such as an or . - /// - /// The application settings - /// The options saved earlier when configuring the control - void BeginEdit(IDictionary applicationSettings, IDictionary guiOptions); - - /// - /// Called when the user has finished editing or creating a backup. - /// This method can be used to alter the collection, - /// and thus persist a change. - /// As the other elements can also modify the collection, - /// this can be used to communicate changes from another component. - /// - /// The application settings - /// The options saved earlier when configuring the control - void EndEdit(IDictionary applicationSettings, IDictionary guiOptions); - } -} diff --git a/Duplicati/Library/Localization/Duplicati.Library.Localization.csproj b/Duplicati/Library/Localization/Duplicati.Library.Localization.csproj index 7e31378b3..b34f5f536 100644 --- a/Duplicati/Library/Localization/Duplicati.Library.Localization.csproj +++ b/Duplicati/Library/Localization/Duplicati.Library.Localization.csproj @@ -1,16 +1,27 @@  + net8.0 Library + + + + + - - - - + - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Localization/LocalizationContext.cs b/Duplicati/Library/Localization/LocalizationContext.cs index 5eba0d441..ec04afa32 100644 --- a/Duplicati/Library/Localization/LocalizationContext.cs +++ b/Duplicati/Library/Localization/LocalizationContext.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -46,7 +46,9 @@ namespace Duplicati.Library.Localization public LocalizationContext(System.Globalization.CultureInfo ci) { m_prev = CallContext.GetData(LocalizationService.LOGICAL_CONTEXT_KEY) as string; - CallContext.SetData(LocalizationService.LOGICAL_CONTEXT_KEY, ci.Name); m_isDisposed = false; + CallContext.SetData(LocalizationService.LOGICAL_CONTEXT_KEY, ci.Name); + + m_isDisposed = false; } /// diff --git a/Duplicati/Library/Localization/LocalizationService.cs b/Duplicati/Library/Localization/LocalizationService.cs index 662f1b712..e110a289d 100644 --- a/Duplicati/Library/Localization/LocalizationService.cs +++ b/Duplicati/Library/Localization/LocalizationService.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a diff --git a/Duplicati/Library/Logging/Duplicati.Library.Logging.csproj b/Duplicati/Library/Logging/Duplicati.Library.Logging.csproj index 914b4e3c8..30eb69cb0 100644 --- a/Duplicati/Library/Logging/Duplicati.Library.Logging.csproj +++ b/Duplicati/Library/Logging/Duplicati.Library.Logging.csproj @@ -3,4 +3,12 @@ net8.0 Library + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/Logging/Log.cs b/Duplicati/Library/Logging/Log.cs index 097822d62..5ea96442d 100644 --- a/Duplicati/Library/Logging/Log.cs +++ b/Duplicati/Library/Logging/Log.cs @@ -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; diff --git a/Duplicati/Library/Main/BackendManager.cs b/Duplicati/Library/Main/BackendManager.cs index b5374b621..b08db406d 100644 --- a/Duplicati/Library/Main/BackendManager.cs +++ b/Duplicati/Library/Main/BackendManager.cs @@ -21,7 +21,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; +using System.Security.Cryptography; using System.Text; using Duplicati.Library.Utility; using Duplicati.Library.Main.Database; @@ -29,6 +29,7 @@ using Duplicati.Library.Main.Volumes; using Newtonsoft.Json; using Duplicati.Library.Localization.Short; using System.Threading; +using System.Net; namespace Duplicati.Library.Main { @@ -39,8 +40,6 @@ namespace Duplicati.Library.Main /// private static readonly string LOGTAG = Logging.Log.LogTagFromType(); - public const string VOLUME_HASH = "SHA256"; - /// /// Class to represent hash failures /// @@ -421,7 +420,7 @@ namespace Duplicati.Library.Main if (m_taskControl != null) m_taskControl.StateChangedEvent += (state) => { if (state == TaskControlState.Abort) - m_thread.Abort(); + m_thread.Interrupt(); }; m_queue = new BlockingQueue(options.SynchronousUpload ? 1 : (options.AsynchronousUploadLimit == 0 ? int.MaxValue : options.AsynchronousUploadLimit)); m_thread = new System.Threading.Thread(this.ThreadRun); @@ -433,14 +432,14 @@ namespace Duplicati.Library.Main public static string CalculateFileHash(string filename) { using (System.IO.FileStream fs = System.IO.File.OpenRead(filename)) - using (var hasher = HashAlgorithmHelper.Create(VOLUME_HASH)) + using (var hasher = VolumeHashFactory.CreateHasher()) return Convert.ToBase64String(hasher.ComputeHash(fs)); } /// Calculate file hash directly on stream object (for piping) public static string CalculateFileHash(System.IO.Stream stream) { - using (var hasher = HashAlgorithmHelper.Create(VOLUME_HASH)) + using (var hasher = VolumeHashFactory.CreateHasher()) return Convert.ToBase64String(hasher.ComputeHash(stream)); } @@ -449,9 +448,8 @@ namespace Duplicati.Library.Main /// with a callback to retrieve the hash when done. /// public static System.Security.Cryptography.CryptoStream GetFileHasherStream - (System.IO.Stream stream, System.Security.Cryptography.CryptoStreamMode mode, out Func getHash) + (System.IO.Stream stream, System.Security.Cryptography.CryptoStreamMode mode, HashAlgorithm hasher, out Func getHash) { - var hasher = HashAlgorithmHelper.Create(VOLUME_HASH); System.Security.Cryptography.CryptoStream retHasherStream = new System.Security.Cryptography.CryptoStream(stream, hasher, mode); getHash = () => @@ -460,7 +458,6 @@ namespace Duplicati.Library.Main && !retHasherStream.HasFlushedFinalBlock) retHasherStream.FlushFinalBlock(); string retHash = Convert.ToBase64String(hasher.Hash); - hasher.Dispose(); return retHash; }; return retHasherStream; @@ -680,7 +677,7 @@ namespace Duplicati.Library.Main IndexVolumeWriter wr = null; try { - var hashsize = HashAlgorithmHelper.Create(m_options.BlockHashAlgorithm).HashSize / 8; + var hashsize = HashFactory.HashSizeBytes(m_options.BlockHashAlgorithm); wr = new IndexVolumeWriter(m_options); using (var rd = new IndexVolumeReader(p.CompressionModule, item.Indexfile.Item2.LocalFilename, m_options, hashsize)) wr.CopyFrom(rd, x => x == oldname ? newname : x); @@ -925,10 +922,10 @@ namespace Duplicati.Library.Main dlTarget = new Library.Utility.TempFile(); if (m_backend is Library.Interface.IStreamingBackend && !m_options.DisableStreamingTransfers) { - Func getFileHash; // extended to use stacked streams using (var fs = System.IO.File.OpenWrite(dlTarget)) - using (var hs = GetFileHasherStream(fs, System.Security.Cryptography.CryptoStreamMode.Write, out getFileHash)) + using (var hasher = VolumeHashFactory.CreateHasher()) + using (var hs = GetFileHasherStream(fs, System.Security.Cryptography.CryptoStreamMode.Write, hasher, out var getFileHash)) using (var ss = new ShaderStream(hs, true)) { using (var ts = new ThrottledStream(ss, 0, m_options.MaxDownloadPrSecond)) @@ -1482,7 +1479,7 @@ namespace Duplicati.Library.Main { if (!m_thread.Join(TimeSpan.FromSeconds(10))) { - m_thread.Abort(); + m_thread.Interrupt(); m_thread.Join(TimeSpan.FromSeconds(10)); } diff --git a/Duplicati/Library/Main/Controller.cs b/Duplicati/Library/Main/Controller.cs index b6fd1618e..8b1d71cf6 100644 --- a/Duplicati/Library/Main/Controller.cs +++ b/Duplicati/Library/Main/Controller.cs @@ -1147,7 +1147,7 @@ namespace Duplicati.Library.Main var t = m_currentTaskThread; if (t != null) - t.Abort(); + t.Interrupt(); } public long MaxUploadSpeed diff --git a/Duplicati/Library/Main/Database/LocalDatabase.cs b/Duplicati/Library/Main/Database/LocalDatabase.cs index e34ccbe23..73798a9dc 100644 --- a/Duplicati/Library/Main/Database/LocalDatabase.cs +++ b/Duplicati/Library/Main/Database/LocalDatabase.cs @@ -76,7 +76,16 @@ namespace Duplicati.Library.Main.Database var c = Duplicati.Library.SQLiteHelper.SQLiteLoader.LoadConnection(path); - Library.SQLiteHelper.DatabaseUpgrader.UpgradeDatabase(c, path, typeof(LocalDatabase)); + try + { + Library.SQLiteHelper.DatabaseUpgrader.UpgradeDatabase(c, path, typeof(LocalDatabase)); + } + catch + { + //Don't leak database connections when something goes wrong + c.Dispose(); + throw; + } return c; } diff --git a/Duplicati/Library/Main/Database/LocalRepairDatabase.cs b/Duplicati/Library/Main/Database/LocalRepairDatabase.cs index daf7d310a..d601da186 100644 --- a/Duplicati/Library/Main/Database/LocalRepairDatabase.cs +++ b/Duplicati/Library/Main/Database/LocalRepairDatabase.cs @@ -22,6 +22,8 @@ using System; using System.Linq; using System.Collections.Generic; +using System.Security.Cryptography; +using Duplicati.Library.Utility; namespace Duplicati.Library.Main.Database { @@ -343,18 +345,17 @@ namespace Duplicati.Library.Main.Database public void FixMissingBlocklistHashes(string blockhashalgorithm, long blocksize) { - var blockhasher = Library.Utility.HashAlgorithmHelper.Create(blockhashalgorithm); - var hashsize = blockhasher.HashSize / 8; var blocklistbuffer = new byte[blocksize]; - blockhasher.Initialize(); - - var sql = string.Format(@"SELECT * FROM (SELECT ""N"".""BlocksetID"", ((""N"".""BlockCount"" + {0} - 1) / {0}) AS ""BlocklistHashCountExpected"", CASE WHEN ""G"".""BlocklistHashCount"" IS NULL THEN 0 ELSE ""G"".""BlocklistHashCount"" END AS ""BlocklistHashCountActual"" FROM (SELECT ""BlocksetID"", COUNT(*) AS ""BlockCount"" FROM ""BlocksetEntry"" GROUP BY ""BlocksetID"") ""N"" LEFT OUTER JOIN (SELECT ""BlocksetID"", COUNT(*) AS ""BlocklistHashCount"" FROM ""BlocklistHash"" GROUP BY ""BlocksetID"") ""G"" ON ""N"".""BlocksetID"" = ""G"".""BlocksetID"" WHERE ""N"".""BlockCount"" > 1) WHERE ""BlocklistHashCountExpected"" != ""BlocklistHashCountActual""", blocksize / hashsize); - var countsql = @"SELECT COUNT(*) FROM (" + sql + @")"; - using(var tr = m_connection.BeginTransaction()) using(var cmd = m_connection.CreateCommand(tr)) + using(var blockhasher = HashFactory.CreateHasher(blockhashalgorithm)) { + var hashsize = blockhasher.HashSize / 8; + + var sql = string.Format(@"SELECT * FROM (SELECT ""N"".""BlocksetID"", ((""N"".""BlockCount"" + {0} - 1) / {0}) AS ""BlocklistHashCountExpected"", CASE WHEN ""G"".""BlocklistHashCount"" IS NULL THEN 0 ELSE ""G"".""BlocklistHashCount"" END AS ""BlocklistHashCountActual"" FROM (SELECT ""BlocksetID"", COUNT(*) AS ""BlockCount"" FROM ""BlocksetEntry"" GROUP BY ""BlocksetID"") ""N"" LEFT OUTER JOIN (SELECT ""BlocksetID"", COUNT(*) AS ""BlocklistHashCount"" FROM ""BlocklistHash"" GROUP BY ""BlocksetID"") ""G"" ON ""N"".""BlocksetID"" = ""G"".""BlocksetID"" WHERE ""N"".""BlockCount"" > 1) WHERE ""BlocklistHashCountExpected"" != ""BlocklistHashCountActual""", blocksize / hashsize); + var countsql = @"SELECT COUNT(*) FROM (" + sql + @")"; + var itemswithnoblocklisthash = cmd.ExecuteScalarInt64(countsql, 0); if (itemswithnoblocklisthash != 0) { diff --git a/Duplicati/Library/Main/Duplicati.Library.Main.csproj b/Duplicati/Library/Main/Duplicati.Library.Main.csproj index 6e154ac10..725c9a43c 100644 --- a/Duplicati/Library/Main/Duplicati.Library.Main.csproj +++ b/Duplicati/Library/Main/Duplicati.Library.Main.csproj @@ -1,21 +1,33 @@  + net8.0 Library - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -31,13 +43,16 @@ + PreserveNewest + - \ No newline at end of file + + diff --git a/Duplicati/Library/Main/Operation/Backup/StreamBlockSplitter.cs b/Duplicati/Library/Main/Operation/Backup/StreamBlockSplitter.cs index 919515433..1dd4f6666 100644 --- a/Duplicati/Library/Main/Operation/Backup/StreamBlockSplitter.cs +++ b/Duplicati/Library/Main/Operation/Backup/StreamBlockSplitter.cs @@ -28,6 +28,7 @@ using Duplicati.Library.Utility; using System.Linq; using Duplicati.Library.Interface; using System.IO; +using System.Security.Cryptography; namespace Duplicati.Library.Main.Operation.Backup { @@ -52,21 +53,11 @@ namespace Duplicati.Library.Main.Operation.Backup async self => { var blocksize = options.Blocksize; - var filehasher = Duplicati.Library.Utility.HashAlgorithmHelper.Create(options.FileHashAlgorithm); - var blockhasher = Duplicati.Library.Utility.HashAlgorithmHelper.Create(options.BlockHashAlgorithm); var emptymetadata = Utility.WrapMetadata(new Dictionary(), options); var maxmetadatasize = (options.Blocksize / (long) options.BlockhashSize) * options.Blocksize; - if (blockhasher == null) - throw new UserInformationException(Strings.Common.InvalidHashAlgorithm(options.BlockHashAlgorithm), "BlockHashAlgorithmNotSupported"); - if (filehasher == null) - throw new UserInformationException(Strings.Common.InvalidHashAlgorithm(options.FileHashAlgorithm), "FileHashAlgorithmNotSupported"); - - if (!blockhasher.CanReuseTransform) - throw new UserInformationException(Strings.Common.InvalidCryptoSystem(options.BlockHashAlgorithm), "BlockHashAlgorithmNotSupported"); - if (!filehasher.CanReuseTransform) - throw new UserInformationException(Strings.Common.InvalidCryptoSystem(options.FileHashAlgorithm), "FileHashAlgorithmNotSupported"); - + using(var filehasher = HashFactory.CreateHasher(options.FileHashAlgorithm)) + using(var blockhasher = HashFactory.CreateHasher(options.BlockHashAlgorithm)) using (var empty_metadata_stream = new MemoryStream(emptymetadata.Blob)) { while (await taskreader.ProgressAsync) diff --git a/Duplicati/Library/Main/Operation/Common/BackendHandler.cs b/Duplicati/Library/Main/Operation/Common/BackendHandler.cs index c0bbb7f69..5659dc71f 100644 --- a/Duplicati/Library/Main/Operation/Common/BackendHandler.cs +++ b/Duplicati/Library/Main/Operation/Common/BackendHandler.cs @@ -24,6 +24,7 @@ using Duplicati.Library.Interface; using Duplicati.Library.Main.Volumes; using Duplicati.Library.Utility; using System.IO; +using System.Security.Cryptography; namespace Duplicati.Library.Main.Operation.Common { @@ -37,8 +38,6 @@ namespace Duplicati.Library.Main.Operation.Common /// private static readonly string LOGTAG = Logging.Log.LogTagFromType(); - public const string VOLUME_HASH = "SHA256"; - /// /// Data storage for an ongoing backend operation /// @@ -116,7 +115,7 @@ namespace Duplicati.Library.Main.Operation.Common public static string CalculateFileHash(string filename) { using (System.IO.FileStream fs = System.IO.File.OpenRead(filename)) - using (var hasher = Duplicati.Library.Utility.HashAlgorithmHelper.Create(VOLUME_HASH)) + using (var hasher = VolumeHashFactory.CreateHasher()) return Convert.ToBase64String(hasher.ComputeHash(fs)); } diff --git a/Duplicati/Library/Main/Operation/Common/IndexVolumeCreator.cs b/Duplicati/Library/Main/Operation/Common/IndexVolumeCreator.cs index 132a51be6..4a9cc8b79 100644 --- a/Duplicati/Library/Main/Operation/Common/IndexVolumeCreator.cs +++ b/Duplicati/Library/Main/Operation/Common/IndexVolumeCreator.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -23,6 +23,7 @@ using System; using System.IO; using System.Threading.Tasks; using Duplicati.Library.Main.Volumes; +using Duplicati.Library.Utility; namespace Duplicati.Library.Main.Operation.Common { @@ -139,7 +140,7 @@ namespace Duplicati.Library.Main.Operation.Common { public static async Task CreateIndexVolume(string blockname, Options options, Common.DatabaseCommon database) { - using(var h = Duplicati.Library.Utility.HashAlgorithmHelper.Create(options.BlockHashAlgorithm)) + using(var h = HashFactory.CreateHasher(options.BlockHashAlgorithm)) { var w = new IndexVolumeWriter(options); w.VolumeID = await database.RegisterRemoteVolumeAsync(w.RemoteFilename, RemoteVolumeType.Index, RemoteVolumeState.Temporary); @@ -172,7 +173,7 @@ namespace Duplicati.Library.Main.Operation.Common /*public static async Task ReCreateIndexVolume(string selfname, Options options, Repair.RepairDatabase database) { - using(var h = System.Security.Cryptography.HashAlgorithm.Create(options.BlockHashAlgorithm)) + using(var h = HashFactory.CreateHasher(options.BlockHashAlgorithm)) { var w = new IndexVolumeWriter(options); w.SetRemoteFilename(selfname); diff --git a/Duplicati/Library/Main/Operation/RecreateDatabaseHandler.cs b/Duplicati/Library/Main/Operation/RecreateDatabaseHandler.cs index 396072851..e00f5a2e1 100644 --- a/Duplicati/Library/Main/Operation/RecreateDatabaseHandler.cs +++ b/Duplicati/Library/Main/Operation/RecreateDatabaseHandler.cs @@ -21,9 +21,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography; using Duplicati.Library.Interface; using Duplicati.Library.Main.Database; using Duplicati.Library.Main.Volumes; +using Duplicati.Library.Utility; namespace Duplicati.Library.Main.Operation { @@ -350,14 +352,14 @@ namespace Duplicati.Library.Main.Operation // (if yes, we never will be able to do a backup !) if (!m_options.RepairOnlyPaths) { - var hashalg = Library.Utility.HashAlgorithmHelper.Create(m_options.BlockHashAlgorithm); - if (hashalg == null) - throw new UserInformationException(Strings.Common.InvalidHashAlgorithm(m_options.BlockHashAlgorithm), "BlockHashAlgorithmNotSupported"); - var hashsize = hashalg.HashSize / 8; - + var hashsize = 0; //Grab all index files, and update the block table + + using(var hashalg = HashFactory.CreateHasher(m_options.BlockHashAlgorithm)) using(var tr = restoredb.BeginTransaction()) { + hashsize = hashalg.HashSize / 8; + var indexfiles = ( from n in remotefiles where n.FileType == RemoteVolumeType.Index @@ -395,8 +397,8 @@ namespace Duplicati.Library.Main.Operation // No such file if (volumeID < 0) - volumeID = ProbeForMatchingFilename(ref filename, restoredb); - + volumeID = ProbeForMatchingFilename(ref filename, restoredb); + var missing = false; // Still broken, register a missing item if (volumeID < 0) @@ -404,7 +406,7 @@ namespace Duplicati.Library.Main.Operation var p = VolumeBase.ParseFilename(filename); if (p == null) throw new Exception(string.Format("Unable to parse filename: {0}", filename)); - Logging.Log.WriteErrorMessage(LOGTAG, "MissingFileDetected", null, "Remote file referenced as {0} by {1}, but not found in list, registering a missing remote file", filename, sf.Name); + Logging.Log.WriteErrorMessage(LOGTAG, "MissingFileDetected", null, "Remote file referenced as {0} by {1}, but not found in list, registering a missing remote file", filename, sf.Name); missing = true; volumeID = restoredb.RegisterRemoteVolume(filename, p.FileType, RemoteVolumeState.Temporary, tr); } @@ -555,9 +557,9 @@ namespace Duplicati.Library.Main.Operation } } - backend.WaitForComplete(restoredb, null); - - // In some cases we have a stale reference from an index file to a deleted block file + backend.WaitForComplete(restoredb, null); + + // In some cases we have a stale reference from an index file to a deleted block file if (!m_options.UnittestMode) restoredb.CleanupMissingVolumes(); diff --git a/Duplicati/Library/Main/Operation/RepairHandler.cs b/Duplicati/Library/Main/Operation/RepairHandler.cs index 9183aa565..98b4f60d3 100644 --- a/Duplicati/Library/Main/Operation/RepairHandler.cs +++ b/Duplicati/Library/Main/Operation/RepairHandler.cs @@ -21,10 +21,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography; using System.Text; using Duplicati.Library.Interface; using Duplicati.Library.Main.Database; using Duplicati.Library.Main.Volumes; +using Duplicati.Library.Utility; namespace Duplicati.Library.Main.Operation { @@ -137,14 +139,8 @@ namespace Duplicati.Library.Main.Operation var tp = FilelistProcessor.RemoteListAnalysis(backend, m_options, db, m_result.BackendWriter, null); var buffer = new byte[m_options.Blocksize]; - var blockhasher = Library.Utility.HashAlgorithmHelper.Create(m_options.BlockHashAlgorithm); - var hashsize = blockhasher.HashSize / 8; + var hashsize = HashFactory.HashSizeBytes(m_options.BlockHashAlgorithm); - if (blockhasher == null) - throw new UserInformationException(Strings.Common.InvalidHashAlgorithm(m_options.BlockHashAlgorithm), "BlockHashAlgorithmNotSupported"); - if (!blockhasher.CanReuseTransform) - throw new UserInformationException(Strings.Common.InvalidCryptoSystem(m_options.BlockHashAlgorithm), "BlockHashAlgorithmNotSupported"); - var progress = 0; var targetProgess = tp.ExtraVolumes.Count() + tp.MissingVolumes.Count() + tp.VerificationRequiredVolumes.Count(); @@ -168,7 +164,7 @@ namespace Duplicati.Library.Main.Operation if (tp.VerificationRequiredVolumes.Any()) { using(var testdb = new LocalTestDatabase(db)) - { + { foreach(var n in tp.VerificationRequiredVolumes) try { @@ -333,27 +329,28 @@ namespace Duplicati.Library.Main.Operation newEntry = w; w.SetRemoteFilename(n.Name); - var h = Library.Utility.HashAlgorithmHelper.Create(m_options.BlockHashAlgorithm); + using(var h = HashFactory.CreateHasher(m_options.BlockHashAlgorithm)) { - foreach (var blockvolume in db.GetBlockVolumesFromIndexName(n.Name)) - { - w.StartVolume(blockvolume.Name); - var volumeid = db.GetRemoteVolumeID(blockvolume.Name); + foreach (var blockvolume in db.GetBlockVolumesFromIndexName(n.Name)) + { + w.StartVolume(blockvolume.Name); + var volumeid = db.GetRemoteVolumeID(blockvolume.Name); - foreach (var b in db.GetBlocks(volumeid)) - w.AddBlock(b.Hash, b.Size); + foreach (var b in db.GetBlocks(volumeid)) + w.AddBlock(b.Hash, b.Size); - w.FinishVolume(blockvolume.Hash, blockvolume.Size); + w.FinishVolume(blockvolume.Hash, blockvolume.Size); - if (m_options.IndexfilePolicy == Options.IndexFileStrategy.Full) - foreach (var b in db.GetBlocklists(volumeid, m_options.Blocksize, hashsize)) - { - var bh = Convert.ToBase64String(h.ComputeHash(b.Item2, 0, b.Item3)); - if (bh != b.Item1) - throw new Exception(string.Format("Internal consistency check failed, generated index block has wrong hash, {0} vs {1}", bh, b.Item1)); + if (m_options.IndexfilePolicy == Options.IndexFileStrategy.Full) + foreach (var b in db.GetBlocklists(volumeid, m_options.Blocksize, hashsize)) + { + var bh = Convert.ToBase64String(h.ComputeHash(b.Item2, 0, b.Item3)); + if (bh != b.Item1) + throw new Exception(string.Format("Internal consistency check failed, generated index block has wrong hash, {0} vs {1}", bh, b.Item1)); - w.WriteBlocklist(b.Item1, b.Item2, 0, b.Item3); - } + w.WriteBlocklist(b.Item1, b.Item2, 0, b.Item3); + } + } } w.Close(); @@ -393,12 +390,14 @@ namespace Duplicati.Library.Main.Operation f.Position = offset; if (size == Library.Utility.Utility.ForceStreamRead(f, buffer, size)) { - var newhash = Convert.ToBase64String(blockhasher.ComputeHash(buffer, 0, size)); - if (newhash == hash) - { - if (mbl.SetBlockRestored(hash, size)) - w.AddBlock(hash, buffer, 0, size, Duplicati.Library.Interface.CompressionHint.Default); - break; + using(var blockhasher = HashFactory.CreateHasher(m_options.BlockHashAlgorithm)) { + var newhash = Convert.ToBase64String(blockhasher.ComputeHash(buffer, 0, size)); + if (newhash == hash) + { + if (mbl.SetBlockRestored(hash, size)) + w.AddBlock(hash, buffer, 0, size, Duplicati.Library.Interface.CompressionHint.Default); + break; + } } } } diff --git a/Duplicati/Library/Main/Operation/RestoreHandler.cs b/Duplicati/Library/Main/Operation/RestoreHandler.cs index a53a248c5..a8698f741 100644 --- a/Duplicati/Library/Main/Operation/RestoreHandler.cs +++ b/Duplicati/Library/Main/Operation/RestoreHandler.cs @@ -26,6 +26,8 @@ using Duplicati.Library.Interface; using Duplicati.Library.Common.IO; using Duplicati.Library.Main.Database; using Duplicati.Library.Main.Volumes; +using System.Security.Cryptography; +using Duplicati.Library.Utility; namespace Duplicati.Library.Main.Operation { @@ -162,8 +164,8 @@ namespace Duplicati.Library.Main.Operation var blocksize = options.Blocksize; var updateCounter = 0L; var fullblockverification = options.FullBlockVerification; - var blockhasher = fullblockverification ? Library.Utility.HashAlgorithmHelper.Create(options.BlockHashAlgorithm) : null; - + + using(var blockhasher = HashFactory.CreateHasher(options.BlockHashAlgorithm)) using (var blockmarker = database.CreateBlockMarker()) using(var volumekeeper = database.GetMissingBlockData(blocks, options.Blocksize)) { @@ -200,7 +202,6 @@ namespace Duplicati.Library.Main.Operation var valid = !fullblockverification; if (!valid) { - blockhasher.Initialize(); var key = Convert.ToBase64String(blockhasher.ComputeHash(blockbuffer, 0, size)); if (targetblock.Key == key) valid = true; @@ -319,18 +320,6 @@ namespace Duplicati.Library.Main.Operation Utility.UpdateOptionsFromDb(database, m_options); Utility.VerifyParameters(database, m_options); m_blockbuffer = new byte[m_options.Blocksize]; - - var blockhasher = Library.Utility.HashAlgorithmHelper.Create(m_options.BlockHashAlgorithm); - var filehasher = Library.Utility.HashAlgorithmHelper.Create(m_options.FileHashAlgorithm); - if (blockhasher == null) - throw new UserInformationException(Strings.Common.InvalidHashAlgorithm(m_options.BlockHashAlgorithm), "BlockHashAlgorithmNotSupported"); - if (!blockhasher.CanReuseTransform) - throw new UserInformationException(Strings.Common.InvalidCryptoSystem(m_options.BlockHashAlgorithm), "BlockHashAlgorithmNotSupported"); - - if (filehasher == null) - throw new UserInformationException(Strings.Common.InvalidHashAlgorithm(m_options.FileHashAlgorithm), "FileHashAlgorithmNotSupported"); - if (!filehasher.CanReuseTransform) - throw new UserInformationException(Strings.Common.InvalidCryptoSystem(m_options.FileHashAlgorithm), "FileHashAlgorithmNotSupported"); if (!m_options.NoBackendverification) { @@ -350,11 +339,14 @@ namespace Duplicati.Library.Main.Operation //If we are patching an existing target folder, do not touch stuff that is already updated m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_ScanForExistingFiles); + using(var blockhasher = HashFactory.CreateHasher(m_options.BlockHashAlgorithm)) + using(var filehasher = HashFactory.CreateHasher(m_options.FileHashAlgorithm)) using(new Logging.Timer(LOGTAG, "ScanForExistingTargetBlocks", "ScanForExistingTargetBlocks")) ScanForExistingTargetBlocks(database, m_blockbuffer, blockhasher, filehasher, m_options, result); //Look for existing blocks in the original source files only - using(new Logging.Timer(LOGTAG, "ScanForExistingSourceBlocksFast", "ScanForExistingSourceBlocksFast")) + using (var blockhasher = HashFactory.CreateHasher(m_options.BlockHashAlgorithm)) + using (new Logging.Timer(LOGTAG, "ScanForExistingSourceBlocksFast", "ScanForExistingSourceBlocksFast")) if (!m_options.NoLocalBlocks && !string.IsNullOrEmpty(m_options.Restorepath)) { m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_ScanForLocalBlocks); @@ -371,7 +363,8 @@ namespace Duplicati.Library.Main.Operation if (m_options.PatchWithLocalBlocks) { m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_PatchWithLocalBlocks); - using(new Logging.Timer(LOGTAG, "PatchWithLocalBlocks", "PatchWithLocalBlocks")) + using (var blockhasher = HashFactory.CreateHasher(m_options.BlockHashAlgorithm)) + using (new Logging.Timer(LOGTAG, "PatchWithLocalBlocks", "PatchWithLocalBlocks")) ScanForExistingSourceBlocks(database, m_options, m_blockbuffer, blockhasher, result, metadatastorage); } @@ -445,9 +438,6 @@ namespace Duplicati.Library.Main.Operation if (!m_options.SkipMetadata) ApplyStoredMetadata(m_options, metadatastorage); - // Reset the filehasher if it was used to verify existing files - filehasher.Initialize(); - if (m_result.TaskControlRendevouz() == TaskControlState.Stop) return; @@ -456,7 +446,8 @@ namespace Duplicati.Library.Main.Operation if (m_options.PerformRestoredFileVerification) { // After all blocks in the files are restored, verify the file hash - using(new Logging.Timer(LOGTAG, "RestoreVerification", "RestoreVerification")) + using (var filehasher = HashFactory.CreateHasher(m_options.FileHashAlgorithm)) + using (new Logging.Timer(LOGTAG, "RestoreVerification", "RestoreVerification")) foreach(var file in database.GetFilesToRestore(true)) { try diff --git a/Duplicati/Library/Main/Operation/TestHandler.cs b/Duplicati/Library/Main/Operation/TestHandler.cs index df4bd15b2..b3334518e 100644 --- a/Duplicati/Library/Main/Operation/TestHandler.cs +++ b/Duplicati/Library/Main/Operation/TestHandler.cs @@ -24,6 +24,8 @@ using Duplicati.Library.Main.Database; using System.Linq; using System.Collections.Generic; using Duplicati.Library.Interface; +using System.Security.Cryptography; +using Duplicati.Library.Utility; namespace Duplicati.Library.Main.Operation { @@ -214,14 +216,7 @@ namespace Duplicati.Library.Main.Operation /// A value between 0 and 1 that indicates how many blocks are tested in a dblock file public static KeyValuePair>> TestVolumeInternals(LocalTestDatabase db, IRemoteVolume vol, string tf, Options options, double sample_percent) { - var blockhasher = Library.Utility.HashAlgorithmHelper.Create(options.BlockHashAlgorithm); - - if (blockhasher == null) - throw new UserInformationException(Strings.Common.InvalidHashAlgorithm(options.BlockHashAlgorithm), "BlockHashAlgorithmInvalid"); - if (!blockhasher.CanReuseTransform) - throw new UserInformationException(Strings.Common.InvalidCryptoSystem(options.BlockHashAlgorithm), "BlockHashAlgorithmInvalid"); - - var hashsize = blockhasher.HashSize / 8; + var hashsize = HashFactory.HashSizeBytes(options.BlockHashAlgorithm); var parsedInfo = Volumes.VolumeBase.ParseFilename(vol.Name); sample_percent = Math.Min(1, Math.Max(sample_percent, 0.01)); @@ -267,6 +262,7 @@ namespace Duplicati.Library.Main.Operation return new KeyValuePair>>(vol.Name, combined.ToList()); case RemoteVolumeType.Blocks: + using (var blockhasher = HashFactory.CreateHasher(options.BlockHashAlgorithm)) using (var bl = db.CreateBlocklist(vol.Name)) using (var rd = new Volumes.BlockVolumeReader(parsedInfo.CompressionModule, tf, options)) { diff --git a/Duplicati/Library/Main/Options.cs b/Duplicati/Library/Main/Options.cs index 207bcf0f6..163e4e0b0 100644 --- a/Duplicati/Library/Main/Options.cs +++ b/Duplicati/Library/Main/Options.cs @@ -22,6 +22,7 @@ using System; using System.Linq; using System.Collections.Generic; +using System.Security.Cryptography; using Duplicati.Library.Interface; using Duplicati.Library.Utility; using System.Globalization; @@ -190,25 +191,6 @@ namespace Duplicati.Library.Main } - private static string[] GetSupportedHashes() - { - var r = new List(); - foreach (var h in new string[] { "SHA1", "MD5", "SHA256", "SHA384", "SHA512" }) - { - try - { - var p = System.Security.Cryptography.HashAlgorithm.Create(h); - if (p != null) - r.Add(h); - } - catch - { - } - } - - return r.ToArray(); - } - private static readonly string DEFAULT_COMPRESSED_EXTENSION_FILE = System.IO.Path.Combine(Duplicati.Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "default_compressed_extensions.txt"); /// @@ -383,8 +365,8 @@ namespace Duplicati.Library.Main new CommandLineArgument("dry-run", CommandLineArgument.ArgumentType.Boolean, Strings.Options.DryrunShort, Strings.Options.DryrunLong, "false", new string[] { "dryrun" }), - new CommandLineArgument("block-hash-algorithm", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.BlockhashalgorithmShort, Strings.Options.BlockhashalgorithmLong, DEFAULT_BLOCK_HASH_ALGORITHM, null, GetSupportedHashes()), - new CommandLineArgument("file-hash-algorithm", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.FilehashalgorithmShort, Strings.Options.FilehashalgorithmLong, DEFAULT_FILE_HASH_ALGORITHM, null, GetSupportedHashes()), + new CommandLineArgument("block-hash-algorithm", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.BlockhashalgorithmShort, Strings.Options.BlockhashalgorithmLong, DEFAULT_BLOCK_HASH_ALGORITHM, null, HashFactory.GetSupportedHashes()), + new CommandLineArgument("file-hash-algorithm", CommandLineArgument.ArgumentType.Enumeration, Strings.Options.FilehashalgorithmShort, Strings.Options.FilehashalgorithmLong, DEFAULT_FILE_HASH_ALGORITHM, null, HashFactory.GetSupportedHashes()), new CommandLineArgument("no-auto-compact", CommandLineArgument.ArgumentType.Boolean, Strings.Options.NoautocompactShort, Strings.Options.NoautocompactLong, "false"), new CommandLineArgument("small-file-size", CommandLineArgument.ArgumentType.Size, Strings.Options.SmallfilesizeShort, Strings.Options.SmallfilesizeLong), @@ -1339,7 +1321,7 @@ namespace Duplicati.Library.Main get { if (m_cachedBlockHashSize.Key != BlockHashAlgorithm) - m_cachedBlockHashSize = new KeyValuePair(BlockHashAlgorithm, Duplicati.Library.Utility.HashAlgorithmHelper.Create(BlockHashAlgorithm).HashSize / 8); + m_cachedBlockHashSize = new KeyValuePair(BlockHashAlgorithm, HashFactory.HashSizeBytes(BlockHashAlgorithm)); return m_cachedBlockHashSize.Value; } diff --git a/Duplicati/Library/Main/ResultClasses.cs b/Duplicati/Library/Main/ResultClasses.cs index f5d137cab..7ac2bbff0 100644 --- a/Duplicati/Library/Main/ResultClasses.cs +++ b/Duplicati/Library/Main/ResultClasses.cs @@ -503,7 +503,7 @@ namespace Duplicati.Library.Main // If we are aborted, throw exception if (m_controlState == TaskControlState.Abort) { - System.Threading.Thread.CurrentThread.Abort(); + System.Threading.Thread.CurrentThread.Interrupt(); // For some reason, aborting the current thread does not always throw an exception throw new CancelException(); diff --git a/Duplicati/Library/Main/Utility.cs b/Duplicati/Library/Main/Utility.cs index 52c95d2ca..fd5c8de06 100644 --- a/Duplicati/Library/Main/Utility.cs +++ b/Duplicati/Library/Main/Utility.cs @@ -22,9 +22,11 @@ using System; using System.Collections.Generic; using System.IO; +using System.Security.Cryptography; using Newtonsoft.Json; using System.Text; using Duplicati.Library.Main.Database; +using Duplicati.Library.Utility; namespace Duplicati.Library.Main { @@ -54,7 +56,7 @@ namespace Duplicati.Library.Main using (var ms = new System.IO.MemoryStream()) using (var w = new StreamWriter(ms, Encoding.UTF8)) - using(var filehasher = Library.Utility.HashAlgorithmHelper.Create(options.FileHashAlgorithm)) + using(var filehasher = HashFactory.CreateHasher(options.FileHashAlgorithm)) { if (filehasher == null) throw new Duplicati.Library.Interface.UserInformationException(Strings.Common.InvalidHashAlgorithm(options.FileHashAlgorithm), "FileHashAlgorithmNotSupported"); diff --git a/Duplicati/Library/Main/VolumeHashFactory.cs b/Duplicati/Library/Main/VolumeHashFactory.cs new file mode 100644 index 000000000..470bd31c7 --- /dev/null +++ b/Duplicati/Library/Main/VolumeHashFactory.cs @@ -0,0 +1,12 @@ +using System.Security.Cryptography; + +namespace Duplicati.Library.Main +{ + static class VolumeHashFactory + { + public static HashAlgorithm CreateHasher() + { + return SHA256.Create(); + } + } +} diff --git a/Duplicati/Library/Modules/Builtin/CheckMonoSSL.cs b/Duplicati/Library/Modules/Builtin/CheckMonoSSL.cs deleted file mode 100644 index d77f2aa7f..000000000 --- a/Duplicati/Library/Modules/Builtin/CheckMonoSSL.cs +++ /dev/null @@ -1,215 +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.X509Certificates; - -namespace Duplicati.Library.Modules.Builtin -{ - public class CheckMonoSSL : Duplicati.Library.Interface.IGenericModule, Duplicati.Library.Interface.IWebModule - { - private static readonly string LOGTAG = Logging.Log.LogTagFromType(); - - public CheckMonoSSL() - { - } - - private int CheckStore(StoreName storename, StoreLocation storelocation) - { - X509Store store = null; - try - { - store = new X509Store(storename, storelocation); - store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); - return store.Certificates.Count; - } - catch - { - } - finally - { - if (store != null) - try { store.Close(); } - catch { } - } - - return 0; - } - - // Prevent inlining, so we can catch loader errors from the caller - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] - private int CheckMonoCerts() - { - // The __MonoCS__ compiler directive is no longer supported. - // We would like to query the x509 store, but this will make it - // hard to compile the project on Windows. - // For this reason, we use reflection to extract the counts - - // Code without reflection looks like: - - //return - // Mono.Security.X509.X509StoreManager.LocalMachine.TrustedRoot.Certificates.Count + - // Mono.Security.X509.X509StoreManager.CurrentUser.TrustedRoot.Certificates.Count + - // Mono.Security.X509.X509StoreManager.TrustedRootCertificates.Count + - // 0; - - System.Reflection.Assembly asm = null; - - try { asm = System.Reflection.Assembly.Load("Mono.Security"); } - catch { } - - var x509Store = asm?.GetType("Mono.Security.X509.X509StoreManager"); - if (x509Store == null) - return 0; - - var localMachine = x509Store.GetProperty("LocalMachine")?.GetValue(null); - var currentUser = x509Store.GetProperty("CurrentUser")?.GetValue(null); - var trustedRoot = x509Store.GetProperty("TrustedRootCertificates")?.GetValue(null); - - var count = 0; - - var tr = localMachine?.GetType().GetProperty("TrustedRoot")?.GetValue(localMachine); - var crt = tr?.GetType().GetProperty("Certificates").GetValue(tr); - var cnt = crt?.GetType().GetProperty("Count")?.GetValue(crt); - if (cnt is int) - count += (int)cnt; - - tr = currentUser?.GetType().GetProperty("TrustedRoot")?.GetValue(currentUser); - crt = tr?.GetType().GetProperty("Certificates").GetValue(tr); - cnt = crt?.GetType().GetProperty("Count")?.GetValue(crt); - if (cnt is int) - count += (int)cnt; - - - cnt = trustedRoot?.GetType().GetProperty("Count")?.GetValue(trustedRoot); - if (cnt is int) - count += (int)cnt; - - return count; - - } - - private int CheckForInstalledCerts() - { - var count = - CheckStore(StoreName.Root, StoreLocation.CurrentUser) + - CheckStore(StoreName.AuthRoot, StoreLocation.CurrentUser) + - CheckStore(StoreName.CertificateAuthority, StoreLocation.CurrentUser) + - CheckStore(StoreName.Root, StoreLocation.LocalMachine) + - CheckStore(StoreName.AuthRoot, StoreLocation.LocalMachine) + - CheckStore(StoreName.CertificateAuthority, StoreLocation.LocalMachine); - - try { count += CheckMonoCerts(); } - catch { } - - return count; - } - -#region IGenericModule implementation - - public string Key { get { return "check-mono-ssl"; } } - public string DisplayName { get { return Strings.CheckMonoSSL.Displayname; } } - public string Description { get { return Strings.CheckMonoSSL.Description; } } - public bool LoadAsDefault { get { return true; } } - public IList SupportedCommands { get { return null; } } - - public void Configure(IDictionary commandlineOptions) - { - //Ensure the setup is valid, could throw an exception - if (!commandlineOptions.ContainsKey("main-action") || !Library.Utility.Utility.IsMono) - return; - - if (CheckForInstalledCerts() == 0) - { - // Do not output this warning, recent Mono packages seems to work - if (Library.Utility.Utility.MonoVersion < new Version(6, 0)) - Duplicati.Library.Logging.Log.WriteWarningMessage(LOGTAG, "MissingCerts", null, Strings.CheckMonoSSL.ErrorMessage); - } - } -#endregion - -#region IWebModule implementation - - private const string KEY_CONFIGTYPE = "mono-ssl-config"; - private const ConfigType DEFAULT_CONFIG_TYPE = ConfigType.List; - - private enum ConfigType - { - List, - Install, - Test - } - - - public IDictionary Execute(IDictionary options) - { - string k; - options.TryGetValue(KEY_CONFIGTYPE, out k); - - ConfigType ct; - if (!Enum.TryParse(k, true, out ct)) - ct = DEFAULT_CONFIG_TYPE; - - var d = new Dictionary(); - - switch (ct) - { - case ConfigType.Install: - - d["error"] = "mozroots.exe install is deprecated"; - d["count"] = CheckForInstalledCerts().ToString(); - break; - - case ConfigType.Test: - try - { - var req = System.Net.WebRequest.CreateHttp("https://updates.duplicati.com"); - req.Method = "HEAD"; - req.AllowAutoRedirect = false; - - using (var resp = (System.Net.HttpWebResponse)req.GetResponse()) - { - d["status"] = resp.StatusDescription; - d["status_code"] = ((int)resp.StatusCode).ToString(); - } - } - catch(Exception ex) - { - d["error"] = ex.ToString(); - } - break; - - case ConfigType.List: - default: - d["count"] = CheckForInstalledCerts().ToString(); - d["mono"] = Library.Utility.Utility.IsMono.ToString(); - d["message"] = Strings.CheckMonoSSL.ErrorMessage; - break; - } - - return d; - } - -#endregion - } -} - diff --git a/Duplicati/Library/Modules/Builtin/Duplicati.Library.Modules.Builtin.csproj b/Duplicati/Library/Modules/Builtin/Duplicati.Library.Modules.Builtin.csproj index a9780fb39..e0a6e618b 100644 --- a/Duplicati/Library/Modules/Builtin/Duplicati.Library.Modules.Builtin.csproj +++ b/Duplicati/Library/Modules/Builtin/Duplicati.Library.Modules.Builtin.csproj @@ -1,40 +1,39 @@  + net8.0 Library + - - DnsLite.cs - + + + + + + + - - - + + + - - - - - PreserveNewest - + PreserveNewest + - - - - - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + \ No newline at end of file diff --git a/Duplicati/Library/Modules/Builtin/SendJabberMessage.cs b/Duplicati/Library/Modules/Builtin/SendJabberMessage.cs index e1cd01d34..9208045c1 100644 --- a/Duplicati/Library/Modules/Builtin/SendJabberMessage.cs +++ b/Duplicati/Library/Modules/Builtin/SendJabberMessage.cs @@ -193,7 +193,7 @@ namespace Duplicati.Library.Modules.Builtin return base.ReplaceTemplate(input, result, subjectline); } - protected override void SendMessage(string subject, string body) + protected override async void SendMessage(string subject, string body) { var uri = new Library.Utility.Uri(m_username.Contains("://") ? m_username : "http://" + m_username); diff --git a/Duplicati/Library/Modules/Builtin/SendMail.cs b/Duplicati/Library/Modules/Builtin/SendMail.cs index 1aab12804..6f03d99a8 100644 --- a/Duplicati/Library/Modules/Builtin/SendMail.cs +++ b/Duplicati/Library/Modules/Builtin/SendMail.cs @@ -26,7 +26,6 @@ using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; -using DnsLib; using Duplicati.Library.Interface; using Duplicati.Library.Logging; using Duplicati.Library.Utility; @@ -35,6 +34,7 @@ using System.Net.Sockets; using MailKit.Net.Smtp; using MimeKit; using Duplicati.Library.Modules.Builtin.ResultSerialization; +using DnsClient; namespace Duplicati.Library.Modules.Builtin { @@ -251,42 +251,10 @@ namespace Duplicati.Library.Modules.Builtin List servers = null; if (string.IsNullOrEmpty(m_server)) { - var dnslite = new DnsLib.DnsLite(); - var dnslist = new List(); + var dnsclient = new LookupClient(); + var records = dnsclient.Query(toMailDomain, QueryType.MX).Answers.MxRecords(); - //Grab all IPv4 addresses - foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces()) - try - { - foreach (IPAddress dnsAddress in networkInterface.GetIPProperties().DnsAddresses) - if (dnsAddress.AddressFamily == AddressFamily.InterNetwork) - dnslist.Add(dnsAddress.ToString()); - } - catch (Exception ex) { Logging.Log.WriteExplicitMessage(LOGTAG, "DNSServerLookupFailure", ex, "Failed to get DNS servers from network interface"); } - - dnslist = dnslist.Distinct().ToList(); - - // If we have no DNS servers, try Google and OpenDNS - if (dnslist.Count == 0) - { - // https://developers.google.com/speed/public-dns/ - dnslist.Add("8.8.8.8"); - dnslist.Add("8.8.4.4"); - - //http://www.opendns.com/opendns-ip-addresses/ - dnslist.Add("208.67.222.222"); - dnslist.Add("208.67.220.220"); - } - - var records = new List(); - foreach (var s in dnslist) - { - var res = dnslite.getMXRecords(toMailDomain, s); - if (res != null) - records.AddRange(res.OfType()); - } - - servers = records.OrderBy(record => record.preference).Select(x => "smtp://" + x.exchange).Distinct().ToList(); + servers = records.OrderBy(record => record.Preference).Select(x => "smtp://" + x.Exchange).Distinct().ToList(); if (servers.Count == 0) throw new IOException(Strings.SendMail.FailedToLookupMXServer(OPTION_SERVER)); } diff --git a/Duplicati/Library/Modules/Builtin/run-script-example.sh b/Duplicati/Library/Modules/Builtin/run-script-example.sh old mode 100644 new mode 100755 diff --git a/Duplicati/Library/SQLiteHelper/DatabaseUpgrader.cs b/Duplicati/Library/SQLiteHelper/DatabaseUpgrader.cs index 35d12b7d2..018aaf322 100644 --- a/Duplicati/Library/SQLiteHelper/DatabaseUpgrader.cs +++ b/Duplicati/Library/SQLiteHelper/DatabaseUpgrader.cs @@ -142,23 +142,24 @@ namespace Duplicati.Library.SQLiteHelper var asm = eltype.Assembly; string schema; - using (var rd = new System.IO.StreamReader(asm.GetManifestResourceStream(eltype, FOLDER_NAME + "." + SCHEMA_NAME))) + using (var rd = new System.IO.StreamReader(asm.GetManifestResourceStream(eltype, $"{FOLDER_NAME}.{SCHEMA_NAME}"))) schema = rd.ReadToEnd(); //Get updates, and sort them according to version //This enables upgrading through several versions //ea, from 1 to 8, by stepping 2->3->4->5->6->7->8 SortedDictionary upgrades = new SortedDictionary(); - string prefix = eltype.Namespace + "." + FOLDER_NAME + "."; + string prefix = FOLDER_NAME + "."; foreach (string s in asm.GetManifestResourceNames()) { - //The resource name will be "Duplicati.GUI.Database_schema.1.Sample upgrade.sql" + //The resource name will be "Duplicati.Library.Main.Database.Database_schema.1.Sample upgrade.sql" //The number indicates the version that will be upgraded to - if (s.StartsWith(prefix, StringComparison.Ordinal) && !s.Equals(prefix + SCHEMA_NAME)) + //Could be ""Duplicati.Server.Database.Database_schema.1. Add Notifications.sql"" + if ((s.IndexOf(prefix, 0, StringComparison.Ordinal) >= 0) && !s.EndsWith(prefix + SCHEMA_NAME)) { try { - string version = s.Substring(prefix.Length, s.IndexOf(".", prefix.Length + 1, StringComparison.Ordinal) - prefix.Length); + string version = s.Substring(s.IndexOf(prefix) + prefix.Length, s.IndexOf(".", s.IndexOf(prefix) + prefix.Length + 1, StringComparison.Ordinal) - s.IndexOf(prefix) - prefix.Length); int fileversion = int.Parse(version); string prev; diff --git a/Duplicati/Library/SQLiteHelper/Duplicati.Library.SQLiteHelper.csproj b/Duplicati/Library/SQLiteHelper/Duplicati.Library.SQLiteHelper.csproj index e07b1906b..cacc3d1ee 100644 --- a/Duplicati/Library/SQLiteHelper/Duplicati.Library.SQLiteHelper.csproj +++ b/Duplicati/Library/SQLiteHelper/Duplicati.Library.SQLiteHelper.csproj @@ -1,42 +1,27 @@  + net8.0 Library SQLiteHelper - - - - SQLite\win64\System.Data.SQLite.dll - PreserveNewest - - - SQLite\win32\System.Data.SQLite.dll - PreserveNewest - - - SQLite\pinvoke\System.Data.SQLite.dll - PreserveNewest - - - SQLite\win32\SQLite.Interop.dll - PreserveNewest - - - SQLite\win64\SQLite.Interop.dll - PreserveNewest - - + SQLiteHelper for Duplicati + + + + + - - - + - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - \ No newline at end of file + + diff --git a/Duplicati/Library/SQLiteHelper/SQLiteLoader.cs b/Duplicati/Library/SQLiteHelper/SQLiteLoader.cs index c82af6ae3..c021015b3 100644 --- a/Duplicati/Library/SQLiteHelper/SQLiteLoader.cs +++ b/Duplicati/Library/SQLiteHelper/SQLiteLoader.cs @@ -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 System; using System.IO; using Duplicati.Library.Common; using Duplicati.Library.Common.IO; +using System.Data.SQLite; namespace Duplicati.Library.SQLiteHelper { @@ -178,83 +179,7 @@ namespace Duplicati.Library.SQLiteHelper { get { - if (m_type != null) - return m_type; - - var filename = "System.Data.SQLite.dll"; - var basePath = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "SQLite"); - - // Set this to make SQLite preload automatically - Environment.SetEnvironmentVariable("PreLoadSQLite_BaseDirectory", basePath); - - //Default is to use the pinvoke version which requires a native .dll/.so - var assemblyPath = Path.Combine(basePath, "pinvoke"); - var loadMixedModeAssembly = false; - - if (!Duplicati.Library.Utility.Utility.IsMono) - { - //If we run with MS.Net we can use the mixed mode assemblies - if (Environment.Is64BitProcess) - { - if (File.Exists(Path.Combine(Path.Combine(basePath, "win64"), filename))) - { - assemblyPath = Path.Combine(basePath, "win64"); - loadMixedModeAssembly = true; - } - } - else - { - if (File.Exists(Path.Combine(Path.Combine(basePath, "win32"), filename))) - { - assemblyPath = Path.Combine(basePath, "win32"); - loadMixedModeAssembly = true; - } - } - - // If we have a new path, try to force load the mixed-mode assembly for the current architecture - // This can be avoided if the preload in SQLite works, but it is easy to do it here as well - if (loadMixedModeAssembly) - { - try { PInvoke.LoadLibraryEx(Path.Combine(assemblyPath, "SQLite.Interop.dll"), IntPtr.Zero, 0); } - catch (Exception ex) { Logging.Log.WriteExplicitMessage(LOGTAG, "LoadMixedModeSQLiteError", ex, "Failed to load the mixed mode SQLite database: {0}", Path.Combine(assemblyPath, "SQLite.Interop.dll")); } - } - } - else - { - //On Mono, we try to find the Mono version of SQLite - - //This secret environment variable can be used to support older installations - var envvalue = System.Environment.GetEnvironmentVariable("DISABLE_MONO_DATA_SQLITE"); - if (!Utility.Utility.ParseBool(envvalue, envvalue != null)) - { - foreach (var asmversion in new[] { "4.0.0.0", "2.0.0.0" }) - { - var name = string.Format("Mono.Data.Sqlite, Version={0}, Culture=neutral, PublicKeyToken=0738eb9f132ed756", asmversion); - try - { - Type t = System.Reflection.Assembly.Load(name).GetType("Mono.Data.Sqlite.SqliteConnection"); - if (t != null && t.GetInterface("System.Data.IDbConnection", false) != null) - { - Version v = new Version((string)t.GetProperty("SQLiteVersion").GetValue(null, null)); - if (v >= new Version(3, 6, 3)) - { - return m_type = t; - } - } - } - catch(Exception ex) - { - Logging.Log.WriteExplicitMessage(LOGTAG, "FailedToLoadSQLiteAssembly", ex, "Failed to load the SQLite assembly: {0}", name); - } - } - - Logging.Log.WriteVerboseMessage(LOGTAG, "FailedToLoadSQLite", "Failed to load Mono.Data.Sqlite.SqliteConnection, reverting to built-in."); - } - } - - m_type = System.Reflection.Assembly.LoadFile(Path.Combine(assemblyPath, filename)).GetType("System.Data.SQLite.SQLiteConnection"); - - return m_type; + return typeof(SQLiteConnection); } } @@ -333,20 +258,4 @@ namespace Duplicati.Library.SQLiteHelper } } } - - /// - /// Helper class with PInvoke methods - /// - internal static class PInvoke - { - /// - /// Loads the specified module into the address space of the calling process. - /// - /// The library ex. - /// The filename of the module to load. - /// Reserved for future use. - /// Action to take on load. - [System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true)] - public static extern IntPtr LoadLibraryEx(string lpFileName, IntPtr hReservedNull, uint dwFlags); - } } diff --git a/Duplicati/Library/Snapshots/Duplicati.Library.Snapshots.csproj b/Duplicati/Library/Snapshots/Duplicati.Library.Snapshots.csproj index 758d3dacc..3668b576c 100644 --- a/Duplicati/Library/Snapshots/Duplicati.Library.Snapshots.csproj +++ b/Duplicati/Library/Snapshots/Duplicati.Library.Snapshots.csproj @@ -1,16 +1,23 @@  + net8.0 Exe + - - - - - - + + + + + + + + + + + PreserveNewest @@ -22,10 +29,12 @@ PreserveNewest + - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + \ No newline at end of file diff --git a/Duplicati/Library/Snapshots/LinuxSnapshot.cs b/Duplicati/Library/Snapshots/LinuxSnapshot.cs index 03235e9cb..48e2a2a98 100644 --- a/Duplicati/Library/Snapshots/LinuxSnapshot.cs +++ b/Duplicati/Library/Snapshots/LinuxSnapshot.cs @@ -218,7 +218,7 @@ namespace Duplicati.Library.Snapshots /// A string with the combined output of the stdout and stderr private static string ExecuteCommand(string program, string commandline, int expectedExitCode) { - program = System.IO.Path.Combine(System.IO.Path.Combine(AutoUpdater.UpdaterManager.InstalledBaseDir, "lvm-scripts"), program); + program = System.IO.Path.Combine(System.IO.Path.Combine(Duplicati.Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "lvm-scripts"), program); var inf = new ProcessStartInfo(program, commandline) { CreateNoWindow = true, diff --git a/Duplicati/Library/Snapshots/lvm-scripts/create-lvm-snapshot.sh b/Duplicati/Library/Snapshots/lvm-scripts/create-lvm-snapshot.sh old mode 100644 new mode 100755 diff --git a/Duplicati/Library/Snapshots/lvm-scripts/find-volume.sh b/Duplicati/Library/Snapshots/lvm-scripts/find-volume.sh old mode 100644 new mode 100755 diff --git a/Duplicati/Library/Snapshots/lvm-scripts/remove-lvm-snapshot.sh b/Duplicati/Library/Snapshots/lvm-scripts/remove-lvm-snapshot.sh old mode 100644 new mode 100755 diff --git a/Duplicati/Library/UsageReporter/Duplicati.Library.UsageReporter.csproj b/Duplicati/Library/UsageReporter/Duplicati.Library.UsageReporter.csproj index 956809a73..4fd88b9cd 100644 --- a/Duplicati/Library/UsageReporter/Duplicati.Library.UsageReporter.csproj +++ b/Duplicati/Library/UsageReporter/Duplicati.Library.UsageReporter.csproj @@ -1,16 +1,25 @@  + net8.0 Library - - - - - - + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Library/UsageReporter/Reporter.cs b/Duplicati/Library/UsageReporter/Reporter.cs index 41fe79029..cf9ac5f96 100644 --- a/Duplicati/Library/UsageReporter/Reporter.cs +++ b/Duplicati/Library/UsageReporter/Reporter.cs @@ -176,7 +176,7 @@ namespace Duplicati.Library.UsageReporter { if (Cached_MaxReportLevel == null) { - var str = Environment.GetEnvironmentVariable(string.Format(DISABLED_ENVNAME_TEMPLATE, AutoUpdater.AutoUpdateSettings.AppName)); + var str = Environment.GetEnvironmentVariable(string.Format(DISABLED_ENVNAME_TEMPLATE, Duplicati.Library.AutoUpdater.AutoUpdateSettings.AppName)); ReportType tmp; if (string.IsNullOrWhiteSpace(str) || !Enum.TryParse(str, true, out tmp)) Cached_MaxReportLevel = ReportType.Information; diff --git a/Duplicati/Library/Utility/CallContextSettings.cs b/Duplicati/Library/Utility/CallContextSettings.cs index eb522aa1a..23ce60dd2 100644 --- a/Duplicati/Library/Utility/CallContextSettings.cs +++ b/Duplicati/Library/Utility/CallContextSettings.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -321,7 +321,6 @@ namespace Duplicati.Library.Utility /// public ContextGuard() { - CallContext.SetData(contextSettingsType, ID); } diff --git a/Duplicati/Library/Utility/Duplicati.Library.Utility.csproj b/Duplicati/Library/Utility/Duplicati.Library.Utility.csproj index b1db22290..bddb0f487 100644 --- a/Duplicati/Library/Utility/Duplicati.Library.Utility.csproj +++ b/Duplicati/Library/Utility/Duplicati.Library.Utility.csproj @@ -1,15 +1,26 @@  + net8.0 Library + - - - - - - + - \ No newline at end of file + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Duplicati/Library/Utility/HashAlgorithmHelper.cs b/Duplicati/Library/Utility/HashAlgorithmHelper.cs deleted file mode 100644 index fb400eb1f..000000000 --- a/Duplicati/Library/Utility/HashAlgorithmHelper.cs +++ /dev/null @@ -1,54 +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 Duplicati.Library.Utility -{ - /// - /// This class helps picking the fastest hash algorithm implementation, - /// which is what should do, but does not. - /// It falls back to the aforementioned System library when creation of - /// the faster hash function fails. - /// - public static class HashAlgorithmHelper - { - /// - /// Create the hash algorithm with the specified name. - /// - /// The hash algorithm. - /// The name of the algorithm to create. - public static HashAlgorithm Create(string name) - { - try - { - return FasterHashing.FasterHash.Create(name); - } - // Only having OpenSSL 3 installed might trigger this exception. - catch (EntryPointNotFoundException) - { - return HashAlgorithm.Create(name); - } - } - } -} diff --git a/Duplicati/Library/Utility/HashCalculatingStream.cs b/Duplicati/Library/Utility/HashCalculatingStream.cs index b4f29fa86..06dda7208 100644 --- a/Duplicati/Library/Utility/HashCalculatingStream.cs +++ b/Duplicati/Library/Utility/HashCalculatingStream.cs @@ -21,21 +21,11 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography; using System.Text; namespace Duplicati.Library.Utility { - /// - /// A special stream that does on-the-fly MD5 calculations - /// - public class MD5CalculatingStream : HashCalculatingStream - { - public MD5CalculatingStream(System.IO.Stream basestream) - : base(basestream, "MD5") - { - } - } - /// /// A special stream that does on-the-fly hash calculations /// @@ -50,11 +40,6 @@ namespace Duplicati.Library.Utility private byte[] m_hashbuffer = null; private int m_hashbufferLength = 0; - public HashCalculatingStream(System.IO.Stream basestream, string hashalgorithm) - : this(basestream, HashAlgorithmHelper.Create(hashalgorithm)) - { - } - public HashCalculatingStream(System.IO.Stream basestream, System.Security.Cryptography.HashAlgorithm algorithm) : base(basestream) { diff --git a/Duplicati/Library/Utility/HashFactory.cs b/Duplicati/Library/Utility/HashFactory.cs new file mode 100644 index 000000000..043b37c67 --- /dev/null +++ b/Duplicati/Library/Utility/HashFactory.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using Crypto = System.Security.Cryptography; +using System.Text; + +namespace Duplicati.Library.Utility +{ + public static class HashFactory + { + public const string MD5 = "MD5"; + public const string SHA1 = "SHA1"; + public const string SHA256 = "SHA256"; + public const string SHA384 = "SHA384"; + public const string SHA512 = "SHA512"; + + public static string[] GetSupportedHashes() + { + var r = new List(); + foreach (var h in new string[] { "SHA1", "MD5", "SHA256", "SHA384", "SHA512" }) + { + try + { + using (var p = CreateHasher(h)) { + if (p != null) + r.Add(h); + } + } + catch + { + } + } + + return r.ToArray(); + } + + public static Crypto.HashAlgorithm CreateHasher(string algorithm) + { + switch (algorithm.ToUpperInvariant()) + { + case "MD5": + return Crypto.MD5.Create(); + case "SHA1": + return Crypto.SHA1.Create(); + case "SHA256": + return Crypto.SHA256.Create(); + case "SHA384": + return Crypto.SHA384.Create(); + case "SHA512": + return Crypto.SHA512.Create(); + default: + throw new ArgumentException("Unknown algorithm: " + algorithm, nameof(algorithm)); + } + } + + public static int HashSizeBytes(string algorithm) + { + using (var hasher = CreateHasher(algorithm)) + { + return hasher.HashSize / 8; + } + } + } +} diff --git a/Duplicati/Library/Utility/Power/WindowsPowerSupplyState.cs b/Duplicati/Library/Utility/Power/WindowsPowerSupplyState.cs index b531f1f4a..9b3ad30f8 100644 --- a/Duplicati/Library/Utility/Power/WindowsPowerSupplyState.cs +++ b/Duplicati/Library/Utility/Power/WindowsPowerSupplyState.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -24,30 +24,26 @@ namespace Duplicati.Library.Utility.Power { public class WindowsPowerSupplyState : IPowerSupplyState { + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] public PowerSupply.Source GetSource() { try { - // Using reflection to allow building on non-Windows - // PowerLineStatus status = System.Windows.Forms.SystemInformation.PowerStatus.PowerLineStatus; - var powerstatus = System.Type.GetType("System.Windows.Forms.SystemInformation, System.Windows.Forms") - .GetProperty("PowerStatus", BindingFlags.Public | BindingFlags.Static) - .GetValue(null); + var managementScope = new System.Management.ManagementScope(new System.Management.ManagementPath("root\\cimv2")); + var objectQuery = new System.Management.ObjectQuery("SELECT BatteryStatus FROM Win32_Battery"); + var objectSearcher = new System.Management.ManagementObjectSearcher(managementScope, objectQuery); + var objectCollection = objectSearcher.Get(); - var status = powerstatus.GetType() - .GetProperty("PowerLineStatus", BindingFlags.Public | BindingFlags.Instance) - .GetValue(powerstatus) - .ToString(); - - - if (string.Equals(status, "Online", System.StringComparison.OrdinalIgnoreCase)) - { + if (objectCollection.Count == 0) return PowerSupply.Source.AC; - } - if (string.Equals(status == "Offline", System.StringComparison.OrdinalIgnoreCase)) - { - return PowerSupply.Source.Battery; - } + + foreach (System.Management.ManagementObject managementObject in objectCollection) + if (Convert.ToUInt16(managementObject.Properties["BatteryStatus"].Value) == 2) + return PowerSupply.Source.AC; + else + return PowerSupply.Source.Battery; + + return PowerSupply.Source.Unknown; } catch { } diff --git a/Duplicati/Library/AutoUpdater/SignatureReadingStream.cs b/Duplicati/Library/Utility/SignatureReadingStream.cs similarity index 95% rename from Duplicati/Library/AutoUpdater/SignatureReadingStream.cs rename to Duplicati/Library/Utility/SignatureReadingStream.cs index c6b26c6cd..c887b8c68 100644 --- a/Duplicati/Library/AutoUpdater/SignatureReadingStream.cs +++ b/Duplicati/Library/Utility/SignatureReadingStream.cs @@ -18,10 +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; -namespace Duplicati.Library.AutoUpdater +namespace Duplicati.Library.Utility { public class SignatureReadingStream : System.IO.Stream, IDisposable { @@ -29,7 +28,7 @@ namespace Duplicati.Library.AutoUpdater /// The size of the SHA256 output hash in bytes /// /// - internal const int SIGNED_HASH_SIZE = 128; + internal const int SIGNED_HASH_SIZE = 128; /// /// The stream to read from diff --git a/Duplicati/Library/Utility/Utility.cs b/Duplicati/Library/Utility/Utility.cs index d1a5e2974..6a9c41d4d 100644 --- a/Duplicati/Library/Utility/Utility.cs +++ b/Duplicati/Library/Utility/Utility.cs @@ -30,7 +30,7 @@ using System.Text.RegularExpressions; using Duplicati.Library.Common.IO; using Duplicati.Library.Common; using System.Globalization; -using Duplicati.Library.Interface; +using System.Security.Cryptography; namespace Duplicati.Library.Utility { @@ -46,11 +46,6 @@ namespace Duplicati.Library.Utility /// private static bool? CachedIsFSCaseSensitive; - /// - /// Gets the hash algorithm used for calculating a hash - /// - public static string HashAlgorithm => "SHA256"; - /// /// The EPOCH offset (unix style) /// @@ -641,16 +636,6 @@ namespace Duplicati.Library.Utility return a1 == a2; } - /// - /// Calculates the hash of a given stream, and returns the results as an base64 encoded string - /// - /// The stream to calculate the hash for - /// The base64 encoded hash - public static string CalculateHash(Stream stream) - { - return Convert.ToBase64String(HashAlgorithmHelper.Create(HashAlgorithm).ComputeHash(stream)); - } - /// /// Reads a file, attempts to detect encoding /// diff --git a/Duplicati/Library/Utility/WorkerThread.cs b/Duplicati/Library/Utility/WorkerThread.cs index 450a1af9c..dfdd99f2a 100644 --- a/Duplicati/Library/Utility/WorkerThread.cs +++ b/Duplicati/Library/Utility/WorkerThread.cs @@ -234,7 +234,7 @@ namespace Duplicati.Library.Utility { try { - m_thread.Abort(); + m_thread.Interrupt(); m_thread.Join(500); } catch diff --git a/Duplicati/License/Duplicati.License.csproj b/Duplicati/License/Duplicati.License.csproj index c99f2fd30..1e8974f62 100644 --- a/Duplicati/License/Duplicati.License.csproj +++ b/Duplicati/License/Duplicati.License.csproj @@ -1,8 +1,10 @@  + net8.0 Library + licenses\alphavss\Homepage.txt @@ -58,19 +60,24 @@ licenses\Tencentyun\download.txt - PreserveNewest licenses\Tencentyun\LICENSE.txt - PreserveNewest + + + licenses\TLSharp\Homepage.txt + + + licenses\TLSharp\License.txt + + + licenses\TLSharp\licensedata.json licenses\uplink.NET\Homepage.txt - PreserveNewest licenses\uplink.NET\license.txt - PreserveNewest licenses\license.txt @@ -111,14 +118,6 @@ licenses\AWSSDK\license.txt PreserveNewest - - licenses\DnsLite\licensedata.json - PreserveNewest - - - licenses\DnsLite\download.txt - PreserveNewest - licenses\gpg\licensedata.json PreserveNewest @@ -188,15 +187,15 @@ PreserveNewest - licenses\\SharePointPnPSitesCore\download.txt + licenses\SharePointPnP-Sites-Core\download.txt PreserveNewest - licenses\\SharePointPnPSitesCore\license.txt + licenses\SharePointPnP-Sites-Core\license.txt PreserveNewest - licenses\\SharePointPnPSitesCore\licensedata.json + licenses\SharePointPnP-Sites-Core\licensedata.json PreserveNewest @@ -298,6 +297,18 @@ licenses\rumps\licensedata.json PreserveNewest + + licenses\FluentFTP\Homepage.txt + PreserveNewest + + + licenses\FluentFTP\License.txt + PreserveNewest + + + licenses\FluentFTP\licensedata.json + PreserveNewest + licenses\MailKit\Homepage.txt PreserveNewest @@ -346,37 +357,12 @@ licenses\AngularGettext\licensedata.json PreserveNewest - - licenses\FasterHashing\download.txt - PreserveNewest - - - licenses\FasterHashing\license.txt - PreserveNewest - - - licenses\FasterHashing\licensedata.json - PreserveNewest - - - - - licenses\FluentFTP\licensedata.json - PreserveNewest - - - licenses\Tencentyun\licensedata.json - PreserveNewest - - - licenses\uplink.NET\licensedata.json - PreserveNewest - - + licenses\SharpCompress\licensedata.json PreserveNewest - + + @@ -384,4 +370,4 @@ - \ No newline at end of file + diff --git a/Duplicati/Server/Database/Filter.cs b/Duplicati/Server/Database/Filter.cs deleted file mode 100644 index 904b2bf4c..000000000 --- a/Duplicati/Server/Database/Filter.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; - -namespace Duplicati.Server.Database -{ - public class Filter : Duplicati.Server.Serialization.Interface.IFilter - { - public long Order { get; set; } - - public bool Include { get; set; } - - public string Expression { get; set; } - } -} - diff --git a/Duplicati/Server/Database/Notification.cs b/Duplicati/Server/Database/Notification.cs deleted file mode 100644 index 27510a661..000000000 --- a/Duplicati/Server/Database/Notification.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; - -namespace Duplicati.Server.Database -{ - public class Notification : Server.Serialization.Interface.INotification - { - #region INotification implementation - public long ID { get; set; } - public Duplicati.Server.Serialization.NotificationType Type { get; set; } - public string Title { get; set; } - public string Message { get; set; } - public string Exception { get; set; } - public string BackupID { get; set; } - public string Action { get; set; } - public DateTime Timestamp { get; set; } - public string LogEntryID { get; set; } - public string MessageID { get; set; } - public string MessageLogTag { get; set; } - #endregion - } -} - diff --git a/Duplicati/Server/Database/Setting.cs b/Duplicati/Server/Database/Setting.cs deleted file mode 100644 index 3c5cff529..000000000 --- a/Duplicati/Server/Database/Setting.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; - -namespace Duplicati.Server.Database -{ - public class Setting : Duplicati.Server.Serialization.Implementations.Setting - { - } -} - diff --git a/Duplicati/Server/Database/TempFile.cs b/Duplicati/Server/Database/TempFile.cs deleted file mode 100644 index 59f1379a1..000000000 --- a/Duplicati/Server/Database/TempFile.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. - -using System; - -namespace Duplicati.Server.Database -{ - public class TempFile - { - public long ID { get; set; } - public string Origin { get; set; } - public string Path { get; set; } - public DateTime Timestamp { get; set; } - public DateTime Expires { get; set; } - } -} - diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Duplicati.Server.Serialization.csproj b/Duplicati/Server/Duplicati.Server.Serialization/Duplicati.Server.Serialization.csproj index ff2decc46..998a5a3ce 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Duplicati.Server.Serialization.csproj +++ b/Duplicati/Server/Duplicati.Server.Serialization/Duplicati.Server.Serialization.csproj @@ -1,13 +1,20 @@  + net8.0 Library - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + \ No newline at end of file diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs b/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs index ddeeb201d..14c410ee7 100644 --- a/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Serializer.cs @@ -20,8 +20,10 @@ // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -59,6 +61,23 @@ namespace Duplicati.Server.Serialization } } + public static async Task SerializeJsonAsync(System.IO.TextWriter tw, object o, bool preventDispose = false) + { + Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(m_jsonSettings); + StringBuilder sb = new StringBuilder(); + StringWriter sw = new StringWriter(sb); + var jsonWriter = new JsonTextWriter(sw); + using (preventDispose ? null : jsonWriter) + { + jsonWriter.Formatting = m_jsonFormatting; + jsonSerializer.Serialize(jsonWriter, o); + await jsonWriter.FlushAsync(); + } + await sw.FlushAsync(); + await tw.WriteAsync(sb.ToString()); + await tw.FlushAsync(); + } + public static T Deserialize(System.IO.TextReader sr) { Newtonsoft.Json.JsonSerializer jsonSerializer = Newtonsoft.Json.JsonSerializer.Create(m_jsonSettings); diff --git a/Duplicati/Server/Duplicati.Server.csproj b/Duplicati/Server/Duplicati.Server.csproj index caa7608b2..527243931 100644 --- a/Duplicati/Server/Duplicati.Server.csproj +++ b/Duplicati/Server/Duplicati.Server.csproj @@ -1,83 +1,86 @@  + net8.0 Exe - - app.manifest - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\..\thirdparty\HttpServer\HttpServer.dll - \ No newline at end of file + + + Always + Always + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index cc37cde48..c5e405013 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -24,6 +24,8 @@ using System.Globalization; using System.Linq; using Duplicati.Library.Common; using Duplicati.Library.Common.IO; +using Duplicati.Library.RestAPI; +using Duplicati.WebserverCore; namespace Duplicati.Server { @@ -56,7 +58,7 @@ namespace Duplicati.Server /// /// Gets the folder where Duplicati data is stored /// - public static string DataFolder { get; private set; } + public static string DataFolder { get => FIXMEGlobal.DataFolder; private set => FIXMEGlobal.DataFolder = value; } /// /// The single instance @@ -66,27 +68,27 @@ namespace Duplicati.Server /// /// This is the only access to the database /// - public static Database.Connection DataConnection; + public static Database.Connection DataConnection { get => FIXMEGlobal.DataConnection; set => FIXMEGlobal.DataConnection = value; } /// /// This is the lock to be used before manipulating the shared resources /// - public static readonly object MainLock = new object(); + public static object MainLock { get => FIXMEGlobal.MainLock; } /// /// This is the scheduling thread /// - public static Scheduler Scheduler; + public static Scheduler Scheduler { get => FIXMEGlobal.Scheduler; set => FIXMEGlobal.Scheduler = value; } /// /// This is the working thread /// - public static Duplicati.Library.Utility.WorkerThread WorkThread; + public static Duplicati.Library.Utility.WorkerThread WorkThread { get => FIXMEGlobal.WorkThread; set => FIXMEGlobal.WorkThread = value; } /// /// List of completed task results /// - public static readonly List> TaskResultCache = new List>(); + public static List> TaskResultCache { get => FIXMEGlobal.TaskResultCache; } /// /// The maximum number of completed task results to keep in memory @@ -106,22 +108,27 @@ namespace Duplicati.Server /// /// The controller interface for pause/resume and throttle options /// - public static LiveControls LiveControl; + public static LiveControls LiveControl { get => FIXMEGlobal.LiveControl; set => FIXMEGlobal.LiveControl = value; } /// /// The application exit event /// - public static System.Threading.ManualResetEvent ApplicationExitEvent; + public static System.Threading.ManualResetEvent ApplicationExitEvent { get => FIXMEGlobal.ApplicationExitEvent; set => FIXMEGlobal.ApplicationExitEvent = value; } /// /// The webserver instance /// private static WebServer.Server WebServer; + /// + /// Callback to shutdown the modern webserver + /// + private static Action ShutdownModernWebserver; + /// /// The update poll thread. /// - public static UpdatePollThread UpdatePoller; + public static UpdatePollThread UpdatePoller { get => FIXMEGlobal.UpdatePoller; set => FIXMEGlobal.UpdatePoller = value; } /// /// An event that is set once the server is ready to respond to requests @@ -131,12 +138,12 @@ namespace Duplicati.Server /// /// The status event signaler, used to control long polling of status updates /// - public static readonly EventPollNotify StatusEventNotifyer = new EventPollNotify(); + public static EventPollNotify StatusEventNotifyer { get => FIXMEGlobal.StatusEventNotifyer; } /// /// A delegate method for creating a copy of the current progress state /// - public static Func GenerateProgressState; + public static Func GenerateProgressState { get => FIXMEGlobal.GenerateProgressState; set => FIXMEGlobal.GenerateProgressState = value; } /// /// An event ID that increases whenever the database is updated @@ -151,12 +158,12 @@ namespace Duplicati.Server /// /// The log redirect handler /// - public static readonly LogWriteHandler LogHandler = new LogWriteHandler(); + public static LogWriteHandler LogHandler { get => FIXMEGlobal.LogHandler; } /// /// Used to check the origin of the web server (e.g. Tray icon or a stand alone Server) /// - public static string Origin = "Server"; + public static string Origin { get => FIXMEGlobal.Origin; set => FIXMEGlobal.Origin = value; } private static System.Threading.Timer PurgeTempFilesTimer = null; @@ -186,6 +193,27 @@ namespace Duplicati.Server set { DataConnection.ApplicationSettings.ServerPortChanged = value; } } + public static void IncrementLastDataUpdateID() + { + System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); + } + + public static void IncrementLastNotificationUpdateID() + { + System.Threading.Interlocked.Increment(ref Program.LastNotificationUpdateID); + } + + static Program() + { + FIXMEGlobal.IncrementLastDataUpdateID = Program.IncrementLastDataUpdateID; + FIXMEGlobal.PeekLastDataUpdateID = () => Program.LastDataUpdateID; + FIXMEGlobal.IncrementLastNotificationUpdateID = Program.IncrementLastNotificationUpdateID; + FIXMEGlobal.PeekLastNotificationUpdateID = () => Program.LastNotificationUpdateID; + FIXMEGlobal.GetDatabaseConnection = Program.GetDatabaseConnection; + FIXMEGlobal.StartOrStopUsageReporter = Program.StartOrStopUsageReporter; + FIXMEGlobal.UpdateThrottleSpeeds = Program.UpdateThrottleSpeeds; + } + /// /// The main entry point for the application. /// @@ -197,6 +225,10 @@ namespace Duplicati.Server public static int RealMain(string[] _args) { +#if DEBUG + System.Diagnostics.Debugger.Launch(); +#endif + //If we are on Windows, append the bundled "win-tools" programs to the search path //We add it last, to allow the user to override with other versions if (Platform.IsClientWindows) @@ -307,6 +339,7 @@ namespace Duplicati.Server { StatusEventNotifyer.SignalNewEvent(); + ShutdownModernWebserver(); UpdatePoller?.Terminate(); Scheduler?.Terminate(true); WorkThread?.Terminate(true); @@ -315,7 +348,7 @@ namespace Duplicati.Server Library.UsageReporter.Reporter.ShutDown(); - try { PingPongThread?.Abort(); } + try { PingPongThread?.Interrupt(); } catch { } LogHandler?.Dispose(); @@ -333,6 +366,9 @@ namespace Duplicati.Server ServerPortChanged |= WebServer.Port != DataConnection.ApplicationSettings.LastWebserverPort; DataConnection.ApplicationSettings.LastWebserverPort = WebServer.Port; + + var server = new DuplicatiWebserver(); + ShutdownModernWebserver = server.Foo(); } private static void SetWorkerThread() @@ -653,7 +689,7 @@ namespace Duplicati.Server if (!System.IO.Directory.Exists(System.IO.Path.GetDirectoryName(DatabasePath))) System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(DatabasePath)); -#if DEBUG +#if DEBUG || !SQLLITE_WITH_CLOSED_SOURCE_ENCRYPTION //Default is to not use encryption for debugging var useDatabaseEncryption = commandlineOptions.ContainsKey("unencrypted-database") && !Library.Utility.Utility.ParseBool(commandlineOptions["unencrypted-database"], true); #else @@ -663,7 +699,7 @@ namespace Duplicati.Server //Attempt to open the database, handling any encryption present Duplicati.Library.SQLiteHelper.SQLiteLoader.OpenDatabase(con, DatabasePath, useDatabaseEncryption, dbPassword); - Duplicati.Library.SQLiteHelper.DatabaseUpgrader.UpgradeDatabase(con, DatabasePath, typeof(Database.Connection)); + Duplicati.Library.SQLiteHelper.DatabaseUpgrader.UpgradeDatabase(con, DatabasePath, typeof(Duplicati.Library.RestAPI.Database.DatabaseConnectionSchemaMarker)); } catch (Exception ex) { diff --git a/Duplicati/Server/Properties/launchSettings.json b/Duplicati/Server/Properties/launchSettings.json new file mode 100644 index 000000000..b0473b0da --- /dev/null +++ b/Duplicati/Server/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Duplicati.Server": { + "commandName": "Project", + "environmentVariables": { + "AUTOUPDATER_Duplicati_SKIP_UPDATE": "1" + } + } + } +} \ No newline at end of file diff --git a/Duplicati/Service/Duplicati.Service.csproj b/Duplicati/Service/Duplicati.Service.csproj index 350f79bf7..12cbea263 100644 --- a/Duplicati/Service/Duplicati.Service.csproj +++ b/Duplicati/Service/Duplicati.Service.csproj @@ -1,11 +1,19 @@  + net8.0 Exe - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/Tools/Duplicati.Tools.csproj b/Duplicati/Tools/Duplicati.Tools.csproj index ffa3c15a8..d463251dd 100644 --- a/Duplicati/Tools/Duplicati.Tools.csproj +++ b/Duplicati/Tools/Duplicati.Tools.csproj @@ -1,8 +1,10 @@  + net8.0 Library + utility-scripts\DuplicatiVerify.ps1 @@ -45,13 +47,18 @@ PreserveNewest - - PreserveNewest - utility-scripts\pause-resume.py + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Duplicati/UnitTest/BackendToolTests.cs b/Duplicati/UnitTest/BackendToolTests.cs index 77689a36e..1ec94df54 100755 --- a/Duplicati/UnitTest/BackendToolTests.cs +++ b/Duplicati/UnitTest/BackendToolTests.cs @@ -95,4 +95,4 @@ namespace Duplicati.UnitTest } } } -} \ No newline at end of file +} diff --git a/Duplicati/UnitTest/BasicSetupHelper.cs b/Duplicati/UnitTest/BasicSetupHelper.cs index 231101e23..a8be31209 100644 --- a/Duplicati/UnitTest/BasicSetupHelper.cs +++ b/Duplicati/UnitTest/BasicSetupHelper.cs @@ -21,12 +21,14 @@ using System; using NUnit.Framework; +using System.Diagnostics; using System.IO; using System.Collections.Generic; using System.IO.Compression; using Duplicati.Library.Common; using Duplicati.Library.Common.IO; using Duplicati.Library.Utility; +using System.Timers; namespace Duplicati.UnitTest { @@ -35,10 +37,11 @@ namespace Duplicati.UnitTest /// /// The base folder where all data is trashed around /// - protected static readonly string BASEFOLDER = + protected static readonly string BASEFOLDER = Path.GetFullPath( string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("UNITTEST_BASEFOLDER")) - ? Path.Combine(Library.Utility.Utility.HOME_PATH, "duplicati_testdata") - : Environment.GetEnvironmentVariable("UNITTEST_BASEFOLDER"); + ? "duplicati_testdata" + : Environment.GetEnvironmentVariable("UNITTEST_BASEFOLDER") + ); /// /// The folder path that serves as the backup destination @@ -57,7 +60,7 @@ namespace Duplicati.UnitTest /// /// The log file for manual examination /// - protected readonly string LOGFILE = Path.Combine(BASEFOLDER, "logfile.log"); + protected readonly string LOGFILE = Path.Combine(BASEFOLDER, "logs/logfile.log"); /// /// The database is fixed so it does not mess up the system where the test is performed /// @@ -85,35 +88,57 @@ namespace Duplicati.UnitTest } [OneTimeSetUp] - public virtual void OneTimeSetUp() - { + public void BasicHelperOneTimeSetUp() + { + TestContext.Progress.WriteLine("One Time Setup {0}", TestContext.CurrentContext.Test.Name); if (DEBUG_OUTPUT) { Console.SetOut(TestContext.Progress); } systemIO.DirectoryCreate(BASEFOLDER); - this.TearDown(); - this.OneTimeTearDown(); + this.BasicHelperTearDown(); } - [OneTimeTearDown] - public virtual void OneTimeTearDown() - { - // No-op by default. + private static int count = 1; + + + private static Timer SetupTimer() { + var timer = new Timer(); + + timer.Interval = 2000; + timer.Elapsed += delegate(object obj, ElapsedEventArgs e){ + var nogc = GC.GetTotalMemory(false)/1000/1000; + var yesgc = GC.GetTotalMemory(true)/1000/1000; + + var me = Process.GetCurrentProcess(); + var process = me.WorkingSet64/1000/1000; + TestContext.Progress.WriteLine("Memory: {0}MB -> {1}MB ({2}MB)", nogc, yesgc, process); + }; + timer.AutoReset = true; + return timer; } + private Timer memoryTimer = SetupTimer(); [SetUp] - public virtual void SetUp() + public void BasicHelperSetUp() { + memoryTimer.Enabled = true; + var me = Process.GetCurrentProcess(); + TestContext.Progress.WriteLine("Setup {0} {1}MB {2}MB", TestContext.CurrentContext.Test.Name, GC.GetTotalMemory(true)/1000/1000, me.WorkingSet64/1000/1000); systemIO.DirectoryCreate(this.DATAFOLDER); systemIO.DirectoryCreate(this.TARGETFOLDER); - systemIO.DirectoryCreate(this.RESTOREFOLDER); + systemIO.DirectoryCreate(this.RESTOREFOLDER); } [TearDown] - public virtual void TearDown() + public void BasicHelperTearDown() { + memoryTimer.Enabled = false; + if( TestContext.CurrentContext.Test.MethodName != null){ + var me = Process.GetCurrentProcess(); + TestContext.Progress.WriteLine("TearDown {0} {1}MB {2}MB", TestContext.CurrentContext.Test.MethodName, GC.GetTotalMemory(true)/1000/1000, me.WorkingSet64/1000/1000); + } if (systemIO.DirectoryExists(this.DATAFOLDER)) { systemIO.DirectoryDelete(this.DATAFOLDER, true); @@ -126,10 +151,6 @@ namespace Duplicati.UnitTest { systemIO.DirectoryDelete(this.RESTOREFOLDER, true); } - if (systemIO.FileExists(this.LOGFILE)) - { - systemIO.FileDelete(this.LOGFILE); - } if (systemIO.FileExists(this.DBFILE)) { systemIO.FileDelete(this.DBFILE); @@ -138,6 +159,18 @@ namespace Duplicati.UnitTest { systemIO.FileDelete($"{this.DBFILE}-journal"); } + if (systemIO.FileExists(this.LOGFILE)) + { + var source = new System.IO.FileStream(this.LOGFILE, System.IO.FileMode.Open, System.IO.FileAccess.Read); + var dest = new System.IO.FileStream($"{this.LOGFILE}.{count++}.gz", System.IO.FileMode.Create, System.IO.FileAccess.Write); + var gzip = new System.IO.Compression.GZipStream(dest, System.IO.Compression.CompressionMode.Compress); + source.CopyTo(gzip); + gzip.Flush(); + source.Close(); + gzip.Close(); + dest.Close(); + systemIO.FileDelete(this.LOGFILE); + } } protected virtual Dictionary TestOptions diff --git a/Duplicati/UnitTest/BorderTests.cs b/Duplicati/UnitTest/BorderTests.cs index cc975dda8..21fd37c72 100644 --- a/Duplicati/UnitTest/BorderTests.cs +++ b/Duplicati/UnitTest/BorderTests.cs @@ -32,10 +32,9 @@ namespace Duplicati.UnitTest { private readonly string recreatedDatabaseFile = Path.Combine(BASEFOLDER, "recreated-database.sqlite"); - public override void TearDown() + [TearDown] + public void TearDown() { - base.TearDown(); - if (File.Exists(this.recreatedDatabaseFile)) { File.Delete(this.recreatedDatabaseFile); diff --git a/Duplicati/UnitTest/CommandLineOperationsTests.cs b/Duplicati/UnitTest/CommandLineOperationsTests.cs index 2480f6851..86ec197d4 100644 --- a/Duplicati/UnitTest/CommandLineOperationsTests.cs +++ b/Duplicati/UnitTest/CommandLineOperationsTests.cs @@ -59,10 +59,9 @@ namespace Duplicati.UnitTest } } - public override void SetUp() + [SetUp] + public void SetUp() { - base.SetUp(); - if (!systemIO.FileExists(zipAlternativeFilepath)) { var url = $"{S3_URL}{this.zipFilename}"; @@ -127,7 +126,8 @@ namespace Duplicati.UnitTest } } - public override void OneTimeTearDown() + [OneTimeTearDown] + public void OneTimeTearDown() { if (systemIO.DirectoryExists(this.SOURCEFOLDER)) { diff --git a/Duplicati/UnitTest/CompressionTests.cs b/Duplicati/UnitTest/CompressionTests.cs index cd1d20f7c..971317b15 100644 --- a/Duplicati/UnitTest/CompressionTests.cs +++ b/Duplicati/UnitTest/CompressionTests.cs @@ -28,7 +28,7 @@ using NUnit.Framework; namespace Duplicati.UnitTest { [Category("Compression")] - public class CompressionTests + public class CompressionTests : BasicSetupHelper { [TestCase("zip")] [TestCase("7z")] diff --git a/Duplicati/UnitTest/DeleteHandlerTests.cs b/Duplicati/UnitTest/DeleteHandlerTests.cs index 24282e344..a822bfbce 100644 --- a/Duplicati/UnitTest/DeleteHandlerTests.cs +++ b/Duplicati/UnitTest/DeleteHandlerTests.cs @@ -29,7 +29,7 @@ using NUnit.Framework; namespace Duplicati.UnitTest { - public class DeleteHandlerTests + public class DeleteHandlerTests : BasicSetupHelper { private class Fileset : IListResultFileset, IEquatable { diff --git a/Duplicati/UnitTest/DisruptionTests.cs b/Duplicati/UnitTest/DisruptionTests.cs index 82a55c1f8..6a44c16ce 100644 --- a/Duplicati/UnitTest/DisruptionTests.cs +++ b/Duplicati/UnitTest/DisruptionTests.cs @@ -65,9 +65,9 @@ namespace Duplicati.UnitTest return await backupTask.ConfigureAwait(false); } - public override void SetUp() + [SetUp] + public void SetUp() { - base.SetUp(); this.ModifySourceFiles(); } diff --git a/Duplicati/UnitTest/Duplicati.UnitTest.csproj b/Duplicati/UnitTest/Duplicati.UnitTest.csproj index 90dba1d95..93554334a 100644 --- a/Duplicati/UnitTest/Duplicati.UnitTest.csproj +++ b/Duplicati/UnitTest/Duplicati.UnitTest.csproj @@ -1,43 +1,71 @@  + + Exe net8.0 - Library + false + false + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Duplicati/UnitTest/GeneralBlackBoxTesting.cs b/Duplicati/UnitTest/GeneralBlackBoxTesting.cs index 36711dc7a..c87dfd331 100644 --- a/Duplicati/UnitTest/GeneralBlackBoxTesting.cs +++ b/Duplicati/UnitTest/GeneralBlackBoxTesting.cs @@ -67,9 +67,11 @@ namespace Duplicati.UnitTest } } - public override void OneTimeSetUp() + + [OneTimeSetUp] + public void OneTimeSetUp() { - base.OneTimeSetUp(); + this.OneTimeTearDown(); var url = $"https://testfiles.duplicati.com/{this.zipFilename}"; var destinationFilePath = this.zipFilepath; var webRequest = (HttpWebRequest)WebRequest.Create(url); @@ -111,7 +113,8 @@ namespace Duplicati.UnitTest System.IO.Compression.ZipFile.ExtractToDirectory(this.zipFilepath, BASEFOLDER); } - public override void OneTimeTearDown() + [OneTimeTearDown] + public void OneTimeTearDown() { if (Directory.Exists(SOURCE_FOLDERS)) { diff --git a/Duplicati/UnitTest/IOTests.cs b/Duplicati/UnitTest/IOTests.cs index 78bff17a9..529baa47e 100644 --- a/Duplicati/UnitTest/IOTests.cs +++ b/Duplicati/UnitTest/IOTests.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -30,7 +30,7 @@ using System.Collections.Generic; namespace Duplicati.UnitTest { [Category("IO")] - public class IOTests + public class IOTests : BasicSetupHelper { public static string LongPath(string pathRoot) @@ -281,7 +281,7 @@ namespace Duplicati.UnitTest }; foreach (var path in testCasesWherePathGetFullGivesSameResultsAsDotNet) { - var actual = SystemIO.IO_WIN.PathGetFullPath(path); + var actual = SystemIO.IO_WIN.PathGetFullPath(path); var expected = System.IO.Path.GetFullPath(path); Assert.AreEqual(expected, actual, $"Path: {path}"); } diff --git a/Duplicati/UnitTest/ImportExportTests.cs b/Duplicati/UnitTest/ImportExportTests.cs index 15d987acc..d976ee7c8 100644 --- a/Duplicati/UnitTest/ImportExportTests.cs +++ b/Duplicati/UnitTest/ImportExportTests.cs @@ -33,7 +33,7 @@ using Backup = Duplicati.Server.Database.Backup; namespace Duplicati.UnitTest { - public class ImportExportTests + public class ImportExportTests : BasicSetupHelper { private string serverDatafolder; diff --git a/Duplicati/UnitTest/RecoveryToolTests.cs b/Duplicati/UnitTest/RecoveryToolTests.cs index 71506763f..f654bbb77 100644 --- a/Duplicati/UnitTest/RecoveryToolTests.cs +++ b/Duplicati/UnitTest/RecoveryToolTests.cs @@ -34,14 +34,13 @@ namespace Duplicati.UnitTest private string originalCurrentDirectory; [SetUp] - public override void SetUp() + public void SetUp() { - base.SetUp(); this.originalCurrentDirectory = Directory.GetCurrentDirectory(); } [TearDown] - public override void TearDown() + public void TearDown() { // Since the RecoveryTool changes the current directory, we will reset it so that // the teardown methods do not complain about the paths being used by another process. @@ -49,8 +48,6 @@ namespace Duplicati.UnitTest { Directory.SetCurrentDirectory(this.originalCurrentDirectory); } - - base.TearDown(); } [Test] diff --git a/Duplicati/UnitTest/RepairHandlerTests.cs b/Duplicati/UnitTest/RepairHandlerTests.cs index c3a494845..34d870966 100644 --- a/Duplicati/UnitTest/RepairHandlerTests.cs +++ b/Duplicati/UnitTest/RepairHandlerTests.cs @@ -34,9 +34,9 @@ namespace Duplicati.UnitTest [TestFixture] public class RepairHandlerTests : BasicSetupHelper { - public override void SetUp() + [SetUp] + public void SetUp() { - base.SetUp(); File.WriteAllBytes(Path.Combine(this.DATAFOLDER, "file"), new byte[] {0}); } diff --git a/Duplicati/UnitTest/RestoreHandlerTests.cs b/Duplicati/UnitTest/RestoreHandlerTests.cs index ace81d48a..307b41e9a 100644 --- a/Duplicati/UnitTest/RestoreHandlerTests.cs +++ b/Duplicati/UnitTest/RestoreHandlerTests.cs @@ -21,7 +21,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Security.AccessControl; using Duplicati.Library.Common; using Duplicati.Library.Common.IO; using Duplicati.Library.Interface; @@ -85,7 +84,7 @@ namespace Duplicati.UnitTest { return; } - + /* TODO-DNC string folderPath = Path.Combine(this.DATAFOLDER, "folder"); Directory.CreateDirectory(folderPath); string filePath = Path.Combine(folderPath, "file"); @@ -138,6 +137,7 @@ namespace Duplicati.UnitTest FileSecurity restoredFileSecurity = new FileInfo(restoredFilePath).GetAccessControl(); Assert.IsTrue(restoredFileSecurity.AreAccessRulesProtected); } + */ } } } \ No newline at end of file diff --git a/Duplicati/UnitTest/ResultFormatSerializerProviderTest.cs b/Duplicati/UnitTest/ResultFormatSerializerProviderTest.cs index 274d8edc1..16143eba8 100644 --- a/Duplicati/UnitTest/ResultFormatSerializerProviderTest.cs +++ b/Duplicati/UnitTest/ResultFormatSerializerProviderTest.cs @@ -25,7 +25,7 @@ using NUnit.Framework; namespace Duplicati.UnitTest { [TestFixture] - public class ResultFormatSerializerProviderTest + public class ResultFormatSerializerProviderTest : BasicSetupHelper { [Test] [Category("Serialization")] diff --git a/Duplicati/UnitTest/UriUtilityTests.cs b/Duplicati/UnitTest/UriUtilityTests.cs index 1742eeb15..5be85eca4 100644 --- a/Duplicati/UnitTest/UriUtilityTests.cs +++ b/Duplicati/UnitTest/UriUtilityTests.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -24,7 +24,7 @@ using System.Collections.Specialized; namespace Duplicati.UnitTest { - public class UriUtilityTests + public class UriUtilityTests : BasicSetupHelper { [Test] [Category("UriUtility")] diff --git a/Duplicati/UnitTest/UtilityTests.cs b/Duplicati/UnitTest/UtilityTests.cs index 9da40798b..a40e8b1db 100644 --- a/Duplicati/UnitTest/UtilityTests.cs +++ b/Duplicati/UnitTest/UtilityTests.cs @@ -32,7 +32,7 @@ using System.Threading.Tasks; namespace Duplicati.UnitTest { - public class UtilityTests + public class UtilityTests : BasicSetupHelper { [Test] [Category("Utility")] diff --git a/Duplicati/WebserverCore/AuthMiddleware.cs b/Duplicati/WebserverCore/AuthMiddleware.cs new file mode 100644 index 000000000..9aaae10e6 --- /dev/null +++ b/Duplicati/WebserverCore/AuthMiddleware.cs @@ -0,0 +1,38 @@ + +namespace Duplicati.WebserverCore +{ + public class AuthMiddleware + { + private readonly RequestDelegate _next; + private readonly Server.WebServer.AuthenticationHandler _authHandler = new(); + + public AuthMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + var info = new Server.WebServer.RESTMethods.RequestInfo(new LegacyHttpRequestShim(context.Request), new LegacyHttpResponseShim(context.Response), new LegacyHttpSessionShim()); + + if (!_authHandler.Process(info.Request, info.Response, info.Session)) + { + // Call the next delegate/middleware in the pipeline. + await _next(context); + } + else + { + info.Response.Send(); + } + } + } + + public static class AuthMiddlewareExtensions + { + public static IApplicationBuilder UseAuthMiddleware( + this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/Duplicati/WebserverCore/Duplicati.WebserverCore.csproj b/Duplicati/WebserverCore/Duplicati.WebserverCore.csproj new file mode 100644 index 000000000..e1ad23e45 --- /dev/null +++ b/Duplicati/WebserverCore/Duplicati.WebserverCore.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + Library + enable + enable + true + + + + + + + + + ..\..\thirdparty\HttpServer\HttpServer.dll + + + diff --git a/Duplicati/WebserverCore/DuplicatiWebserver.cs b/Duplicati/WebserverCore/DuplicatiWebserver.cs new file mode 100644 index 000000000..ca227d420 --- /dev/null +++ b/Duplicati/WebserverCore/DuplicatiWebserver.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.Extensions.FileProviders; + +namespace Duplicati.WebserverCore +{ + public class DuplicatiWebserver + { + public Action Foo() + { + var builder = WebApplication.CreateBuilder(); + builder.Host.UseRESTHandlers(); + builder.Services.AddControllers() + // This app gets launched by a different assembly, so we need to tell it to look in this one + .AddApplicationPart(this.GetType().Assembly); + builder.Services.AddHostedService(); + var app = builder.Build(); + app.UseAuthMiddleware(); + + + string webroot = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + string install_webroot = System.IO.Path.Combine(Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "webroot"); + webroot = System.IO.Path.Combine(webroot, "webroot"); + var webroot_fileprovider = new PhysicalFileProvider(webroot); + + // index.html + DefaultFilesOptions defaultFilesOptions = new DefaultFilesOptions(); + defaultFilesOptions.DefaultFileNames.Clear(); + defaultFilesOptions.DefaultFileNames.Add("index.html"); + defaultFilesOptions.FileProvider = webroot_fileprovider; + app.UseDefaultFiles(defaultFilesOptions); + + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = webroot_fileprovider, + RequestPath = "" + }); + + app.MapControllers(); + + // Test http://127.0.0.1:3001/api/v1/Licenses + app.RunAsync("http://localhost:3001"); + return () => { app.StopAsync(); }; + } + + public Action Bar() + { + var builder = Host.CreateDefaultBuilder(); + builder.UseRESTHandlers(); + var app = builder.Build(); + + app.RunAsync(); + return () => { app.StopAsync(); }; + } + } + + //Useful for debugging ASP.net magically loading controllers + public class ApplicationPartsLogger : IHostedService + { + private readonly ILogger _logger; + private readonly ApplicationPartManager _partManager; + + public ApplicationPartsLogger(ILogger logger, ApplicationPartManager partManager) + { + _logger = logger; + _partManager = partManager; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + // Get the names of all the application parts. This is the short assembly name for AssemblyParts + var applicationParts = _partManager.ApplicationParts.Select(x => x.Name); + + // Create a controller feature, and populate it from the application parts + var controllerFeature = new ControllerFeature(); + _partManager.PopulateFeature(controllerFeature); + + // Get the names of all of the controllers + var controllers = controllerFeature.Controllers.Select(x => x.Name); + + // Log the application parts and controllers + _logger.LogInformation("Found the following application parts: '{ApplicationParts}' with the following controllers: '{Controllers}'", + string.Join(", ", applicationParts), string.Join(", ", controllers)); + + return Task.CompletedTask; + } + + // Required by the interface + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Duplicati/WebserverCore/LegacyHttpRequestShim.cs b/Duplicati/WebserverCore/LegacyHttpRequestShim.cs new file mode 100644 index 000000000..381e1be3f --- /dev/null +++ b/Duplicati/WebserverCore/LegacyHttpRequestShim.cs @@ -0,0 +1,152 @@ + +using Duplicati.Library.Utility; +using HttpServer; +using HttpServer.FormDecoders; +using Microsoft.AspNetCore.Http.Extensions; +using System; +using System.Collections.Specialized; +using System.Globalization; +using System.Net; +using System.Xml.Linq; +using HttpRequest = Microsoft.AspNetCore.Http.HttpRequest; + +class LegacyHttpRequestShim : HttpServer.IHttpRequest +{ + HttpRequest request; + public LegacyHttpRequestShim(HttpRequest request) { this.request = request; } + + public string[] AcceptTypes => throw new NotImplementedException(); + + public Stream Body { get => request.Body; set => throw new NotImplementedException(); } + + public bool BodyIsComplete => throw new NotImplementedException(); + + public ConnectionType Connection { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public int ContentLength { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public RequestCookies Cookies => new RequestCookies(request.Headers.Cookie); + + public HttpForm Form + { + get + { + var form = new HttpForm(); + + if (request.ContentType != null && (request.ContentType.StartsWith("multipart/form-data", true, CultureInfo.InvariantCulture) || request.ContentType.StartsWith("application/x-www-form-urlencoded", true, CultureInfo.InvariantCulture) )) + { + + foreach (var kvp in request.Form) + { + form.Add(kvp.Key, kvp.Value); + } + + //Files + foreach (var file in request.Form.Files) + { + // Generate a temp file *CRY* I can't believe duplicati did this + string path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "HttpServer"); + string tempFile = Path.Combine(path, Math.Abs(file.FileName.GetHashCode()) + ".tmp"); + + // If the file exists generate a new filename + while (File.Exists(tempFile)) + tempFile = Path.Combine(path, Math.Abs(file.FileName.GetHashCode() + 1) + ".tmp"); + + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + using (var memoryStream = new MemoryStream()) + { + file.CopyTo(memoryStream); + File.WriteAllBytes(tempFile, memoryStream.ToArray()); + } + + form.AddFile(new HttpFile(file.Name, tempFile, file.ContentType, file.FileName)); + } + } + return form; + } + } + + public NameValueCollection Headers + { + get + { + var headers = new NameValueCollection(); + foreach (var pair in request.Headers) + { + foreach (var value in pair.Value) + { + headers.Add(pair.Key, value); + } + } + return headers; + } + } + + public string HttpVersion { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public bool IsAjax => throw new NotImplementedException(); + + public string Method { get => request.Method; set => throw new NotImplementedException(); } + + public HttpParam Param => throw new NotImplementedException(); + + public HttpInput QueryString + { + get + { + var input = new HttpInput("QueryString"); + foreach (var kvp in request.Query) { input.Add(kvp.Key, kvp.Value); } + return input; + } + } + + public bool Secure => throw new NotImplementedException(); + + public IPEndPoint RemoteEndPoint => throw new NotImplementedException(); + + public System.Uri Uri { get => new System.Uri(request.GetEncodedUrl()); set => throw new NotImplementedException(); } + + public string[] UriParts => throw new NotImplementedException(); + + public string UriPath { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public void AddHeader(string name, string value) + { + throw new NotImplementedException(); + } + + public int AddToBody(byte[] bytes, int offset, int length) + { + throw new NotImplementedException(); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public object Clone() + { + throw new NotImplementedException(); + } + + public IHttpResponse CreateResponse(IHttpClientContext context) + { + throw new NotImplementedException(); + } + + public void DecodeBody(FormDecoderProvider providers) + { + throw new NotImplementedException(); + } + + public byte[] GetBody() + { + throw new NotImplementedException(); + } + + public void SetCookies(RequestCookies cookies) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Duplicati/WebserverCore/LegacyHttpResponseShim.cs b/Duplicati/WebserverCore/LegacyHttpResponseShim.cs new file mode 100644 index 000000000..873c7ccb9 --- /dev/null +++ b/Duplicati/WebserverCore/LegacyHttpResponseShim.cs @@ -0,0 +1,73 @@ + +using HttpServer; +using HttpServer.FormDecoders; +using System.Collections.Specialized; +using System.Net; +using System.Text; +using HttpResponse = Microsoft.AspNetCore.Http.HttpResponse; + +class LegacyHttpResponseShim : HttpServer.IHttpResponse +{ + HttpResponse response; + ResponseCookies cookies = new(); + public LegacyHttpResponseShim(HttpResponse response) { this.response = response; } + public Stream Body { get => response.Body; set => throw new NotImplementedException(); } + public string ProtocolVersion { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public bool Chunked { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public ConnectionType Connection { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public Encoding Encoding { get => Encoding.UTF8; set => throw new NotImplementedException(); } + public int KeepAlive { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public HttpStatusCode Status { get => throw new NotImplementedException(); set => response.StatusCode = (int)value; } + public string Reason { get => throw new NotImplementedException(); set { /*NOP*/ } } + public long ContentLength { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public string ContentType { get => throw new NotImplementedException(); set => response.ContentType = value; } + + public bool HeadersSent => response.HasStarted; + + public bool Sent => throw new NotImplementedException(); + + public ResponseCookies Cookies => cookies; + + public void AddHeader(string name, string value) + { + if (response.Headers.ContainsKey(name)) + { + response.Headers.Remove(name); + } + response.Headers.Add(name, value); + } + + public void Redirect(Uri uri) + { + throw new NotImplementedException(); + } + + public void Redirect(string url) + { + throw new NotImplementedException(); + } + + public void Send() + { + foreach(ResponseCookie cookie in cookies) + { + response.Cookies.Append(cookie.Name, cookie.Value, new CookieOptions() { Expires = cookie.Expires, Path = cookie.Path, IsEssential=true }) ; + } + response.StartAsync(); + } + + public void SendBody(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public void SendBody(byte[] buffer) + { + throw new NotImplementedException(); + } + + public void SendHeaders() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Duplicati/WebserverCore/LegacyHttpSessionShim.cs b/Duplicati/WebserverCore/LegacyHttpSessionShim.cs new file mode 100644 index 000000000..92b119d9f --- /dev/null +++ b/Duplicati/WebserverCore/LegacyHttpSessionShim.cs @@ -0,0 +1,37 @@ + +using HttpServer; +using HttpServer.FormDecoders; +using HttpServer.Sessions; +using System.Collections.Specialized; +using System.Net; +using System.Text; + +class LegacyHttpSessionShim : HttpServer.Sessions.IHttpSession +{ + public object this[string name] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public string Id => throw new NotImplementedException(); + + public DateTime Accessed { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public int Count => throw new NotImplementedException(); + + public IEnumerable Keys => throw new NotImplementedException(); + + public event HttpSessionClearedHandler BeforeClear; + + public void Clear() + { + throw new NotImplementedException(); + } + + public void Clear(bool expires) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Duplicati/WebserverCore/RestHandlerCore.cs b/Duplicati/WebserverCore/RestHandlerCore.cs new file mode 100644 index 000000000..b6f57ec1e --- /dev/null +++ b/Duplicati/WebserverCore/RestHandlerCore.cs @@ -0,0 +1,240 @@ +// Copyright (C) 2023, 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 Amazon.S3; +using Duplicati.Library.RestAPI; +using Duplicati.Server.WebServer.RESTMethods; +using Duplicati.WebserverCore; +using Microsoft.AspNetCore.Mvc; +using Org.BouncyCastle.Bcpg.Sig; +using System.Collections.Concurrent; +using System.Net; +using System.Reflection; +using System.Text; +using System.Xml.Linq; + +namespace Duplicati.WebserverCore +{ + public class RESTHandlerCoreController : Controller + { + public const string API_URI_PATH = "/api/v1"; + public static readonly int API_URI_SEGMENTS = API_URI_PATH.Split(new char[] { '/' }).Length; + + private readonly Dictionary _modules = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public RESTHandlerCoreController(IEnumerable restMethods) + { + Console.WriteLine("Loaded controller!"); + //FIXME: Why no methods? + foreach (var method in restMethods) + { + var t = method.GetType(); + _modules.Add(t.Name.ToLowerInvariant(), method); + } + } + + [Route(API_URI_PATH + "/{module?}/{**key}")] + [AcceptVerbs("GET", "POST", "PUT", "DELETE", "PATCH")] + public async System.Threading.Tasks.Task Index(string? module, string? key) + { + + module = (module != null && module != "") ? module : "help"; + + var method = Request.Method; + + var ci = ParseRequestCulture(Request.Headers.Accept); + + var info = new RequestInfo(new LegacyHttpRequestShim(Request), new LegacyHttpResponseShim(Response), new LegacyHttpSessionShim()); + + using (Library.Localization.LocalizationService.TemporaryContext(ci)) + { + try + { + if (ci != null) + Response.Headers.Add("Content-Language", ci.Name); + + IRESTMethod mod; + _modules.TryGetValue(module, out mod); + + if (mod == null) + { + Response.StatusCode = (int)HttpStatusCode.NotFound; + await Response.Body.WriteAsync(Encoding.UTF8.GetBytes("No such module")); + return; + } + else if (method == "GET" && mod is IRESTMethodGET get) + { + //if (info.Request.Form != HttpServer.HttpForm.EmptyForm) + //{ + // if (info.Request.QueryString == HttpServer.HttpInput.Empty) + // { + // var r = info.Request.GetType().GetField("_queryString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + // r.SetValue(info.Request, new HttpServer.HttpInput("formdata")); + // } + + // foreach (HttpServer.HttpInputItem v in info.Request.Form) + // if (!info.Request.QueryString.Contains(v.Name)) + // info.Request.QueryString.Add(v.Name, v.Value); + //} + + get.GET(key, info); + } + else if (method =="PUT" && mod is IRESTMethodPUT put) + put.PUT(key, info); + else if (method == "POST" && mod is IRESTMethodPOST post) + { + //if (info.Request.Form == HttpServer.HttpForm.EmptyForm || info.Request.Form == HttpServer.HttpInput.Empty) + //{ + // var r = info.Request.GetType().GetMethod("AssignForm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new Type[] { typeof(HttpServer.HttpForm) }, null); + // r.Invoke(info.Request, new object[] { new HttpServer.HttpForm(info.Request.QueryString) }); + //} + //else + //{ + // foreach (HttpServer.HttpInputItem v in info.Request.QueryString) + // if (!info.Request.Form.Contains(v.Name)) + // info.Request.Form.Add(v.Name, v.Value); + //} + + post.POST(key, info); + } + else if (method == "DELETE" && mod is IRESTMethodDELETE delete) + delete.DELETE(key, info); + else if (method == "PATCH" && mod is IRESTMethodPATCH patch) + patch.PATCH(key, info); + else + { + Response.StatusCode = (int)HttpStatusCode.MethodNotAllowed; + await Response.Body.WriteAsync(Encoding.UTF8.GetBytes("Method is not allowed")); + return; + } + } + catch (Exception ex) + { + FIXMEGlobal.DataConnection.LogError("", string.Format("Request for {0} gave error",Request.Path), ex); + Console.WriteLine(ex); + + try + { + var wex = ex; + while (wex is System.Reflection.TargetInvocationException && wex.InnerException != wex && wex.InnerException != null) + wex = wex.InnerException; + + Response.StatusCode = (int)HttpStatusCode.InternalServerError; + await Response.Body.WriteAsync(Encoding.UTF8.GetBytes( + wex.Message +#if DEBUG + + "\n" + wex.StackTrace +#endif + )); + return; + } + catch (Exception flex) + { + FIXMEGlobal.DataConnection.LogError("", "Reporting error gave error", flex); + } + } + } + } + + + private static readonly ConcurrentDictionary _cultureCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private static System.Globalization.CultureInfo ParseRequestCulture(string acceptheader) + { + acceptheader = acceptheader ?? string.Empty; + + // Lock-free read + System.Globalization.CultureInfo ci; + if (_cultureCache.TryGetValue(acceptheader, out ci)) + return ci; + + // Lock-free assignment, we might compute the value twice + return _cultureCache[acceptheader] = + // Parse headers like "Accept-Language: da, en-gb;q=0.8, en;q=0.7" + acceptheader + .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + { + var opts = x.Split(new[] { ";" }, StringSplitOptions.RemoveEmptyEntries); + var lang = opts.FirstOrDefault(); + var weight = + opts.Where(y => y.StartsWith("q=", StringComparison.OrdinalIgnoreCase)) + .Select(y => + { + float f; + float.TryParse(y.Substring(2), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out f); + return f; + }).FirstOrDefault(); + + // Set the default weight=1 + if (weight <= 0.001 && weight >= 0) + weight = 1; + + return new KeyValuePair(lang, weight); + }) + // Handle priority + .OrderByDescending(x => x.Value) + .Select(x => x.Key) + .Distinct() + // Filter invalid/unsupported items + .Where(x => !string.IsNullOrWhiteSpace(x) && Library.Localization.LocalizationService.ParseCulture(x) != null) + .Select(x => Library.Localization.LocalizationService.ParseCulture(x)) + // And get the first that works + .FirstOrDefault(System.Globalization.CultureInfo.InvariantCulture); + + } + } + public static class RESTHandlerCoreExtensions + { + public static IHostBuilder UseRESTHandlers( + this IHostBuilder builder) + { + var lst = + from n in typeof(IRESTMethod).Assembly.GetTypes() + where + n.Namespace == typeof(IRESTMethod).Namespace + && + typeof(IRESTMethod).IsAssignableFrom(n) + && + !n.IsAbstract + && + !n.IsInterface + select n; + + builder.ConfigureServices(services => + { + foreach (var t in lst) + { + services.AddSingleton((IRESTMethod?)Activator.CreateInstance(t) ?? throw new NullReferenceException("RestMethod " + nameof(t) + " is null")); + } + }); + + return builder; + } + + //public static IEndpointRouteBuilder UseRESTHandlerEndpoints(this IEndpointRouteBuilder builder) + //{ + // builder.MapFallback(async (HttpContext context) => { + // await System.Threading.Tasks.Task.Delay(1); + // // context. + // //TODO: Register controllers without this middleware ? + // }); + // return builder; + //} + } + +} + diff --git a/Duplicati/WindowsService/App.config b/Duplicati/WindowsService/App.config deleted file mode 100644 index 79ce3c6f4..000000000 --- a/Duplicati/WindowsService/App.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/Duplicati/WindowsService/Program.cs b/Duplicati/WindowsService/Program.cs index e45a26995..d0d425c1a 100644 --- a/Duplicati/WindowsService/Program.cs +++ b/Duplicati/WindowsService/Program.cs @@ -20,9 +20,10 @@ // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; -using System.Configuration.Install; +using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Security.Principal; using System.ServiceProcess; using System.Text; using System.Threading.Tasks; @@ -48,13 +49,10 @@ namespace Duplicati.WindowsService Console.WriteLine("This is a Windows Service wrapper tool that hosts the Duplicati.Server.exe process, letting it run as a windows service."); Console.WriteLine("|To run from a console, run the Duplicati.Server.exe instead of Duplicati.WindowsService.exe"); Console.WriteLine(); - Console.WriteLine("Supported commands:"); + Console.WriteLine("Supported commands (Must be run as Administrator):"); Console.WriteLine(" install:\r\n Installs the service"); Console.WriteLine(" uninstall:\r\n Uninstalls the service"); Console.WriteLine(); - Console.WriteLine("Supported options for the install command:"); - Console.WriteLine(" /localuser:\r\n Installs the service as a local user"); - Console.WriteLine(); Console.WriteLine("It is possible to pass arguments to Duplicati.Server.exe, simply add them to the commandline:"); Console.WriteLine(" Duplicati.WindowsService.exe install --webservice-interface=loopback --log-retention=3M"); Console.WriteLine(); @@ -69,13 +67,34 @@ namespace Duplicati.WindowsService // Remove the install and uninstall flags if they are present var commandline = Library.Utility.Utility.WrapAsCommandLine(args.Where(x => !(string.Equals("install", x, StringComparison.OrdinalIgnoreCase) || string.Equals("uninstall", x, StringComparison.OrdinalIgnoreCase)))); var selfexec = Assembly.GetExecutingAssembly().Location; - - + // --uninstall + --install = reinstall if (uninstall) - ManagedInstallerClass.InstallHelper(new string[] { "/u", selfexec }); + { + try + { + ServiceInstaller.DeleteService(ServiceControl.SERVICE_NAME); + Console.WriteLine("Duplicati service delete succeeded."); + } + catch (Exception ex) + { + Console.WriteLine("Duplicati service delete failed. Exception: {0}", ex.Message); + } + } if (install) - ManagedInstallerClass.InstallHelper(new string[] { "/commandline=" + commandline, selfexec }); + { + //The fully qualified path to the service binary file. If the path contains a space, it must be quoted so that it is correctly interpreted. For example, "d:\my share\myservice.exe" should be specified as ""d:\my share\myservice.exe"". + //The path can also include arguments for an auto-start service. For example, "d:\myshare\myservice.exe arg1 arg2". These arguments are passed to the service entry point (typically the main function). + try + { + ServiceInstaller.InstallService(ServiceControl.SERVICE_NAME, ServiceControl.DISPLAY_NAME, "\"" + selfexec + "\"" + " " + commandline); + Console.WriteLine("Duplicati service installation succeeded."); + } + catch (Exception ex) + { + Console.WriteLine("Duplicati service installation failed. Exception: {0}", ex.Message); + } + } } else { diff --git a/Duplicati/WindowsService/ProjectInstaller.cs b/Duplicati/WindowsService/ProjectInstaller.cs deleted file mode 100644 index 081010478..000000000 --- a/Duplicati/WindowsService/ProjectInstaller.cs +++ /dev/null @@ -1,66 +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; -using System.Collections.Generic; -using System.ComponentModel; -using System.Configuration.Install; -using System.Text; -using System.Linq; -using System.Threading.Tasks; - -namespace Duplicati.WindowsService -{ - [RunInstaller(true)] - public class ProjectInstaller : Installer - { - public ProjectInstaller() - { - this.Installers.AddRange(new Installer[] { - new ServiceProcessInstaller(), - new ServiceInstaller() - }); - } - - public override void Install(IDictionary stateSaver) - { - var commandline = Context.Parameters["commandline"]; - if (!string.IsNullOrWhiteSpace(commandline)) - { - var rawpath = Context.Parameters["assemblypath"]; - var path = new StringBuilder(rawpath); - if (!rawpath.StartsWith("\"", StringComparison.Ordinal) || !rawpath.EndsWith("\"", StringComparison.Ordinal)) - { - path.Insert(0, '"'); - path.Append('"'); - } - - path.Append(" "); - path.Append(commandline); - - Context.Parameters["assemblypath"] = path.ToString(); - } - - base.Install(stateSaver); - } - - } -} diff --git a/Duplicati/WindowsService/ServiceControl.cs b/Duplicati/WindowsService/ServiceControl.cs index 863103029..51a99273b 100644 --- a/Duplicati/WindowsService/ServiceControl.cs +++ b/Duplicati/WindowsService/ServiceControl.cs @@ -34,7 +34,6 @@ namespace Duplicati.WindowsService private const string LOG_NAME = "Application"; public const string SERVICE_NAME = "Duplicati"; public const string DISPLAY_NAME = "Duplicati service"; - public const string DESCRIPTION = "Runs Duplicati as a service"; private readonly System.Diagnostics.EventLog m_eventLog; diff --git a/Duplicati/WindowsService/ServiceInstaller.cs b/Duplicati/WindowsService/ServiceInstaller.cs index 339acbd5b..eda1b24eb 100644 --- a/Duplicati/WindowsService/ServiceInstaller.cs +++ b/Duplicati/WindowsService/ServiceInstaller.cs @@ -20,21 +20,426 @@ // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; -using System.Linq; +using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; namespace Duplicati.WindowsService { - //[System.ComponentModel.RunInstaller(true)] - public class ServiceInstaller : System.ServiceProcess.ServiceInstaller + public class ServiceInstaller { - public ServiceInstaller() + [DllImport("advapi32.dll", EntryPoint = "OpenSCManagerW", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr OpenSCManager(string machineName, string databaseName, uint dwAccess); + + [DllImport("advapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseServiceHandle(IntPtr hSCObject); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern IntPtr OpenService(IntPtr hSCManager, string lpServiceName, uint dwDesiredAccess); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern IntPtr CreateService(IntPtr hSCManager, string lpServiceName, string lpDisplayName, uint dwDesiredAccess, uint dwServiceType, + uint dwStartType, uint dwErrorControl, string lpBinaryPathName, string lpLoadOrderGroup, string lpdwTagId, string lpDependencies, string lpServiceStartName, + string lpPassword); + + [DllImport("advapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool DeleteService(IntPtr hService); + + /// + /// Access to the service. Before granting the requested access, the + /// system checks the access token of the calling process. + /// + [Flags] + private enum SERVICE_ACCESS : uint { - this.StartType = System.ServiceProcess.ServiceStartMode.Automatic; - this.ServiceName = ServiceControl.SERVICE_NAME; - this.DisplayName = ServiceControl.DISPLAY_NAME; - this.Description = ServiceControl.DESCRIPTION; + /// + /// Required to call the QueryServiceConfig and + /// QueryServiceConfig2 functions to query the service configuration. + /// + SERVICE_QUERY_CONFIG = 0x00001, + + /// + /// Required to call the ChangeServiceConfig or ChangeServiceConfig2 function + /// to change the service configuration. Because this grants the caller + /// the right to change the executable file that the system runs, + /// it should be granted only to administrators. + /// + SERVICE_CHANGE_CONFIG = 0x00002, + + /// + /// Required to call the QueryServiceStatusEx function to ask the service + /// control manager about the status of the service. + /// + SERVICE_QUERY_STATUS = 0x00004, + + /// + /// Required to call the EnumDependentServices function to enumerate all + /// the services dependent on the service. + /// + SERVICE_ENUMERATE_DEPENDENTS = 0x00008, + + /// + /// Required to call the StartService function to start the service. + /// + SERVICE_START = 0x00010, + + /// + /// Required to call the ControlService function to stop the service. + /// + SERVICE_STOP = 0x00020, + + /// + /// Required to call the ControlService function to pause or continue + /// the service. + /// + SERVICE_PAUSE_CONTINUE = 0x00040, + + /// + /// Required to call the EnumDependentServices function to enumerate all + /// the services dependent on the service. + /// + SERVICE_INTERROGATE = 0x00080, + + /// + /// Required to call the ControlService function to specify a user-defined + /// control code. + /// + SERVICE_USER_DEFINED_CONTROL = 0x00100, + + /// + /// Includes STANDARD_RIGHTS_REQUIRED in addition to all access rights in this table. + /// + SERVICE_ALL_ACCESS = (ACCESS_MASK.STANDARD_RIGHTS_REQUIRED | + SERVICE_QUERY_CONFIG | + SERVICE_CHANGE_CONFIG | + SERVICE_QUERY_STATUS | + SERVICE_ENUMERATE_DEPENDENTS | + SERVICE_START | + SERVICE_STOP | + SERVICE_PAUSE_CONTINUE | + SERVICE_INTERROGATE | + SERVICE_USER_DEFINED_CONTROL), + + GENERIC_READ = ACCESS_MASK.STANDARD_RIGHTS_READ | + SERVICE_QUERY_CONFIG | + SERVICE_QUERY_STATUS | + SERVICE_INTERROGATE | + SERVICE_ENUMERATE_DEPENDENTS, + + GENERIC_WRITE = ACCESS_MASK.STANDARD_RIGHTS_WRITE | + SERVICE_CHANGE_CONFIG, + + GENERIC_EXECUTE = ACCESS_MASK.STANDARD_RIGHTS_EXECUTE | + SERVICE_START | + SERVICE_STOP | + SERVICE_PAUSE_CONTINUE | + SERVICE_USER_DEFINED_CONTROL, + + /// + /// Required to call the QueryServiceObjectSecurity or + /// SetServiceObjectSecurity function to access the SACL. The proper + /// way to obtain this access is to enable the SE_SECURITY_NAME + /// privilege in the caller's current access token, open the handle + /// for ACCESS_SYSTEM_SECURITY access, and then disable the privilege. + /// + ACCESS_SYSTEM_SECURITY = ACCESS_MASK.ACCESS_SYSTEM_SECURITY, + + /// + /// Required to call the DeleteService function to delete the service. + /// + DELETE = ACCESS_MASK.DELETE, + + /// + /// Required to call the QueryServiceObjectSecurity function to query + /// the security descriptor of the service object. + /// + READ_CONTROL = ACCESS_MASK.READ_CONTROL, + + /// + /// Required to call the SetServiceObjectSecurity function to modify + /// the Dacl member of the service object's security descriptor. + /// + WRITE_DAC = ACCESS_MASK.WRITE_DAC, + + /// + /// Required to call the SetServiceObjectSecurity function to modify + /// the Owner and Group members of the service object's security + /// descriptor. + /// + WRITE_OWNER = ACCESS_MASK.WRITE_OWNER, + } + + [Flags] + private enum ACCESS_MASK : uint + { + DELETE = 0x00010000, + READ_CONTROL = 0x00020000, + WRITE_DAC = 0x00040000, + WRITE_OWNER = 0x00080000, + SYNCHRONIZE = 0x00100000, + + STANDARD_RIGHTS_REQUIRED = 0x000F0000, + + STANDARD_RIGHTS_READ = 0x00020000, + STANDARD_RIGHTS_WRITE = 0x00020000, + STANDARD_RIGHTS_EXECUTE = 0x00020000, + + STANDARD_RIGHTS_ALL = 0x001F0000, + + SPECIFIC_RIGHTS_ALL = 0x0000FFFF, + + ACCESS_SYSTEM_SECURITY = 0x01000000, + + MAXIMUM_ALLOWED = 0x02000000, + + GENERIC_READ = 0x80000000, + GENERIC_WRITE = 0x40000000, + GENERIC_EXECUTE = 0x20000000, + GENERIC_ALL = 0x10000000, + + DESKTOP_READOBJECTS = 0x00000001, + DESKTOP_CREATEWINDOW = 0x00000002, + DESKTOP_CREATEMENU = 0x00000004, + DESKTOP_HOOKCONTROL = 0x00000008, + DESKTOP_JOURNALRECORD = 0x00000010, + DESKTOP_JOURNALPLAYBACK = 0x00000020, + DESKTOP_ENUMERATE = 0x00000040, + DESKTOP_WRITEOBJECTS = 0x00000080, + DESKTOP_SWITCHDESKTOP = 0x00000100, + + WINSTA_ENUMDESKTOPS = 0x00000001, + WINSTA_READATTRIBUTES = 0x00000002, + WINSTA_ACCESSCLIPBOARD = 0x00000004, + WINSTA_CREATEDESKTOP = 0x00000008, + WINSTA_WRITEATTRIBUTES = 0x00000010, + WINSTA_ACCESSGLOBALATOMS = 0x00000020, + WINSTA_EXITWINDOWS = 0x00000040, + WINSTA_ENUMERATE = 0x00000100, + WINSTA_READSCREEN = 0x00000200, + + WINSTA_ALL_ACCESS = 0x0000037F + } + + /// + /// Service types. + /// + [Flags] + private enum SERVICE_TYPE : uint + { + /// + /// Driver service. + /// + SERVICE_KERNEL_DRIVER = 0x00000001, + + /// + /// File system driver service. + /// + SERVICE_FILE_SYSTEM_DRIVER = 0x00000002, + + /// + /// Service that runs in its own process. + /// + SERVICE_WIN32_OWN_PROCESS = 0x00000010, + + /// + /// Service that shares a process with one or more other services. + /// + SERVICE_WIN32_SHARE_PROCESS = 0x00000020, + + /// + /// The service can interact with the desktop. + /// + SERVICE_INTERACTIVE_PROCESS = 0x00000100, + } + + /// + /// Service start options + /// + private enum SERVICE_START : uint + { + /// + /// A device driver started by the system loader. This value is valid + /// only for driver services. + /// + SERVICE_BOOT_START = 0x00000000, + + /// + /// A device driver started by the IoInitSystem function. This value + /// is valid only for driver services. + /// + SERVICE_SYSTEM_START = 0x00000001, + + /// + /// A service started automatically by the service control manager + /// during system startup. For more information, see Automatically + /// Starting Services. + /// + SERVICE_AUTO_START = 0x00000002, + + /// + /// A service started by the service control manager when a process + /// calls the StartService function. For more information, see + /// Starting Services on Demand. + /// + SERVICE_DEMAND_START = 0x00000003, + + /// + /// A service that cannot be started. Attempts to start the service + /// result in the error code ERROR_SERVICE_DISABLED. + /// + SERVICE_DISABLED = 0x00000004, + } + + /// + /// Severity of the error, and action taken, if this service fails + /// to start. + /// + private enum SERVICE_ERROR + { + /// + /// The startup program ignores the error and continues the startup + /// operation. + /// + SERVICE_ERROR_IGNORE = 0x00000000, + + /// + /// The startup program logs the error in the event log but continues + /// the startup operation. + /// + SERVICE_ERROR_NORMAL = 0x00000001, + + /// + /// The startup program logs the error in the event log. If the + /// last-known-good configuration is being started, the startup + /// operation continues. Otherwise, the system is restarted with + /// the last-known-good configuration. + /// + SERVICE_ERROR_SEVERE = 0x00000002, + + /// + /// The startup program logs the error in the event log, if possible. + /// If the last-known-good configuration is being started, the startup + /// operation fails. Otherwise, the system is restarted with the + /// last-known good configuration. + /// + SERVICE_ERROR_CRITICAL = 0x00000003, + } + + [Flags] + private enum SCM_ACCESS : uint + { + /// + /// Required to connect to the service control manager. + /// + SC_MANAGER_CONNECT = 0x00001, + + /// + /// Required to call the CreateService function to create a service + /// object and add it to the database. + /// + SC_MANAGER_CREATE_SERVICE = 0x00002, + + /// + /// Required to call the EnumServicesStatusEx function to list the + /// services that are in the database. + /// + SC_MANAGER_ENUMERATE_SERVICE = 0x00004, + + /// + /// Required to call the LockServiceDatabase function to acquire a + /// lock on the database. + /// + SC_MANAGER_LOCK = 0x00008, + + /// + /// Required to call the QueryServiceLockStatus function to retrieve + /// the lock status information for the database. + /// + SC_MANAGER_QUERY_LOCK_STATUS = 0x00010, + + /// + /// Required to call the NotifyBootConfigStatus function. + /// + SC_MANAGER_MODIFY_BOOT_CONFIG = 0x00020, + + /// + /// Includes STANDARD_RIGHTS_REQUIRED, in addition to all access + /// rights in this table. + /// + SC_MANAGER_ALL_ACCESS = ACCESS_MASK.STANDARD_RIGHTS_REQUIRED | + SC_MANAGER_CONNECT | + SC_MANAGER_CREATE_SERVICE | + SC_MANAGER_ENUMERATE_SERVICE | + SC_MANAGER_LOCK | + SC_MANAGER_QUERY_LOCK_STATUS | + SC_MANAGER_MODIFY_BOOT_CONFIG, + + GENERIC_READ = ACCESS_MASK.STANDARD_RIGHTS_READ | + SC_MANAGER_ENUMERATE_SERVICE | + SC_MANAGER_QUERY_LOCK_STATUS, + + GENERIC_WRITE = ACCESS_MASK.STANDARD_RIGHTS_WRITE | + SC_MANAGER_CREATE_SERVICE | + SC_MANAGER_MODIFY_BOOT_CONFIG, + + GENERIC_EXECUTE = ACCESS_MASK.STANDARD_RIGHTS_EXECUTE | + SC_MANAGER_CONNECT | SC_MANAGER_LOCK, + + GENERIC_ALL = SC_MANAGER_ALL_ACCESS, + } + + public static void InstallService(string ServiceName, string DisplayName, string Path) + { + var scMgrHandle = OpenSCManager(null, null, (uint)SCM_ACCESS.SC_MANAGER_ALL_ACCESS); + + try + { + if (scMgrHandle == IntPtr.Zero) + throw new Exception($"Win32 error { Marshal.GetLastWin32Error().ToString() } during install service (OpenSCManager)"); + + var serviceHandle = CreateService(scMgrHandle, ServiceName, DisplayName, (uint)SERVICE_ACCESS.SERVICE_ALL_ACCESS, (uint)SERVICE_TYPE.SERVICE_WIN32_OWN_PROCESS, + (uint)SERVICE_START.SERVICE_AUTO_START, (uint)SERVICE_ERROR.SERVICE_ERROR_NORMAL, Path, null, null, null, null, null); + + if (serviceHandle == IntPtr.Zero) + throw new Exception($"Win32 error { Marshal.GetLastWin32Error().ToString() } during install service (CreateService)"); + + CloseServiceHandle(serviceHandle); + } + finally + { + CloseServiceHandle(scMgrHandle); + } + } + + public static void DeleteService(string ServiceName) + { + var scMgrHandle = OpenSCManager(null, null, (uint)SCM_ACCESS.SC_MANAGER_ALL_ACCESS); + + try + { + if (scMgrHandle == IntPtr.Zero) + throw new Exception($"Win32 error { Marshal.GetLastWin32Error().ToString() } during delete service (OpenSCManager)"); + + var serviceHandle = OpenService(scMgrHandle, ServiceName, (uint)SERVICE_ACCESS.SERVICE_ALL_ACCESS); + + if (serviceHandle == IntPtr.Zero) + throw new Exception($"Win32 error { Marshal.GetLastWin32Error().ToString() } during delete service (OpenService)"); + + try + { + if (DeleteService(serviceHandle) == false) + throw new Exception($"Win32 error { Marshal.GetLastWin32Error().ToString() } during delete service (DeleteService)"); + } + finally + { + CloseServiceHandle(serviceHandle); + } + } + finally + { + CloseServiceHandle(scMgrHandle); + } } } } diff --git a/Duplicati/WindowsService/ServiceProcessInstaller.cs b/Duplicati/WindowsService/ServiceProcessInstaller.cs deleted file mode 100644 index 7c1038650..000000000 --- a/Duplicati/WindowsService/ServiceProcessInstaller.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (C) 2024, The Duplicati Team -// https://duplicati.com, hello@duplicati.com -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Duplicati.WindowsService -{ - public class ServiceProcessInstaller : System.ServiceProcess.ServiceProcessInstaller - { - public ServiceProcessInstaller() - { - this.Account = System.ServiceProcess.ServiceAccount.LocalSystem; - } - - public override void Install(System.Collections.IDictionary stateSaver) - { - var localuser = Context.Parameters["localuser"]; - if (localuser != null) - { - this.Account = System.ServiceProcess.ServiceAccount.User; - this.Username = localuser; - } - - - base.Install(stateSaver); - } - } -} diff --git a/Duplicati/WindowsService/WindowsService.csproj b/Duplicati/WindowsService/WindowsService.csproj index 1228c97a1..96fd61bb8 100644 --- a/Duplicati/WindowsService/WindowsService.csproj +++ b/Duplicati/WindowsService/WindowsService.csproj @@ -1,38 +1,30 @@  + net8.0 - Exe - Duplicati.WindowsService - Duplicati.WindowsService + Duplicati.WindowsService.Implementation - - app.manifest - - - - Runner.cs - - - Component - - - Component - - - Component - - - Component - - + + - - - - + + Runner.cs + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Executables/net48/Duplicati.CommandLine.AutoUpdater/Duplicati.CommandLine.AutoUpdater.csproj b/Executables/net48/Duplicati.CommandLine.AutoUpdater/Duplicati.CommandLine.AutoUpdater.csproj new file mode 100644 index 000000000..2526fcc2a --- /dev/null +++ b/Executables/net48/Duplicati.CommandLine.AutoUpdater/Duplicati.CommandLine.AutoUpdater.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + AutoUpdater tool for Duplicati + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net48/Duplicati.CommandLine.AutoUpdater/Program.cs b/Executables/net48/Duplicati.CommandLine.AutoUpdater/Program.cs new file mode 100644 index 000000000..4a1ef94a7 --- /dev/null +++ b/Executables/net48/Duplicati.CommandLine.AutoUpdater/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.CommandLine.AutoUpdater.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.CommandLine.AutoUpdater.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net48/Duplicati.CommandLine.BackendTester/Duplicati.CommandLine.BackendTester.csproj b/Executables/net48/Duplicati.CommandLine.BackendTester/Duplicati.CommandLine.BackendTester.csproj new file mode 100644 index 000000000..26b74761b --- /dev/null +++ b/Executables/net48/Duplicati.CommandLine.BackendTester/Duplicati.CommandLine.BackendTester.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + A backend debugging tool for Duplicati + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net48/Duplicati.CommandLine.BackendTester/Program.cs b/Executables/net48/Duplicati.CommandLine.BackendTester/Program.cs new file mode 100644 index 000000000..b16773448 --- /dev/null +++ b/Executables/net48/Duplicati.CommandLine.BackendTester/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.CommandLine.BackendTester.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.CommandLine.BackendTester.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net48/Duplicati.CommandLine.BackendTool/Duplicati.CommandLine.BackendTool.csproj b/Executables/net48/Duplicati.CommandLine.BackendTool/Duplicati.CommandLine.BackendTool.csproj new file mode 100644 index 000000000..3388efe5d --- /dev/null +++ b/Executables/net48/Duplicati.CommandLine.BackendTool/Duplicati.CommandLine.BackendTool.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + Tool for file-level access to remote destinations + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net48/Duplicati.CommandLine.BackendTool/Program.cs b/Executables/net48/Duplicati.CommandLine.BackendTool/Program.cs new file mode 100644 index 000000000..aacea31c5 --- /dev/null +++ b/Executables/net48/Duplicati.CommandLine.BackendTool/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.CommandLine.BackendTool.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.CommandLine.BackendTool.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net48/Duplicati.CommandLine.ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj b/Executables/net48/Duplicati.CommandLine.ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj new file mode 100644 index 000000000..63d79520d --- /dev/null +++ b/Executables/net48/Duplicati.CommandLine.ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + Configuration Import Tool for Duplicati + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net48/Duplicati.CommandLine.ConfigurationImporter/Program.cs b/Executables/net48/Duplicati.CommandLine.ConfigurationImporter/Program.cs new file mode 100644 index 000000000..2d9f8b5c7 --- /dev/null +++ b/Executables/net48/Duplicati.CommandLine.ConfigurationImporter/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.CommandLine.ConfigurationImporter.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.CommandLine.ConfigurationImporter.ConfigurationImporter.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net48/Duplicati.CommandLine.RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj b/Executables/net48/Duplicati.CommandLine.RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj new file mode 100644 index 000000000..6909d652e --- /dev/null +++ b/Executables/net48/Duplicati.CommandLine.RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + RecoveryTool for Duplicati + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net48/Duplicati.CommandLine.RecoveryTool/Program.cs b/Executables/net48/Duplicati.CommandLine.RecoveryTool/Program.cs new file mode 100644 index 000000000..5c7e2848d --- /dev/null +++ b/Executables/net48/Duplicati.CommandLine.RecoveryTool/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.CommandLine.RecoveryTool.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.CommandLine.RecoveryTool.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net48/Duplicati.CommandLine/Duplicati.CommandLine.csproj b/Executables/net48/Duplicati.CommandLine/Duplicati.CommandLine.csproj new file mode 100644 index 000000000..ffe763801 --- /dev/null +++ b/Executables/net48/Duplicati.CommandLine/Duplicati.CommandLine.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + Commandline implementation of Duplicati + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Executables/net48/Duplicati.CommandLine/Program.cs b/Executables/net48/Duplicati.CommandLine/Program.cs new file mode 100644 index 000000000..beda17e05 --- /dev/null +++ b/Executables/net48/Duplicati.CommandLine/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.CommandLine.CLI.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.CommandLine.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net48/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj b/Executables/net48/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj new file mode 100644 index 000000000..ac1a6d2cb --- /dev/null +++ b/Executables/net48/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj @@ -0,0 +1,21 @@ + + + + WinExe + net8.0 + The Duplicati Tray implementation + ..\..\..\Duplicati\GUI\Duplicati.GUI.TrayIcon\Duplicati.ico + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net48/Duplicati.GUI.TrayIcon/Program.cs b/Executables/net48/Duplicati.GUI.TrayIcon/Program.cs new file mode 100644 index 000000000..6e4552ba9 --- /dev/null +++ b/Executables/net48/Duplicati.GUI.TrayIcon/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.GUI.TrayIcon.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.GUI.TrayIcon.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net48/Duplicati.Server/Duplicati.Server.csproj b/Executables/net48/Duplicati.Server/Duplicati.Server.csproj new file mode 100644 index 000000000..10a157e5c --- /dev/null +++ b/Executables/net48/Duplicati.Server/Duplicati.Server.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + The Server Duplicati implementation + Duplicati.Server + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net48/Duplicati.Server/Program.cs b/Executables/net48/Duplicati.Server/Program.cs new file mode 100644 index 000000000..834667b9e --- /dev/null +++ b/Executables/net48/Duplicati.Server/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.Server.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.Server.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net48/Duplicati.WindowsService/Duplicati.WindowsService.csproj b/Executables/net48/Duplicati.WindowsService/Duplicati.WindowsService.csproj new file mode 100644 index 000000000..b3c3f96bc --- /dev/null +++ b/Executables/net48/Duplicati.WindowsService/Duplicati.WindowsService.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + WindowsService for Duplicati + Duplicati.WindowsService + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net48/Duplicati.WindowsService/Program.cs b/Executables/net48/Duplicati.WindowsService/Program.cs new file mode 100644 index 000000000..c7ba47cbc --- /dev/null +++ b/Executables/net48/Duplicati.WindowsService/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.WindowsService.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.WindowsService.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net48/Duplicati_Service/Duplicati.Service.csproj b/Executables/net48/Duplicati_Service/Duplicati.Service.csproj new file mode 100644 index 000000000..090650c3f --- /dev/null +++ b/Executables/net48/Duplicati_Service/Duplicati.Service.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + Service controller for Duplicati + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net48/Duplicati_Service/Program.cs b/Executables/net48/Duplicati_Service/Program.cs new file mode 100644 index 000000000..53eeb44b6 --- /dev/null +++ b/Executables/net48/Duplicati_Service/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.Service.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.Service.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net5/Duplicati.CommandLine.AutoUpdater/Duplicati.CommandLine.AutoUpdater.csproj b/Executables/net5/Duplicati.CommandLine.AutoUpdater/Duplicati.CommandLine.AutoUpdater.csproj new file mode 100644 index 000000000..2526fcc2a --- /dev/null +++ b/Executables/net5/Duplicati.CommandLine.AutoUpdater/Duplicati.CommandLine.AutoUpdater.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + AutoUpdater tool for Duplicati + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net5/Duplicati.CommandLine.AutoUpdater/Program.cs b/Executables/net5/Duplicati.CommandLine.AutoUpdater/Program.cs new file mode 100644 index 000000000..4a1ef94a7 --- /dev/null +++ b/Executables/net5/Duplicati.CommandLine.AutoUpdater/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.CommandLine.AutoUpdater.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.CommandLine.AutoUpdater.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net5/Duplicati.CommandLine.BackendTester/Duplicati.CommandLine.BackendTester.csproj b/Executables/net5/Duplicati.CommandLine.BackendTester/Duplicati.CommandLine.BackendTester.csproj new file mode 100644 index 000000000..61422bfae --- /dev/null +++ b/Executables/net5/Duplicati.CommandLine.BackendTester/Duplicati.CommandLine.BackendTester.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + A backend debugging tool for Duplicati + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net5/Duplicati.CommandLine.BackendTester/Program.cs b/Executables/net5/Duplicati.CommandLine.BackendTester/Program.cs new file mode 100644 index 000000000..b16773448 --- /dev/null +++ b/Executables/net5/Duplicati.CommandLine.BackendTester/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.CommandLine.BackendTester.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.CommandLine.BackendTester.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net5/Duplicati.CommandLine.BackendTool/Duplicati.CommandLine.BackendTool.csproj b/Executables/net5/Duplicati.CommandLine.BackendTool/Duplicati.CommandLine.BackendTool.csproj new file mode 100644 index 000000000..3388efe5d --- /dev/null +++ b/Executables/net5/Duplicati.CommandLine.BackendTool/Duplicati.CommandLine.BackendTool.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + Tool for file-level access to remote destinations + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net5/Duplicati.CommandLine.BackendTool/Program.cs b/Executables/net5/Duplicati.CommandLine.BackendTool/Program.cs new file mode 100644 index 000000000..aacea31c5 --- /dev/null +++ b/Executables/net5/Duplicati.CommandLine.BackendTool/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.CommandLine.BackendTool.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.CommandLine.BackendTool.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net5/Duplicati.CommandLine.ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj b/Executables/net5/Duplicati.CommandLine.ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj new file mode 100644 index 000000000..6f71d33f5 --- /dev/null +++ b/Executables/net5/Duplicati.CommandLine.ConfigurationImporter/Duplicati.CommandLine.ConfigurationImporter.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + Configuration Import Tool for Duplicati + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net5/Duplicati.CommandLine.ConfigurationImporter/Program.cs b/Executables/net5/Duplicati.CommandLine.ConfigurationImporter/Program.cs new file mode 100644 index 000000000..2d9f8b5c7 --- /dev/null +++ b/Executables/net5/Duplicati.CommandLine.ConfigurationImporter/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.CommandLine.ConfigurationImporter.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.CommandLine.ConfigurationImporter.ConfigurationImporter.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net5/Duplicati.CommandLine.RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj b/Executables/net5/Duplicati.CommandLine.RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj new file mode 100644 index 000000000..6909d652e --- /dev/null +++ b/Executables/net5/Duplicati.CommandLine.RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + RecoveryTool for Duplicati + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net5/Duplicati.CommandLine.RecoveryTool/Program.cs b/Executables/net5/Duplicati.CommandLine.RecoveryTool/Program.cs new file mode 100644 index 000000000..5c7e2848d --- /dev/null +++ b/Executables/net5/Duplicati.CommandLine.RecoveryTool/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.CommandLine.RecoveryTool.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.CommandLine.RecoveryTool.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net5/Duplicati.CommandLine/Duplicati.CommandLine.csproj b/Executables/net5/Duplicati.CommandLine/Duplicati.CommandLine.csproj new file mode 100644 index 000000000..ffe763801 --- /dev/null +++ b/Executables/net5/Duplicati.CommandLine/Duplicati.CommandLine.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + Commandline implementation of Duplicati + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Executables/net5/Duplicati.CommandLine/Program.cs b/Executables/net5/Duplicati.CommandLine/Program.cs new file mode 100644 index 000000000..beda17e05 --- /dev/null +++ b/Executables/net5/Duplicati.CommandLine/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.CommandLine.CLI.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.CommandLine.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net5/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj b/Executables/net5/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj new file mode 100644 index 000000000..5ea7662c1 --- /dev/null +++ b/Executables/net5/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj @@ -0,0 +1,21 @@ + + + + WinExe + net8.0 + The Duplicati Tray implementation + ..\..\..\Duplicati\GUI\Duplicati.GUI.TrayIcon\Duplicati.ico + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net5/Duplicati.GUI.TrayIcon/Program.cs b/Executables/net5/Duplicati.GUI.TrayIcon/Program.cs new file mode 100644 index 000000000..6e4552ba9 --- /dev/null +++ b/Executables/net5/Duplicati.GUI.TrayIcon/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.GUI.TrayIcon.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.GUI.TrayIcon.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net5/Duplicati.Server/Duplicati.Server.csproj b/Executables/net5/Duplicati.Server/Duplicati.Server.csproj new file mode 100644 index 000000000..10a157e5c --- /dev/null +++ b/Executables/net5/Duplicati.Server/Duplicati.Server.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + The Server Duplicati implementation + Duplicati.Server + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net5/Duplicati.Server/Program.cs b/Executables/net5/Duplicati.Server/Program.cs new file mode 100644 index 000000000..834667b9e --- /dev/null +++ b/Executables/net5/Duplicati.Server/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.Server.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.Server.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net5/Duplicati.WindowsService/Duplicati.WindowsService.csproj b/Executables/net5/Duplicati.WindowsService/Duplicati.WindowsService.csproj new file mode 100644 index 000000000..b0b830e20 --- /dev/null +++ b/Executables/net5/Duplicati.WindowsService/Duplicati.WindowsService.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + WindowsService for Duplicati + Duplicati.WindowsService + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net5/Duplicati.WindowsService/Program.cs b/Executables/net5/Duplicati.WindowsService/Program.cs new file mode 100644 index 000000000..c7ba47cbc --- /dev/null +++ b/Executables/net5/Duplicati.WindowsService/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.WindowsService.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.WindowsService.Program.Main(args); + } +} \ No newline at end of file diff --git a/Executables/net5/Duplicati_Service/Duplicati.Service.csproj b/Executables/net5/Duplicati_Service/Duplicati.Service.csproj new file mode 100644 index 000000000..01dc7828c --- /dev/null +++ b/Executables/net5/Duplicati_Service/Duplicati.Service.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + Exe + Service controller for Duplicati + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Executables/net5/Duplicati_Service/Program.cs b/Executables/net5/Duplicati_Service/Program.cs new file mode 100644 index 000000000..53eeb44b6 --- /dev/null +++ b/Executables/net5/Duplicati_Service/Program.cs @@ -0,0 +1,9 @@ +namespace Duplicati.Service.Net5 +{ + // Wrapper class to keep code independent + public static class Program + { + public static int Main(string[] args) + => Duplicati.Service.Program.Main(args); + } +} \ No newline at end of file diff --git a/Installer/Makefile/Makefile b/Installer/Makefile/Makefile deleted file mode 100755 index 550e7b9f1..000000000 --- a/Installer/Makefile/Makefile +++ /dev/null @@ -1,85 +0,0 @@ -BUILD_TOOL=msbuild -BUILD_ARGS=/property:Platform=Any\ CPU /property:Configuration=Release -BUILD_ARGS_DEBUG=/property:Platform=Any\ CPU /property:Configuration=Debug - -NUGET_TOOL=nuget -NUGET_ARGS=restore - -OUTPUT_ARGS=/property:OutputPath=bin/Release -SOURCE_DIR=../../. -PACKAGE_DIR=./pkg -PKG_TARGET=$(PACKAGE_DIR)/usr/lib/duplicati - -all: package - -build: - $(NUGET_TOOL) $(NUGET_ARGS) "$(SOURCE_DIR)/Duplicati.sln" - $(BUILD_TOOL) $(BUILD_ARGS) "$(SOURCE_DIR)/Duplicati.sln" - $(BUILD_TOOL) $(BUILD_ARGS) "$(SOURCE_DIR)/BuildTools/UpdateVersionStamp/UpdateVersionStamp.sln" - -debug: - $(NUGET_TOOL) $(NUGET_ARGS) "$(SOURCE_DIR)/Duplicati.sln" - $(BUILD_TOOL) $(BUILD_ARGS_DEBUG) "$(SOURCE_DIR)/Duplicati.sln" - $(BUILD_TOOL) $(BUILD_ARGS_DEBUG) "$(SOURCE_DIR)/BuildTools/UpdateVersionStamp/UpdateVersionStamp.sln" - -package: build - if [ -e "$(PACKAGE_DIR)" ]; then \ - rm -rf "$(PACKAGE_DIR)"; \ - fi - mkdir "$(PACKAGE_DIR)" - mkdir "$(PACKAGE_DIR)/usr" - mkdir "$(PACKAGE_DIR)/usr/lib" - mkdir "$(PACKAGE_DIR)/usr/bin" - mkdir "$(PACKAGE_DIR)/usr/share" - mkdir "$(PACKAGE_DIR)/usr/share/applications" - mkdir "$(PACKAGE_DIR)/usr/share/pixmaps" - mkdir "$(PACKAGE_DIR)/usr/share/doc" - mkdir "$(PACKAGE_DIR)/usr/share/doc/duplicati" - cp -R "$(SOURCE_DIR)/Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release" "$(PACKAGE_DIR)/usr/lib/duplicati" - - rm -rf "$(PKG_TARGET)/SQLite" - rm -rf "$(PKG_TARGET)/win-tools" - rm -rf "$(PKG_TARGET)/"*.mdb - - cp -R "$(SOURCE_DIR)/Duplicati/Server/webroot" "$(PACKAGE_DIR)/usr/lib/duplicati/" - cp "$(SOURCE_DIR)/Installer/debian/duplicati-launcher.sh" "$(PACKAGE_DIR)/usr/bin/duplicati" - cp "$(SOURCE_DIR)/Installer/debian/duplicati-commandline-launcher.sh" "$(PACKAGE_DIR)/usr/bin/duplicati-cli" - cp "$(SOURCE_DIR)/Installer/debian/duplicati-server-launcher.sh" "$(PACKAGE_DIR)/usr/bin/duplicati-server" - cp "$(SOURCE_DIR)/Installer/debian/duplicati.desktop" "$(PACKAGE_DIR)/usr/share/applications/" - cp "$(SOURCE_DIR)/Installer/debian/duplicati.xpm" "$(PACKAGE_DIR)/usr/share/pixmaps/" - cp "$(SOURCE_DIR)/Installer/debian/duplicati.png" "$(PACKAGE_DIR)/usr/share/pixmaps/" - - # If we edit files, we sometimes get backup files included - find "$(PACKAGE_DIR)" -type f -name \*\~ -exec rm -rf '{}' \; - - #Set permissions - #chown -R root:root "$(PACKAGE_DIR)" - chmod -R 755 "$(PACKAGE_DIR)" - - find "$(PACKAGE_DIR)" -type d -exec chmod 755 "{}" \; - find "$(PACKAGE_DIR)" -type f -exec chmod 644 "{}" \; - - for EXT in exe py sh; do \ - find "$(PACKAGE_DIR)" -type f -name \*.$(EXT) -exec chmod 755 "{}" \; ; \ - done - - chmod +x "$(PACKAGE_DIR)/usr/bin/duplicati" - chmod +x "$(PACKAGE_DIR)/usr/bin/duplicati-cli" - - if [ -e "duplicati.tar" ]; then \ - rm -rf "duplicati.tar"; \ - fi - if [ -e "duplicati.tar.gz" ]; then \ - rm -rf "duplicati.tar.gz"; \ - fi - - tar -C "$(PACKAGE_DIR)" -cf "duplicati.tar" . - gzip --best "duplicati.tar" - -clean: - (find "$(SOURCE_DIR)" -type d -name bin -exec rm -rf "{}" \; &> /dev/null) || echo "bin cleaned" - (find "$(SOURCE_DIR)" -type d -name obj -exec rm -rf "{}" \; &> /dev/null) || echo "obj cleaned" - rm -rf "$(PACKAGE_DIR)" - - -.PHONY: build all clean package debug diff --git a/Installer/OSX/Duplicati.icns b/Installer/OSX/dmg/Duplicati.icns similarity index 100% rename from Installer/OSX/Duplicati.icns rename to Installer/OSX/dmg/Duplicati.icns diff --git a/Installer/OSX/Info.plist b/Installer/OSX/dmg/Info.plist similarity index 88% rename from Installer/OSX/Info.plist rename to Installer/OSX/dmg/Info.plist index 72f6c37df..21d44ef69 100644 --- a/Installer/OSX/Info.plist +++ b/Installer/OSX/dmg/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion English CFBundleExecutable - duplicati + Duplicati.GUI.TrayIcon CFBundleIconFile Duplicati.icns CFBundleIdentifier @@ -24,6 +24,8 @@ Duplicati CFBundleShortVersionString !LONG_VERSION! + NSPrincipalClass + NSApplication LSUIElement diff --git a/Installer/OSX/dmg/build.sh b/Installer/OSX/dmg/build.sh new file mode 100755 index 000000000..575089239 --- /dev/null +++ b/Installer/OSX/dmg/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash +#This is a helper to make the macos app. It assumes a bin folder exists a the root of the project with an unzipped install + +SCRIPTDIR=$( cd "$(dirname "$0")" ; pwd -P ) + +VERSION=`grep '' < $SCRIPTDIR/../../../Executables/net5/Duplicati.Server/Duplicati.Server.csproj | sed 's/.*\([^\.]*\.[^\.]*\.[^\.]*\).*<\/Version>.*/\1/'` +VERSION=${VERSION//$'\r\n'} +echo "Building version: ($VERSION)" +export VERSION_NUMBER=$VERSION + +$SCRIPTDIR/make-dmg.sh $SCRIPTDIR/../../../bin/ + diff --git a/Installer/OSX/dmg/make-dmg.sh b/Installer/OSX/dmg/make-dmg.sh new file mode 100755 index 000000000..b89b8b5b0 --- /dev/null +++ b/Installer/OSX/dmg/make-dmg.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# +SCRIPTDIR=$( cd "$(dirname "$0")" ; pwd -P ) +SRC=$1 + +WC_DMG=wc.dmg +WC_DIR=wc +TEMPLATE_DMG=template.dmg +OUTPUT_DMG=Duplicati.dmg +UNWANTED_FILES="win-tools control_dir Duplicati.sqlite Duplicati-server.sqlite run-script-example.bat lvm-scripts Duplicati.debug.log" + +TEMPLATE_DMG_BZ2=$(echo "$TEMPLATE_DMG.bz2") +DELETE_DMG=0 + +if [ -f "$SCRIPTDIR/$TEMPLATE_DMG_BZ2" ]; then + if [ -f "$SCRIPTDIR/$TEMPLATE_DMG" ]; then + rm -rf "$SCRIPTDIR/$TEMPLATE_DMG" + fi + + bzip2 --decompress --keep --quiet "$SCRIPTDIR/$TEMPLATE_DMG_BZ2" + DELETE_DMG=1 +fi + +if [ ! -f "$SCRIPTDIR/$TEMPLATE_DMG" ]; then + echo "Template file $TEMPLATE_DMG not found" + exit +fi + +if [ ! -d "$SRC" ]; then + echo "Please supply a source directory as the first argument" + exit +fi + +OUTPUT_DMG="$SRC/../$OUTPUT_DMG" + +if [ -z ${VERSION_NUMBER+x} ]; then + echo "Please set the VERSION_NUMBER environment variable before calling" + VERSION_NUMBER=0.0.0 +fi + +VERSION_NAME="Duplicati" +if [ -e "${OUTPUT_DMG}" ]; then + rm -rf "${OUTPUT_DMG}" +fi + +# Remove any existing work copy +if [ -e "Duplicati.app" ]; then + sudo rm -rf "Duplicati.app" +fi + +# Create folder structure +mkdir "Duplicati.app" +mkdir "Duplicati.app/Contents" +mkdir "Duplicati.app/Contents/MacOS" +mkdir "Duplicati.app/Contents/Resources" + +# Extract the zip into the MacOS folder +cp -r $SRC "Duplicati.app/Contents/MacOS" + +# Install the Info.plist and icon, patch the plist file as well +echo Patching "$SCRIPTDIR/Info.plist" +PLIST=$(cat "$SCRIPTDIR/Info.plist") +PLIST=${PLIST//!LONG_VERSION!/${VERSION_NUMBER}} +echo "${PLIST}" > "Duplicati.app/Contents/Info.plist" +cp "$SCRIPTDIR/Duplicati.icns" "Duplicati.app/Contents/Resources" + +chmod +x "Duplicati.app/Contents/MacOS/Duplicati.GUI.TrayIcon" + +# Remove some of the files that we do not like +for FILE in $UNWANTED_FILES +do + if [ -e "Duplicati.app/Contents/MacOS/${FILE}" ] + then + rm -rf "Duplicati.app/Contents/MacOS/${FILE}" + fi +done + +# Set permissions +sudo chown -R root:admin "Duplicati.app" + +# Prepare a new dmg +echo "Building dmg" +if [ "$DELETE_DMG" -eq "1" ] +then + # If we have just extracted the dmg, use that as working copy + WC_DMG=$SCRIPTDIR/$TEMPLATE_DMG +else + # Otherwise we want a copy so we kan keep the original fresh + cp "$SCRIPTDIR/$TEMPLATE_DMG" "$WC_DMG" +fi + +# Make a mount point and mount the new dmg +mkdir -p "$WC_DIR" +hdiutil resize -size 300M "$WC_DMG" +hdiutil attach "$WC_DMG" -noautoopen -quiet -mountpoint "$WC_DIR" + +# Change the dmg name +echo "Setting dmg name to $VERSION_NAME" +diskutil quiet rename wc "$VERSION_NAME" + +# Make the Duplicati.app structure, root folder should exist +if [ -e "$WC_DIR/Duplicati.app" ] +then + rm -rf "$WC_DIR/Duplicati.app" +fi + +# Move in the prepared folder +sudo mv "Duplicati.app" "$WC_DIR/Duplicati.app" + +# Unmount the dmg +hdiutil detach "$WC_DIR" -quiet -force + +# Compress the dmg +hdiutil convert "$WC_DMG" -quiet -format UDZO -imagekey zlib-level=9 -o "${OUTPUT_DMG}" + +# Clean up +rm -rf "$WC_DMG" +rm -rf "$WC_DIR" + +echo "Done, created ${OUTPUT_DMG}" diff --git a/Installer/OSX/template.dmg.bz2 b/Installer/OSX/dmg/template.dmg.bz2 similarity index 100% rename from Installer/OSX/template.dmg.bz2 rename to Installer/OSX/dmg/template.dmg.bz2 diff --git a/Installer/OSX/make-dmg.sh b/Installer/OSX/make-dmg.sh deleted file mode 100644 index 0b090ab75..000000000 --- a/Installer/OSX/make-dmg.sh +++ /dev/null @@ -1,237 +0,0 @@ -#!/bin/sh -# -# DMG building script adopted from: -# http://el-tramo.be/git/fancy-dmg/plain/Makefile -# - -WC_DMG=wc.dmg -WC_DIR=wc -TEMPLATE_DMG=template.dmg -OUTPUT_DMG=Duplicati.dmg -OUTPUT_PKG=Duplicati.pkg -UNWANTED_FILES="appindicator-sharp.dll SQLite win-tools control_dir Duplicati.sqlite Duplicati-server.sqlite run-script-example.bat lvm-scripts Duplicati.debug.log SVGIcons storj_uplink.dll libstorj_uplink.so win-x64 win-x86" - -# These are set via the macos-gatekeeper file -CODESIGN_IDENTITY= -NOTARIZE_USERNAME= -NOTARIZE_PASSWORD= -GATEKEEPER_SETTINGS_FILE="${HOME}/.config/signkeys/Duplicati/macos-gatekeeper" - -if [ -f "${GATEKEEPER_SETTINGS_FILE}" ]; then - source "${GATEKEEPER_SETTINGS_FILE}" -fi - - -TEMPLATE_DMG_BZ2=$(echo "$TEMPLATE_DMG.bz2") -DELETE_DMG=0 - -if [ -f "$TEMPLATE_DMG_BZ2" ]; then - if [ -f "$TEMPLATE_DMG" ]; then - rm -rf "$TEMPLATE_DMG" - fi - - bzip2 --decompress --keep --quiet "$TEMPLATE_DMG_BZ2" - DELETE_DMG=1 -fi - -if [ ! -f "$TEMPLATE_DMG" ]; then - echo "Template file $TEMPLATE_DMG not found" - exit -fi - -if [ ! -f "$1" ]; then - echo "Please supply a packaged zip file as the first input argument" - exit -fi - -ZIPNAME=$(basename "$1") -VERSION_NUMBER=$(echo "$ZIPNAME" | awk -F- '{print $2}' | awk -F_ '{print $1}') - -VERSION_NAME="Duplicati" -if [ -e "${OUTPUT_DMG}" ]; then - rm -rf "${OUTPUT_DMG}" -fi - -if [ -e "${OUTPUT_PKG}" ]; then - rm -rf "${OUTPUT_PKG}" -fi - -# Remove any existing work copy -if [ -e "Duplicati.app" ]; then - sudo rm -rf "Duplicati.app" -fi - -# Create folder structure -mkdir "Duplicati.app" -mkdir "Duplicati.app/Contents" -mkdir "Duplicati.app/Contents/MacOS" -mkdir "Duplicati.app/Contents/Resources" - -# Extract the zip into the Resouces folder -unzip -q "$1" -d "Duplicati.app/Contents/Resources" - -# Install the Info.plist and icon, patch the plist file as well -PLIST=$(cat "Info.plist") -PLIST=${PLIST//!LONG_VERSION!/${VERSION_NUMBER}} -echo ${PLIST} > "Duplicati.app/Contents/Info.plist" -cp "Duplicati.icns" "Duplicati.app/Contents/Resources" - -for n in "../oem" "../../oem" "../../../oem" -do - if [ -d $n ]; then - echo "Installing OEM files" - cp -R $n Duplicati.app/Contents/Resources/webroot/ - fi -done - -for n in "oem-app-name.txt" "oem-update-url.txt" "oem-update-key.txt" "oem-update-readme.txt" "oem-update-installid.txt" -do - for p in "../$n" "../../$n" "../../../$n" - do - if [ -f $p ]; then - echo "Installing OEM override file" - cp $p Duplicati.app/Contents/Resources - fi - done -done - - -# Install the LauncAgent if anyone needs it -cp -R "daemon" "Duplicati.app/Contents/Resources" - -# Build launchers -cd "launchers" -bash "compile.sh" -cd .. - -# Install launchers -mv "launchers/bin/duplicati" "Duplicati.app/Contents/MacOS/duplicati" -mv "launchers/bin/duplicati-cli" "Duplicati.app/Contents/MacOS/duplicati-cli" -mv "launchers/bin/duplicati-server" "Duplicati.app/Contents/MacOS/duplicati-server" -cp "uninstall.sh" "Duplicati.app/Contents/MacOS/" - -chmod +x "Duplicati.app/Contents/MacOS/duplicati" -chmod +x "Duplicati.app/Contents/MacOS/duplicati-cli" -chmod +x "Duplicati.app/Contents/MacOS/duplicati-server" -chmod +x "Duplicati.app/Contents/MacOS/uninstall.sh" - -# Remove some of the files that we do not like -for FILE in $UNWANTED_FILES -do - if [ -e "Duplicati.app/Contents/Resources/${FILE}" ] - then - rm -rf "Duplicati.app/Contents/Resources/${FILE}" - fi -done - -# Codesign the app bundle -if [ "x${CODESIGN_IDENTITY}" != "x" ]; then - echo "Codesigning application bundle" - - #Do a poke to get sudo prompt up before the long-running sign-process - UNUSED=$(sudo ls) - - # Codesign all resources in bundle (i.e. the actual code) - # Not required, but nice-to-have - find "Duplicati.app/Contents/Resources" -type f -print0 | xargs -0 codesign -s "${CODESIGN_IDENTITY}" - - # These files have dependencies, so we need to sign them in the correct order - for file in "duplicati-cli" "duplicati-server" "uninstall.sh"; do - codesign -f --options=runtime -s "${CODESIGN_IDENTITY}" "Duplicati.app/Contents/MacOS/${file}" - done - - # Then sign the whole package - codesign -f --options=runtime -s "${CODESIGN_IDENTITY}" "Duplicati.app" -else - echo "No codesign identity supplied, skipping bundle signing" -fi - -# Set permissions -sudo chown -R root:admin "Duplicati.app" -sudo chown -R root:wheel "daemon/com.duplicati.app.launchagent.plist" -sudo chmod -R 644 "daemon/com.duplicati.app.launchagent.plist" -sudo chmod +x daemon-scripts/postinstall -sudo chmod +x daemon-scripts/preinstall -sudo chmod +x app-scripts/postinstall -sudo chmod +x app-scripts/preinstall - - -if [ -f "DuplicatiApp.pkg" ]; then - rm -rf "DuplicatiApp.pkg" -fi - -if [ -f "DuplicatiDaemon.pkg" ]; then - rm -rf "DuplicatiDaemon.pkg" -fi - -# Make a PKG file, commented out lines can be uncommented to re-generate the lists -#pkgbuild --analyze --root "./Duplicati.app" --install-location /Applications/Duplicati.app "InstallerComponent.plist" -pkgbuild --scripts app-scripts --identifier com.duplicati.app --root "./Duplicati.app" --install-location /Applications/Duplicati.app --component-plist "InstallerComponent.plist" "DuplicatiApp.pkg" -pkgbuild --scripts daemon-scripts --identifier com.duplicati.app.daemon --root "./daemon" --install-location /Library/LaunchAgents "DuplicatiDaemon.pkg" - -#productbuild --synthesize --package "DuplicatiApp.pkg" "Distribution.xml" -productbuild --distribution "Distribution.xml" --package-path "." --resources "." "${OUTPUT_PKG}" - -# Alternate to allow fixing the package -#productbuild --distribution "Distribution.xml" --package-path . "DuplicatiTmp.pkg" -#pkgutil --expand "DuplicatiTmp.pkg" "DuplicatiIntermediate" -#pkgutil --flatten "DuplicatiIntermediate" "Duplicati.pkg" -#rm -rf "DuplicatiTmp.pkg" - -rm -rf "DuplicatiApp.pkg" -rm -rf "DuplicatiDaemon.pkg" - -if [ "x${CODESIGN_IDENTITY}" != "x" ]; then - echo "Codesigning installer package" - productsign --sign "${CODESIGN_IDENTITY}" "${OUTPUT_PKG}" "${OUTPUT_PKG}.signed" - mv "${OUTPUT_PKG}.signed" "${OUTPUT_PKG}" -else - echo "No codesign identity supplied, skipping package signing" -fi - -# Prepare a new dmg -echo "Building dmg" -if [ "$DELETE_DMG" -eq "1" ] -then - # If we have just extracted the dmg, use that as working copy - WC_DMG=$TEMPLATE_DMG -else - # Otherwise we want a copy so we kan keep the original fresh - cp "$TEMPLATE_DMG" "$WC_DMG" -fi - -# Make a mount point and mount the new dmg -mkdir -p "$WC_DIR" -hdiutil attach "$WC_DMG" -noautoopen -quiet -mountpoint "$WC_DIR" - -# Change the dmg name -echo "Setting dmg name to $VERSION_NAME" -diskutil quiet rename wc "$VERSION_NAME" - -# Make the Duplicati.app structure, root folder should exist -if [ -e "$WC_DIR/Duplicati.app" ] -then - rm -rf "$WC_DIR/Duplicati.app" -fi - -# Move in the prepared folder -sudo mv "Duplicati.app" "$WC_DIR/Duplicati.app" - -# Unmount the dmg -hdiutil detach "$WC_DIR" -quiet -force - -# Compress the dmg -hdiutil convert "$WC_DMG" -quiet -format UDZO -imagekey zlib-level=9 -o "${OUTPUT_DMG}" - -# Clean up -rm -rf "$WC_DMG" -rm -rf "$WC_DIR" - -if [ "x${CODESIGN_IDENTITY}" != "x" ]; then - echo "Codesigning DMG image" - codesign -f --options=runtime -s "${CODESIGN_IDENTITY}" "${OUTPUT_DMG}" -else - echo "No codesign identity supplied, skipping DMG signing" -fi - -echo "Done, created ${OUTPUT_DMG}" diff --git a/Installer/OSX/uninstall.sh b/Installer/OSX/uninstall.sh old mode 100644 new mode 100755 diff --git a/Installer/Synology/INFO b/Installer/Synology/INFO deleted file mode 100755 index 1af1ba15a..000000000 --- a/Installer/Synology/INFO +++ /dev/null @@ -1,18 +0,0 @@ -package="Duplicati" -support_url="https://www.duplicati.com/" -displayname="Duplicati" -maintainer="The Duplicati Team" -maintainer_url="http://github.com/duplicati/duplicati" -distributor="The Duplicati Team" -distributor_url="https://www.duplicati.com/" -arch="noarch" -dsmuidir="webroot" -dsmappname="SYNO.BACKUP.DUPLICATI" -support_center="no" -install_dep_services="mono" -start_dep_services="mono" -thirdparty="yes" -support_conf_folder="yes" -description="Duplicati is a free, open source, backup client that securely stores encrypted, incremental, compressed backups on cloud storage services and remote file servers." -firmware="6.0-7300" -startstop_restart_services="nginx" diff --git a/Installer/Synology/LICENSE b/Installer/Synology/LICENSE deleted file mode 100644 index b400c6896..000000000 --- a/Installer/Synology/LICENSE +++ /dev/null @@ -1,17 +0,0 @@ -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. \ No newline at end of file diff --git a/Installer/Synology/PACKAGE_ICON.PNG b/Installer/Synology/PACKAGE_ICON.PNG deleted file mode 100644 index ea42debab..000000000 Binary files a/Installer/Synology/PACKAGE_ICON.PNG and /dev/null differ diff --git a/Installer/Synology/PACKAGE_ICON_120.PNG b/Installer/Synology/PACKAGE_ICON_120.PNG deleted file mode 100644 index 377d9c273..000000000 Binary files a/Installer/Synology/PACKAGE_ICON_120.PNG and /dev/null differ diff --git a/Installer/Synology/PACKAGE_ICON_256.PNG b/Installer/Synology/PACKAGE_ICON_256.PNG deleted file mode 100644 index 0605c4ed9..000000000 Binary files a/Installer/Synology/PACKAGE_ICON_256.PNG and /dev/null differ diff --git a/Installer/Synology/dsm.duplicati.conf b/Installer/Synology/dsm.duplicati.conf deleted file mode 100644 index f6971d22b..000000000 --- a/Installer/Synology/dsm.duplicati.conf +++ /dev/null @@ -1,12 +0,0 @@ -location ~ ^/webman/3rdparty/Duplicati/((.*)\.cgi|api/(.*))$ { - proxy_set_header X-Server-IP $server_addr; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Real-HTTPS $https; - proxy_set_header X-Server-Port $server_port; - proxy_set_header X-Real-Port $remote_port; - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-Host $http_host; - - proxy_http_version 1.1; - proxy_pass http://127.0.0.1:8200/$1$is_args$args; -} \ No newline at end of file diff --git a/Installer/Synology/make-binary-package.sh b/Installer/Synology/make-binary-package.sh deleted file mode 100644 index e2cf0aac6..000000000 --- a/Installer/Synology/make-binary-package.sh +++ /dev/null @@ -1,148 +0,0 @@ -#!/bin/bash - -if [ ! -f "$1" ]; then - echo "Please provide the filename of an existing zip build as the first argument" - exit -fi - -FILENAME=$(basename $1) -DIRNAME=$(echo "${FILENAME}" | cut -d "_" -f 1) -VERSION=$(echo "${DIRNAME}" | cut -d "-" -f 2) -DATE_STAMP=$(LANG=C date -R) -BASE_FILE_NAME="${FILENAME%.*}" -TMPDIRNAME="${BASE_FILE_NAME}-extract" -MONO=/Library/Frameworks/Mono.framework/Commands/mono -GPG_KEYFILE="${HOME}/.config/signkeys/Duplicati/updater-gpgkey.key" - -# Sort on macOS does not have -V / --version-sort -# https://stackoverflow.com/questions/4493205/unix-sort-of-version-numbers -SORT_OPTIONS="-t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n" - -if [ -d "${DIRNAME}" ]; then - rm -rf "${DIRNAME}" -fi - -if [ -d "${TMPDIRNAME}" ]; then - rm -rf "${TMPDIRNAME}" -fi - -if [ -f "package.tgz" ]; then - rm -rf "package.tgz" -fi - -if [ -f "${BASE_FILE_NAME}.spk" ]; then - rm -rf "${BASE_FILE_NAME}.spk" -fi - -if [ -f "${BASE_FILE_NAME}.spk.tmp" ]; then - rm -rf "${BASE_FILE_NAME}.spk.tmp" -fi - -if [ -f "${BASE_FILE_NAME}.signature" ]; then - rm -rf "${BASE_FILE_NAME}.spk.signature" -fi - -TIMESERVER="http://timestamp.synology.com/timestamp.php" - -unzip -q -d "${DIRNAME}" "$1" - -for n in "../oem" "../../oem" "../../../oem" -do - if [ -d $n ]; then - echo "Installing OEM files" - cp -R $n "${DIRNAME}/webroot/" - fi -done - -for n in "oem-app-name.txt" "oem-update-url.txt" "oem-update-key.txt" "oem-update-readme.txt" "oem-update-installid.txt" -do - for p in "../$n" "../../$n" "../../../$n" - do - if [ -f $p ]; then - echo "Installing OEM override file" - cp $p "${DIRNAME}" - fi - done -done - -cd "${DIRNAME}" - -# Remove items unused on the Synology platform -rm -rf ./win-tools -rm -rf ./SQLite/win64 -rm -rf ./SQLite/win32 -rm -rf ./MonoMac.dll -rm -rf ./OSX\ Icons -rm -rf ./OSXTrayHost -rm -rf ./licenses/MonoMac -rm -rf ./licenses/gpg -rm -rf ./win-x64/storj_uplink.dll -rm -rf ./win-x86/storj_uplink.dll -rm -rf ./storj_uplink.dll -rm -rf ./libstorj_uplink.dylib - -# Install extra items for Synology -cp -R ../web-extra/* webroot/ -cp ../dsm.duplicati.conf . - -DIRSIZE_KB=$(BLOCKSIZE=1024 du -s | cut -d '.' -f 1) -let "DIRSIZE=DIRSIZE_KB*1024" - -tar cf ../package.tgz ./* -cd .. - -rm -rf "${DIRNAME}" - -ICON_72=$(openssl base64 -A -in PACKAGE_ICON.PNG) -ICON_256=$(openssl base64 -A -in PACKAGE_ICON_256.PNG) - -git checkout INFO -echo "version=\"${VERSION}\"" >> "INFO" -MD5=$(md5 "package.tgz" | awk -F ' ' '{print $NF}') -echo "checksum=\"${MD5}\"" >> "INFO" -echo "extractsize=\"${DIRSIZE_KB}\"" >> "INFO" -echo "package_icon=\"${ICON_72}\"" >> "INFO" -echo "package_icon_256=\"${ICON_256}\"" >> "INFO" - -chmod +x scripts/* - -tar cf "${BASE_FILE_NAME}.spk" INFO LICENSE *.PNG package.tgz scripts conf WIZARD_UIFILES - -git checkout INFO -rm package.tgz - -if [ -f "${GPG_KEYFILE}" ]; then - if [ "z${KEYFILE_PASSWORD}" == "z" ]; then - echo -n "Enter keyfile password: " - read -s KEYFILE_PASSWORD - echo - fi - - GPGDATA=$("${MONO}" "../../BuildTools/AutoUpdateBuilder/bin/Debug/SharpAESCrypt.exe" d "${KEYFILE_PASSWORD}" "${GPG_KEYFILE}") - if [ ! $? -eq 0 ]; then - echo "Decrypting GPG keyfile failed" - exit 1 - fi - GPGID=$(echo "${GPGDATA}" | head -n 1) - GPGKEY=$(echo "${GPGDATA}" | head -n 2 | tail -n 1) -else - echo "No GPG keyfile found, skipping gpg signing" -fi - -if [ "z${GPGID}" != "z" ]; then - # Now codesign the spk file - mkdir "${TMPDIRNAME}" - tar xf "${BASE_FILE_NAME}.spk" -C "${TMPDIRNAME}" - cat $(find ${TMPDIRNAME} -type f | sort ${SORT_OPTIONS}) > "${BASE_FILE_NAME}.spk.tmp" - - gpg2 --ignore-time-conflict --ignore-valid-from --yes --batch --armor --detach-sign --default-key="${GPGID}" --output "${BASE_FILE_NAME}.signature" "${BASE_FILE_NAME}.spk.tmp" - rm "${BASE_FILE_NAME}.spk.tmp" - - curl --silent --form "file=@${BASE_FILE_NAME}.signature" "${TIMESERVER}" > "${TMPDIRNAME}/syno_signature.asc" - rm "${BASE_FILE_NAME}.signature" - - rm "${BASE_FILE_NAME}.spk" - tar cf "${BASE_FILE_NAME}.spk" -C "${TMPDIRNAME}" $(ls -1 ${TMPDIRNAME}) - - rm -rf "${TMPDIRNAME}" -fi diff --git a/Installer/Synology/scripts/postinst b/Installer/Synology/scripts/postinst deleted file mode 100755 index bda2ac6af..000000000 --- a/Installer/Synology/scripts/postinst +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -mkdir -p /usr/local/bin - -echo "mono ${SYNOPKG_PKGDEST}/Duplicati.CommandLine.exe $@" > "/usr/local/bin/duplicati-cli" -chmod +x "/usr/local/bin/duplicati-cli" - -echo "" >> "${SYNOPKG_PKGDEST}/webroot/nph-proxy.cgi" -echo "mono ${SYNOPKG_PKGDEST}/CGIProxyHandler.exe" >> "${SYNOPKG_PKGDEST}/webroot/nph-proxy.cgi" -chmod +x "${SYNOPKG_PKGDEST}/webroot/nph-proxy.cgi" - -exit 0 \ No newline at end of file diff --git a/Installer/Synology/scripts/postuninst b/Installer/Synology/scripts/postuninst deleted file mode 100755 index d21cac945..000000000 --- a/Installer/Synology/scripts/postuninst +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -rm "/usr/local/bin/duplicati-cli" -exit 0 \ No newline at end of file diff --git a/Installer/Synology/scripts/postupgrade.sh b/Installer/Synology/scripts/postupgrade.sh deleted file mode 100755 index 03538c00a..000000000 --- a/Installer/Synology/scripts/postupgrade.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -exit 0 \ No newline at end of file diff --git a/Installer/Synology/scripts/preinst b/Installer/Synology/scripts/preinst deleted file mode 100755 index 03538c00a..000000000 --- a/Installer/Synology/scripts/preinst +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -exit 0 \ No newline at end of file diff --git a/Installer/Synology/scripts/preuninst b/Installer/Synology/scripts/preuninst deleted file mode 100755 index 03538c00a..000000000 --- a/Installer/Synology/scripts/preuninst +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -exit 0 \ No newline at end of file diff --git a/Installer/Synology/scripts/preupgrade.sh b/Installer/Synology/scripts/preupgrade.sh deleted file mode 100755 index 03538c00a..000000000 --- a/Installer/Synology/scripts/preupgrade.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -exit 0 \ No newline at end of file diff --git a/Installer/Synology/scripts/start-stop-status b/Installer/Synology/scripts/start-stop-status deleted file mode 100755 index c22c29b57..000000000 --- a/Installer/Synology/scripts/start-stop-status +++ /dev/null @@ -1,142 +0,0 @@ -#!/bin/sh -source /root/.profile # Get Environment Variables from Root Profile - -if ! type mono 2>/dev/null; then - for d in /volume*/@appstore/mono; do - if [ -x "$d/bin/mono" ]; then - PATH="$PATH:$d/bin" - export PATH - break - fi - done -fi - -# Package Varables -PACKAGE_NAME_SIMPLE="$(echo "$SYNOPKG_PKGNAME" | awk '{print tolower($0)}' | sed -e 's/ /_/g')" -PACKAGE_DIR="${SYNOPKG_PKGDEST}" -PACKAGE_UPGRADE_FLAG="/tmp/${PACKAGE_NAME_SIMPLE}.upgrade" - -# We need more space than what /tmp holds -PACKAGE_TEMP_DIR="${PACKAGE_DIR}/temp" - -# These control how the authentication is integrated -SYNO_LOGIN_CGI=/usr/syno/synoman/webman/login.cgi -SYNO_AUTHENTICATE_CGI=/usr/syno/synoman/webman/modules/authenticate.cgi - -# If all users should have access, set to "1" -SYNO_ALL_USERS=0 - -# If we should disable verification of Synology's XSRF tokens, set to "1" -SYNO_SKIP_XSRF=0 - -# If we should disable Synology auth completely, set to "0" -SYNO_DSM_AUTH=1 - - -# Start & Stop Varables -PID_FILE="/var/run/${PACKAGE_NAME_SIMPLE}.pid" - -DaemonStart() { - DaemonStatus - if [ $? == 0 ]; then - echo "Starting ${PACKAGE_NAME_SIMPLE}." - - mkdir -p "${PACKAGE_TEMP_DIR}" - - SYNO_DSM_AUTH=${SYNO_DSM_AUTH} \ - SYNO_SKIP_XSRF=${SYNO_SKIP_XSRF} \ - SYNO_ALL_USERS=${SYNO_ALL_USERS} \ - SYNO_AUTHENTICATE_CGI="${SYNO_AUTHENTICATE_CGI}" \ - SYNO_LOGIN_CGI="${SYNO_LOGIN_CGI}" \ - TMP_DIR="${PACKAGE_TEMP_DIR}" \ - TEMP="${PACKAGE_TEMP_DIR}" \ - mono "${PACKAGE_DIR}/Duplicati.Server.exe" & - echo $! > "$PID_FILE" - - cp -f ${PACKAGE_DIR}/dsm.duplicati.conf /usr/local/etc/nginx/conf.d/ - - else - echo "${PACKAGE_NAME_SIMPLE} already running." - fi -} - -DaemonDebug() { - DaemonStatus -} - -DaemonStop() { - DaemonStatus - if [ $? == 1 ]; then - echo "Stopping ${PACKAGE_NAME_SIMPLE}." - pkill -P $(cat "$PID_FILE") - rm -f "$PID_FILE" - - rm -f /usr/local/etc/nginx/conf.d/dsm.duplicati.conf - - sleep 3 - else - echo "Nothing to stop for ${PACKAGE_NAME_SIMPLE}." - fi -} - -DaemonStatus() { - if [ -f "$PID_FILE" ]; then - PID=$(cat "$PID_FILE") - - if [ -n "$(ps | grep $PID | grep -vn "grep $PID")" ]; then - echo "${PACKAGE_NAME_SIMPLE} is running ..." - return 1 # is running - else - echo "${PACKAGE_NAME_SIMPLE} is NOT running ..." - rm -f ${PID_FILE} # Remove Invalid PID - return 0 # is NOT running - fi - else - echo "${PACKAGE_NAME_SIMPLE} is NOT running ...." - return 0 # is NOT running - fi -} - -case $1 in - start) - DaemonStart - sleep 1 - DaemonStatus - exit $(( ! $? )) # [ $? == 1 ] && exit 0 || exit 1 # this if statement flips the boolean outcome. - ;; - stop) - DaemonStop - sleep 1 - DaemonStatus - exit $? - ;; - restart) - DaemonStop - sleep 10 - DaemonStart - sleep 1 - DaemonStatus - exit $(( ! $? )) # this if statement flips the boolean outcome. - ;; - status) - DaemonStatus - exit $(( ! $? )) # this if statement flips the boolean outcome. - ;; - debug) - DaemonDebug - exit 0 - ;; - log-show) - exit 0 - ;; - log-clear) - exit 0 - ;; - log) - exit 0 - ;; - *) - echo "Usage: $0 {start|stop|restart|status|debug|log|log-show|log-clear}" - exit 1 - ;; -esac diff --git a/Installer/Synology/web-extra/config b/Installer/Synology/web-extra/config deleted file mode 100644 index 8aadc00c2..000000000 --- a/Installer/Synology/web-extra/config +++ /dev/null @@ -1,12 +0,0 @@ -{ - ".url": { - "com.duplicati.App": { - "type": "legacy", - "allUsers": false, - "title": "Duplicati", - "desc": "Use Duplicati to backup your data to cloud services", - "icon": "synoimg/app_{0}.png", - "url": "webman/3rdparty/Duplicati/index.html" - } - } -} \ No newline at end of file diff --git a/Installer/Synology/web-extra/package/ngax/package.js b/Installer/Synology/web-extra/package/ngax/package.js deleted file mode 100644 index 027f067e7..000000000 --- a/Installer/Synology/web-extra/package/ngax/package.js +++ /dev/null @@ -1,49 +0,0 @@ -backupApp.service('ProxyService', function(AppService, $http) { - var origconfig = AppService.proxy_config; - var synotoken = null; - var is_grabbing = false; - - try - { - if (synotoken == null) - synotoken = SYNO.SDS.Session.SynoToken; - } - catch (e) {} - - try - { - if (synotoken == null) - synotoken = window.parent.SYNO.SDS.Session.SynoToken; - } - catch (e) {} - - function grab_syno_token() { - if (synotoken != null || is_grabbing) - return; - - is_grabbing = true; - $http.get('/webman/login.cgi?_dc=' + Math.random()).then( - function(data) { - is_grabbing = false; - synotoken = data.data.SynoToken; - }, - - function() { - is_grabbing = false; - } - ); - }; - - AppService.proxy_config = function(method, options, data, targeturl) { - grab_syno_token(); - - if (synotoken != null) - options.headers['X-Syno-Token'] = synotoken; - - if (origconfig != null) - origconfig(method, options, data, targeturl); - }; - - grab_syno_token(); - -}); \ No newline at end of file diff --git a/Installer/Synology/web-extra/synoimg/app_128.png b/Installer/Synology/web-extra/synoimg/app_128.png deleted file mode 100644 index 01bc68497..000000000 Binary files a/Installer/Synology/web-extra/synoimg/app_128.png and /dev/null differ diff --git a/Installer/Synology/web-extra/synoimg/app_16.png b/Installer/Synology/web-extra/synoimg/app_16.png deleted file mode 100644 index 9dd644e77..000000000 Binary files a/Installer/Synology/web-extra/synoimg/app_16.png and /dev/null differ diff --git a/Installer/Synology/web-extra/synoimg/app_192.png b/Installer/Synology/web-extra/synoimg/app_192.png deleted file mode 100644 index 1ba11bc33..000000000 Binary files a/Installer/Synology/web-extra/synoimg/app_192.png and /dev/null differ diff --git a/Installer/Synology/web-extra/synoimg/app_24.png b/Installer/Synology/web-extra/synoimg/app_24.png deleted file mode 100644 index 5b31ed070..000000000 Binary files a/Installer/Synology/web-extra/synoimg/app_24.png and /dev/null differ diff --git a/Installer/Synology/web-extra/synoimg/app_256.png b/Installer/Synology/web-extra/synoimg/app_256.png deleted file mode 100644 index 4600ffd78..000000000 Binary files a/Installer/Synology/web-extra/synoimg/app_256.png and /dev/null differ diff --git a/Installer/Synology/web-extra/synoimg/app_32.png b/Installer/Synology/web-extra/synoimg/app_32.png deleted file mode 100644 index 15cf40684..000000000 Binary files a/Installer/Synology/web-extra/synoimg/app_32.png and /dev/null differ diff --git a/Installer/Synology/web-extra/synoimg/app_48.png b/Installer/Synology/web-extra/synoimg/app_48.png deleted file mode 100644 index 5184f62c6..000000000 Binary files a/Installer/Synology/web-extra/synoimg/app_48.png and /dev/null differ diff --git a/Installer/Synology/web-extra/synoimg/app_64.png b/Installer/Synology/web-extra/synoimg/app_64.png deleted file mode 100644 index a1f02a530..000000000 Binary files a/Installer/Synology/web-extra/synoimg/app_64.png and /dev/null differ diff --git a/Installer/Synology/web-extra/synoimg/app_72.png b/Installer/Synology/web-extra/synoimg/app_72.png deleted file mode 100644 index f353c59d3..000000000 Binary files a/Installer/Synology/web-extra/synoimg/app_72.png and /dev/null differ diff --git a/Installer/Windows/MSI/build.sh b/Installer/Windows/MSI/build.sh new file mode 100644 index 000000000..a0498f4bd --- /dev/null +++ b/Installer/Windows/MSI/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash +#This is a helper to make the msi. It assumes a bin folder exists a the root of the project with an unzipped install + +SCRIPTDIR=$( cd "$(dirname "$0")" ; pwd -P ) + +VERSION=`grep '' < $SCRIPTDIR/../../../Executables/net5/Duplicati.Server/Duplicati.Server.csproj | sed 's/.*\([^\.]*\.[^\.]*\.[^\.]*\).*<\/Version>.*/\1/'` +VERSION=${VERSION//$'\r\n'} +echo "Building version: ($VERSION)" + +cd $SCRIPTDIR/../../../ + +go-msi make --msi `pwd`/duplicati-win-x64.msi --out `pwd`/build --version $VERSION --path Installer/Windows/MSI/wix.json --src Installer/Windows/MSI/templates \ No newline at end of file diff --git a/Installer/Windows/MSI/templates/LicenseAgreementDlg_HK.wxs b/Installer/Windows/MSI/templates/LicenseAgreementDlg_HK.wxs new file mode 100644 index 000000000..fefb829b1 --- /dev/null +++ b/Installer/Windows/MSI/templates/LicenseAgreementDlg_HK.wxs @@ -0,0 +1,36 @@ + + + + + + + + + + CostingComplete = 1 + "1"]]> + LicenseAccepted = "1" + + + 1 + + + + + {{if gt (.License | len) 0}} + + {{end}} + + + + 1 + + + + + + + + + diff --git a/Installer/Windows/MSI/templates/WixUI_HK.wxs b/Installer/Windows/MSI/templates/WixUI_HK.wxs new file mode 100644 index 000000000..cda1b3df9 --- /dev/null +++ b/Installer/Windows/MSI/templates/WixUI_HK.wxs @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + "1"]]> + + 1 + + NOT Installed + Installed AND PATCH + + 1 + LicenseAccepted = "1" + + 1 + 1 + NOT WIXUI_DONTVALIDATEPATH + "1"]]> + WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1" + + 1 + 1 + + Installed + + 1 + + 1 + 1 + 1 + + + + + diff --git a/Installer/Windows/MSI/templates/choco/LICENSE.txt b/Installer/Windows/MSI/templates/choco/LICENSE.txt new file mode 100644 index 000000000..c10af6d11 --- /dev/null +++ b/Installer/Windows/MSI/templates/choco/LICENSE.txt @@ -0,0 +1,9 @@ +From: {{.Choco.LicenseURL}} + +LICENSE + +{{if gt (.License | len) 0}} +{{.License | cat}} +{{else if gt (.Choco.LicenseURL | len) 0}} +{{.Choco.LicenseURL | download}} +{{end}} diff --git a/Installer/Windows/MSI/templates/choco/VERIFICATION.txt b/Installer/Windows/MSI/templates/choco/VERIFICATION.txt new file mode 100644 index 000000000..7c399c93f --- /dev/null +++ b/Installer/Windows/MSI/templates/choco/VERIFICATION.txt @@ -0,0 +1,10 @@ +VERIFICATION + +To check the checksum of this package, extract the msi file contained into it, +then run + + checksum.exe {{.Choco.MsiFile}} -t=sha256 + +The result must match + + {{.Choco.MsiSum | upper}} diff --git a/Installer/Windows/MSI/templates/choco/chocolateyInstall.ps1 b/Installer/Windows/MSI/templates/choco/chocolateyInstall.ps1 new file mode 100644 index 000000000..de61669cc --- /dev/null +++ b/Installer/Windows/MSI/templates/choco/chocolateyInstall.ps1 @@ -0,0 +1,7 @@ +$packageName = '{{.Choco.ID}}' +$fileType = 'msi' +$silentArgs = '/quiet'; +$scriptPath = $(Split-Path $MyInvocation.MyCommand.Path); +$fileFullPath = Join-Path $scriptPath '{{.Choco.MsiFile}}'; + +Install-ChocolateyInstallPackage $packageName $fileType $silentArgs $fileFullPath -checksum '{{.Choco.MsiSum}}' -checksumType = 'sha256' diff --git a/Installer/Windows/MSI/templates/choco/chocolateyUninstall.ps1 b/Installer/Windows/MSI/templates/choco/chocolateyUninstall.ps1 new file mode 100644 index 000000000..d4b26d054 --- /dev/null +++ b/Installer/Windows/MSI/templates/choco/chocolateyUninstall.ps1 @@ -0,0 +1,6 @@ +$packageName = "{{.Choco.ID}}"; +$fileType = 'msi'; +$scriptPath = $(Split-Path $MyInvocation.MyCommand.Path); +$fileFullPath = Join-Path $scriptPath '{{.Choco.MsiFile}}'; + +Uninstall-ChocolateyPackage $packageName $fileType "$fileFullPath /q" diff --git a/Installer/Windows/MSI/templates/choco/pkg.nuspec b/Installer/Windows/MSI/templates/choco/pkg.nuspec new file mode 100644 index 000000000..f1ed73102 --- /dev/null +++ b/Installer/Windows/MSI/templates/choco/pkg.nuspec @@ -0,0 +1,38 @@ + + + + {{.Choco.ID}} + {{.Choco.Title}} + {{.VersionOk}} + {{.Choco.Authors}} + {{.Choco.Owners}} + {{.Choco.Description}} + {{if gt (.Choco.ProjectURL | len) 0}} + {{.Choco.ProjectURL}} + {{end}} + {{if gt (.Choco.Tags | len) 0}} + {{.Choco.Tags}} + {{end}} + {{if gt (.Choco.LicenseURL | len) 0}} + {{.Choco.LicenseURL}} + {{end}} + {{if gt (.Choco.IconURL | len) 0}} + {{.Choco.IconURL}} + {{end}} + {{if gt (.Choco.ChangeLog | len) 0}} + {{.Choco.ChangeLog}} + {{end}} + {{if .Choco.RequireLicense}} + true + {{else}} + false + {{end}} + + + + + + + + + diff --git a/Installer/Windows/MSI/templates/product.wxs b/Installer/Windows/MSI/templates/product.wxs new file mode 100644 index 000000000..d035d42d0 --- /dev/null +++ b/Installer/Windows/MSI/templates/product.wxs @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + NOT NEWERVERSIONDETECTED + + + + + + {{if gt (.Files.Items | len) 0}} + + {{range $i, $e := .Files.Items}} + + {{end}} + + {{end}} + {{if gt (.Directories | len) 0}} + {{range $i, $e := .Directories}} + + {{end}} + {{end}} + + + + {{if gt (.Env.Vars | len) 0}} + + {{range $i, $e := .Env.Vars}} + + {{end}} + + {{end}} + + {{if gt (.Shortcuts.Items | len) 0}} + + + + {{range $i, $e := .Shortcuts.Items}} + + {{if gt ($e.Icon | len) 0}} + + {{end}} + + + {{end}} + + + + + {{end}} + + + + {{range $i, $e := .InstallHooks}} + + + {{end}} + {{range $i, $e := .UninstallHooks}} + + + {{end}} + + + {{range $i, $e := .InstallHooks}} + NOT Installed AND NOT REMOVE + {{end}} + {{range $i, $e := .UninstallHooks}} + REMOVE ~= "ALL" + {{end}} + + + + {{if gt (.Env.Vars | len) 0}} + + {{end}} + {{if gt (.Files.Items | len) 0}} + + {{end}} + {{if gt (.Shortcuts.Items | len) 0}} + + {{end}} + {{range $i, $e := .Directories}} + + {{end}} + + + + + + + + + + + + + + + + + + diff --git a/Installer/Windows/MSI/wix.json b/Installer/Windows/MSI/wix.json new file mode 100644 index 000000000..786fa3701 --- /dev/null +++ b/Installer/Windows/MSI/wix.json @@ -0,0 +1,33 @@ +{ + "product": "Duplicati", + "company": "duplicati", + "license": "LICENSE.txt", + "upgrade-code": "69163125-be0b-407e-b65d-80f7023524db", + "files": { + "guid": "95dd9d6c-89e7-4fc7-bfdc-708ffa99c68d", + "items": [ + ] + }, + "directories": [ + "bin" + ], + "shortcuts": { + "guid": "19455e18-67ec-4ace-9c70-0aa48be5b815", + "items": [ + { + "name": "Duplicati", + "description": "Launch Duplicati", + "target": "[INSTALLDIR]\\bin\\Duplicati.GUI.TrayIcon.exe", + "wdir": "INSTALLDIR", + "arguments": "", + "icon": "Assets/application icon/application_icon.ico" + } + ] + }, + "choco": { + "description": "Free backup software to store encrypted backups online", + "project-url": "https://www.duplicati.com/", + "tags": "backup", + "license-url": "https://github.com/duplicati/duplicati/blob/master/LICENSE.txt" + } +} \ No newline at end of file diff --git a/Installer/Windows/UpdateVersion/UpdateVersion.csproj b/Installer/Windows/UpdateVersion/UpdateVersion.csproj index fcf56089f..e63a14db6 100644 --- a/Installer/Windows/UpdateVersion/UpdateVersion.csproj +++ b/Installer/Windows/UpdateVersion/UpdateVersion.csproj @@ -1,34 +1,14 @@ - - - - Debug - AnyCPU - {179ADEC2-1856-47FE-954E-64BCD7213373} - Exe - UpdateVersion - UpdateVersion - v4.7 - - - true - full - false - bin\Debug - DEBUG - 4 - true - - - true - bin\Release - 4 - - - - - - - - - + + + + net8.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Installer/debian/bin-rules.sh b/Installer/debian/bin-rules.sh old mode 100644 new mode 100755 diff --git a/Installer/debian/build.sh b/Installer/debian/build.sh new file mode 100755 index 000000000..d4ce97805 --- /dev/null +++ b/Installer/debian/build.sh @@ -0,0 +1,13 @@ +#!/bin/bash +#This is a helper to make the release linux zip. Mainly for testing outside of ci/cd + +SCRIPTDIR=$( cd "$(dirname "$0")" ; pwd -P ) + +docker build $SCRIPTDIR/docker -t duplicati-debian + +VERSION=`grep '' < $SCRIPTDIR/../../Executables/net5/Duplicati.Server/Duplicati.Server.csproj | sed 's/.*\(.*\)<\/Version>.*/\1/'` +VERSION=${VERSION//$'\r\n'} +echo "Building version: ($VERSION)" + +export MSYS_NO_PATHCONV=1 +docker run --rm -eVERSION=$VERSION -v $SCRIPTDIR/../../:/sources duplicati-debian \ No newline at end of file diff --git a/Installer/debian/debian/docs b/Installer/debian/debian/docs deleted file mode 100644 index 18b71a363..000000000 --- a/Installer/debian/debian/docs +++ /dev/null @@ -1,2 +0,0 @@ -changelog.txt -releasenotes.txt diff --git a/Installer/debian/debian/rules b/Installer/debian/debian/rules deleted file mode 100755 index d2b5e2037..000000000 --- a/Installer/debian/debian/rules +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/make -f -# -*- makefile -*- -# Sample debian/rules that uses debhelper. -# -# This file was originally written by Joey Hess and Craig Small. -# As a special exception, when this file is copied by dh-make into a -# dh-make output file, you may use that output file without restriction. -# This special exception was added by Craig Small in version 0.37 of dh-make. -# -# Modified to make a template file for a multi-binary package with separated -# build-arch and build-indep targets by Bill Allombert 2001 - -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 - -# This has to be exported to make some magic below work. -export DH_OPTIONS -VERSION := $(shell cat ./version) - - -%: - dh $@ - -override_dh_clean: - dh_clean - xbuild /property:Configuration=Release /target:Clean Duplicati.sln - xbuild /property:Configuration=Release /target:Clean BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj - find -type d -name bin | xargs rm -rf - find -type d -name obj | xargs rm -rf - find -maxdepth 1 -type d -name build | xargs rm -rf - -override_dh_auto_build: - nuget restore Duplicati.sln - - xbuild /property:Configuration=Release BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj - mono BuildTools/UpdateVersionStamp/bin/Release/UpdateVersionStamp.exe --version=$(VERSION) - - xbuild /property:Configuration=Release Duplicati.sln - -override_dh_auto_install: - mkdir build - mkdir build/bin - mkdir build/lib - mkdir build/lib/duplicati - mkdir build/lib/duplicati/SQLite - mkdir build/share - mkdir build/share/applications - mkdir build/share/pixmaps - cp Installer/debian/duplicati-launcher.sh build/bin/duplicati - cp Installer/debian/duplicati-commandline-launcher.sh build/bin/duplicati-cli - cp Installer/debian/duplicati-server-launcher.sh build/bin/duplicati-server - cp Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/*.dll build/lib/duplicati/ - cp Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/*.exe build/lib/duplicati/ - cp Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/*.exe.config build/lib/duplicati/ - cp -r Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/SQLite/pinvoke build/lib/duplicati/SQLite/ - cp Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/default_compressed_extensions.txt build/lib/duplicati/ - cp Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/acknowledgements.txt build/lib/duplicati/ - cp Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/changelog.txt build/lib/duplicati/ - cp Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/run-script-example.sh build/lib/duplicati/ - cp Tools/Verification/DuplicatiVerify.py build/lib/duplicati/ - cp -r Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/ build/lib/duplicati/ - cp -r Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/licenses build/lib/duplicati/ - cp -r Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/lvm-scripts build/lib/duplicati/lvm-scripts - cp -r Duplicati/Server/webroot build/lib/duplicati/ - if [ -f "oem-app-name.txt" ]; then cp "oem-app-name.txt" build/lib/duplicati; fi - if [ -f "oem-update-url.txt" ]; then cp "oem-update-url.txt" build/lib/duplicati; fi - if [ -f "oem-update-key.txt" ]; then cp "oem-update-key.txt" build/lib/duplicati; fi - if [ -f "oem-update-readme.txt" ]; then cp "oem-update-readme.txt" build/lib/duplicati; fi - if [ -f "oem-update-installid.txt" ]; then cp "oem-update-installid.txt" build/lib/duplicati; fi - cp Installer/debian/duplicati.desktop build/share/applications - cp Installer/debian/duplicati.xpm build/share/pixmaps - cp Installer/debian/duplicati.png build/share/pixmaps - cp Installer/debian/duplicati.svg build/share/pixmaps - rm -rf build/lib/duplicati/win-tools - rm -rf build/lib/duplicati/SQLite/win64 - rm -rf build/lib/duplicati/SQLite/win32 - rm -rf build/lib/duplicati/MonoMac.dll - rm -rf build/lib/duplicati/OSX\ Icons - rm -rf build/lib/duplicati/OSXTrayHost - rm -rf build/lib/duplicati/licenses/MonoMac - rm -rf build/lib/duplicati/licenses/gpg - find build/lib/duplicati/* -type f | xargs chmod 644 - find build/lib/duplicati/* -type d | xargs chmod 755 - find build/lib/duplicati/* -type f -name \*.exe | xargs chmod 755 - find build/lib/duplicati/* -type f -name \*.sh | xargs chmod 755 - find build/lib/duplicati/* -type f -name \*.py | xargs chmod 755 - dh_install - -#override_dh_auto_install: -# rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/*.mdb -# mkdir ${DESTDIR}/usr/lib/duplicati/ -# cp -R Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/* ${DESTDIR}/usr/lib/duplicati/ -# cp -R Duplicati/Server/webroot ${DESTDIR}/usr/lib/duplicati/ - -override_dh_systemd_enable: - dh_systemd_enable --no-enable diff --git a/Installer/debian/docker-build-binary.sh b/Installer/debian/docker-build-binary.sh old mode 100644 new mode 100755 diff --git a/Installer/debian/docker/Dockerfile b/Installer/debian/docker/Dockerfile new file mode 100644 index 000000000..049814cd4 --- /dev/null +++ b/Installer/debian/docker/Dockerfile @@ -0,0 +1,24 @@ +FROM ubuntu:20.04 + +ENV DEBIAN_FRONTEND noninteractive + +# Install common build tools +RUN set -uex; \ + apt-get update; \ + apt-get install --no-install-suggests --no-install-recommends -y \ + build-essential \ + debhelper \ + dpkg-dev \ + ; \ + apt-get clean all +RUN apt-get update && apt-get install -y curl +RUN curl -sL -o ~/packages-microsoft-prod.deb https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb +RUN dpkg -i ~/packages-microsoft-prod.deb +RUN apt-get update && apt-get install -y dotnet-sdk-6.0 + +ADD debian /deb/debian +RUN chmod -x /deb/debian/duplicati.install +RUN chmod -x /deb/debian/docs + +ADD runner.sh / +CMD /runner.sh diff --git a/Installer/debian/debian/changelog b/Installer/debian/docker/debian/changelog similarity index 100% rename from Installer/debian/debian/changelog rename to Installer/debian/docker/debian/changelog diff --git a/Installer/debian/debian/compat b/Installer/debian/docker/debian/compat similarity index 100% rename from Installer/debian/debian/compat rename to Installer/debian/docker/debian/compat diff --git a/Installer/debian/debian/control b/Installer/debian/docker/debian/control similarity index 51% rename from Installer/debian/debian/control rename to Installer/debian/docker/debian/control index 8ea62664d..077ac30b6 100644 --- a/Installer/debian/debian/control +++ b/Installer/debian/docker/debian/control @@ -2,14 +2,14 @@ Source: duplicati Section: utils Priority: extra Maintainer: Kenneth Skovhede -Build-Depends: debhelper (>= 8.0.0) +Build-Depends: debhelper (>= 8.0.0), dotnet-sdk-6.0 Standards-Version: 3.9.4 Homepage: http://www.duplicati.com Vcs-Git: https://github.com/duplicati/duplicati.git Package: duplicati Architecture: all -Depends: mono-runtime (>= 5.10.0), ca-certificates-mono, libmono-2.0-1, libmono-system-core4.0-cil, libmono-system-configuration4.0-cil, libmono-system-configuration-install4.0-cil, libmono-system-data4.0-cil, libmono-system-drawing4.0-cil, libmono-system-net4.0-cil, libmono-system-net-http4.0-cil, libmono-system-net-http-webrequest4.0-cil, libmono-system-numerics4.0-cil, libmono-system-runtime-serialization4.0-cil, libmono-system-servicemodel4.0a-cil, libmono-system-servicemodel-discovery4.0-cil, libmono-system-serviceprocess4.0-cil, libmono-system-transactions4.0-cil, libmono-system-web4.0-cil, libmono-system-web-services4.0-cil, libmono-system-xml4.0-cil, libmono-microsoft-csharp4.0-cil, libsqlite3-0 (>= 3.6.12), libappindicator0.1-cil | libappindicator3-0.1-cil | libayatana-appindicator1, gtk-sharp2 +Depends: Description: Backup client for encrypted online backups Duplicati is a free backup client that securely stores encrypted, incremental, compressed backups on cloud storage services and remote file servers. It diff --git a/Installer/debian/debian/copyright b/Installer/debian/docker/debian/copyright similarity index 100% rename from Installer/debian/debian/copyright rename to Installer/debian/docker/debian/copyright diff --git a/Installer/debian/docker/debian/docs b/Installer/debian/docker/debian/docs new file mode 100644 index 000000000..68905cd33 --- /dev/null +++ b/Installer/debian/docker/debian/docs @@ -0,0 +1 @@ +changelog.txt diff --git a/Installer/debian/debian/duplicati.default b/Installer/debian/docker/debian/duplicati.default similarity index 100% rename from Installer/debian/debian/duplicati.default rename to Installer/debian/docker/debian/duplicati.default diff --git a/Installer/debian/debian/duplicati.install b/Installer/debian/docker/debian/duplicati.install similarity index 100% rename from Installer/debian/debian/duplicati.install rename to Installer/debian/docker/debian/duplicati.install diff --git a/Installer/debian/debian/duplicati.service b/Installer/debian/docker/debian/duplicati.service similarity index 100% rename from Installer/debian/debian/duplicati.service rename to Installer/debian/docker/debian/duplicati.service diff --git a/Installer/debian/docker/debian/make.sh b/Installer/debian/docker/debian/make.sh new file mode 100755 index 000000000..be0f97563 --- /dev/null +++ b/Installer/debian/docker/debian/make.sh @@ -0,0 +1,2 @@ +#!/bin/bash +make --debug=b -f $@ \ No newline at end of file diff --git a/Installer/debian/debian/patches/series b/Installer/debian/docker/debian/patches/series similarity index 100% rename from Installer/debian/debian/patches/series rename to Installer/debian/docker/debian/patches/series diff --git a/Installer/debian/docker/debian/rules b/Installer/debian/docker/debian/rules new file mode 100644 index 000000000..1995c5ed3 --- /dev/null +++ b/Installer/debian/docker/debian/rules @@ -0,0 +1,60 @@ +#!/usr/bin/make -f +# -*- makefile -*- +# Sample debian/rules that uses debhelper. +# +# This file was originally written by Joey Hess and Craig Small. +# As a special exception, when this file is copied by dh-make into a +# dh-make output file, you may use that output file without restriction. +# This special exception was added by Craig Small in version 0.37 of dh-make. +# +# Modified to make a template file for a multi-binary package with separated +# build-arch and build-indep targets by Bill Allombert 2001 + +# Uncomment this to turn on verbose mode. +export DH_VERBOSE=1 +VERBOSE := 1 +# This has to be exported to make some magic below work. +export DH_OPTIONS +export VERSION + +%: + dh $@ + +override_dh_clean: + dh_clean + #dotnet clean does stupid things with nuget, so just delete the folders + find /sources -type d -name bin | xargs rm -rf + find /sources -type d -name obj | xargs rm -rf + +override_dh_auto_build: + dotnet publish -c Release --runtime=linux-x64 -p:DefineConstants=ENABLE_GTK -o build/lib/duplicati/ /sources/Duplicati.sln + +override_dh_auto_install: + mkdir build/bin + mkdir build/share + mkdir build/share/applications + mkdir build/share/pixmaps + ln -sf /usr/lib/duplicati/Duplicati.GUI.TrayIcon build/bin/duplicati + ln -sf /usr/lib/duplicati/Duplicati.CommandLine build/bin/duplicati-cli + ln -sf /usr/lib/duplicati/Duplicati.Server build/bin/duplicati-server + cp /sources/Tools/Verification/DuplicatiVerify.py build/lib/duplicati/ + cp /sources/Installer/debian/duplicati.desktop build/share/applications + cp /sources/Installer/debian/duplicati.xpm build/share/pixmaps + cp /sources/Installer/debian/duplicati.png build/share/pixmaps + cp /sources/Installer/debian/duplicati.svg build/share/pixmaps + rm -rf build/lib/duplicati/win-tools + rm -rf build/lib/duplicati/OSX\ Icons + rm -rf build/lib/duplicati/OSXTrayHost + rm -rf build/lib/duplicati/licenses/MonoMac + rm -rf build/lib/duplicati/licenses/gpg + find build/lib/duplicati/* -type f | xargs chmod 644 + find build/lib/duplicati/* -type d | xargs chmod 755 + find build/lib/duplicati/* -type f -name \*.sh | xargs chmod 755 + find build/lib/duplicati/* -type f -name \*.py | xargs chmod 755 + chmod 755 build/lib/duplicati/Duplicati.GUI.TrayIcon + chmod 755 build/lib/duplicati/Duplicati.CommandLine + chmod 755 build/lib/duplicati/Duplicati.Server + dh_install + +override_dh_systemd_enable: + dh_systemd_enable --no-enable diff --git a/Installer/debian/docker/runner.sh b/Installer/debian/docker/runner.sh new file mode 100755 index 000000000..e817442bf --- /dev/null +++ b/Installer/debian/docker/runner.sh @@ -0,0 +1,12 @@ +cd /deb + +DATE_STAMP=$(LANG=C date -R) +sed -e "s;%VERSION%;$VERSION;g" -e "s;%DATE%;$DATE_STAMP;g" -i "debian/changelog" + +cat debian/changelog + +cp /sources/changelog.txt ./ + +dpkg-buildpackage -b --no-sign + +cp /*.deb /sources/ \ No newline at end of file diff --git a/Installer/debian/duplicati-commandline-launcher.sh b/Installer/debian/duplicati-commandline-launcher.sh deleted file mode 100755 index f3597e869..000000000 --- a/Installer/debian/duplicati-commandline-launcher.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -INSTALLDIR=/usr/lib/duplicati -export LD_LIBRARY_PATH="${INSTALLDIR}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" -export MONO_PATH=$MONO_PATH:${INSTALLDIR} - -EXE_FILE=${INSTALLDIR}/Duplicati.CommandLine.exe -APP_NAME=Duplicati.CommandLine - -exec -a "$APP_NAME" mono "$EXE_FILE" "$@" diff --git a/Installer/debian/duplicati-launcher.sh b/Installer/debian/duplicati-launcher.sh deleted file mode 100755 index 785a0c82e..000000000 --- a/Installer/debian/duplicati-launcher.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -INSTALLDIR=/usr/lib/duplicati -export LD_LIBRARY_PATH="${INSTALLDIR}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" -export MONO_PATH=$MONO_PATH:${INSTALLDIR} - -EXE_FILE=${INSTALLDIR}/Duplicati.GUI.TrayIcon.exe -APP_NAME=Duplicati - -exec -a "$APP_NAME" mono "$EXE_FILE" "$@" diff --git a/Installer/debian/duplicati-server-launcher.sh b/Installer/debian/duplicati-server-launcher.sh deleted file mode 100644 index 1a7297062..000000000 --- a/Installer/debian/duplicati-server-launcher.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -INSTALLDIR=/usr/lib/duplicati -export LD_LIBRARY_PATH="${INSTALLDIR}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" -export MONO_PATH=$MONO_PATH:${INSTALLDIR} - -EXE_FILE=${INSTALLDIR}/Duplicati.Server.exe -APP_NAME=DuplicatiServer - -exec -a "$APP_NAME" mono "$EXE_FILE" "$@" diff --git a/Installer/debian/init-script-example.sh b/Installer/debian/init-script-example.sh old mode 100644 new mode 100755 diff --git a/Installer/debian/make-binary-package.sh b/Installer/debian/make-binary-package.sh old mode 100644 new mode 100755 diff --git a/Installer/fedora/build-package.sh b/Installer/fedora/build-package.sh old mode 100644 new mode 100755 diff --git a/Installer/fedora/build.sh b/Installer/fedora/build.sh new file mode 100755 index 000000000..5a2b1549a --- /dev/null +++ b/Installer/fedora/build.sh @@ -0,0 +1,13 @@ +#!/bin/bash +#This is a helper to make the release linux zip. Mainly for testing outside of ci/cd + +SCRIPTDIR=$( cd "$(dirname "$0")" ; pwd -P ) + +docker build $SCRIPTDIR/docker -t duplicati-fedora + +VERSION=`grep '' < $SCRIPTDIR/../../Executables/net5/Duplicati.Server/Duplicati.Server.csproj | sed 's/.*\(.*\)<\/Version>.*/\1/'` +VERSION=${VERSION//$'\r\n'} +echo "Building version: ($VERSION)" + +export MSYS_NO_PATHCONV=1 +docker run --rm -eVERSION=$VERSION -v $SCRIPTDIR/../../:/sources duplicati-fedora \ No newline at end of file diff --git a/Installer/fedora/docker-build-binary.sh b/Installer/fedora/docker-build-binary.sh old mode 100644 new mode 100755 diff --git a/Installer/fedora/docker-build-package.sh b/Installer/fedora/docker-build-package.sh old mode 100644 new mode 100755 diff --git a/Installer/fedora/docker/Dockerfile b/Installer/fedora/docker/Dockerfile new file mode 100644 index 000000000..1c256066d --- /dev/null +++ b/Installer/fedora/docker/Dockerfile @@ -0,0 +1,17 @@ +FROM fedora:36 + +# Install common build tools +RUN dnf -y install deltarpm +RUN dnf -y upgrade + +#No Longer needed +#RUN rpm --import https://packages.microsoft.com/keys/microsoft.asc +#RUN curl --output /etc/yum.repos.d/microsoft-prod.repo https://packages.microsoft.com/config/fedora/33/prod.repo + +RUN dnf check-update +RUN dnf -y --allowerasing install @"Minimal Install" @buildsys-build yum-utils rpm-sign gnupg rpmdevtools desktop-file-utils dos2unix dotnet-sdk-6.0 + +ADD buildroot /buildroot + +ADD runner.sh / +CMD /runner.sh diff --git a/Installer/fedora/docker/buildroot/duplicati.spec b/Installer/fedora/docker/buildroot/duplicati.spec new file mode 100644 index 000000000..06653174c --- /dev/null +++ b/Installer/fedora/docker/buildroot/duplicati.spec @@ -0,0 +1,155 @@ +# TODO: +# - check where Tools/* scripts should really be +# - try to fix every mono compiler warning +# - fix rpmlint warnings + +# Set up some defaults +%global namer duplicati +%global debug_package %{nil} +%global alphatag .git + +# Then load overrides +%include %{_topdir}/SOURCES/%{namer}-buildinfo.spec + +Name: %{namer} +Version: %{_buildversion} +Release: %{_gittag}%{?alphatag}%{?dist} +Icon: duplicati.xpm +#BuildArch: noarch +#Should work, but does not allow building noarch +#ExclusiveArch: % {mono_arches} + +# Disable auto dependencies as it picks up .Net 2.0 profile +# and does not support supplying them with 4.5 +# Also, all thirdparty libraries are given as "provides" but they +# are not installed for use externally +AutoReqProv: no + +Summary: Backup client for encrypted online backups +License: MIT +URL: http://www.duplicati.com +#Source0: http://duplicati.googlecode.com/files/Duplicati%20% {_buildversion}.tgz +Source0: duplicati-%{_buildversion}.tar.bz2 + +# based on libdrm's make-git-snapshot.sh +# sh duplicati-make-git-snapshot.sh <_builddate> +Source1: %{namer}-build-package.sh +Source2: %{namer}-make-git-snapshot.sh +Source3: %{namer}-buildinfo.spec + +BuildRequires: desktop-file-utils +BuildRequires: dos2unix +BuildRequires: systemd + +Requires: desktop-file-utils +Requires: bash +Requires: libappindicator + +Provides: duplicati +Provides: duplicati-cli +Provides: duplicati-server + +%description +Duplicati is a free backup client that securely stores encrypted, +incremental, compressed backups on cloud storage services and remote file +servers. It supports targets like Amazon S3, Windows Live SkyDrive, +Rackspace Cloud Files or WebDAV, SSH, FTP (and many more). + +Duplicati has built-in AES-256 encryption and backups be can signed using +GNU Privacy Guard. A built-in scheduler makes sure that backups are always +up-to-date. Last but not least, Duplicati provides various options and +tweaks like filters, deletion rules, transfer and bandwidth options to run +backups for specific purposes. + +%prep +%setup -q + +find -type f -name "*dll" -or -name "*DLL" -or -name "*exe" + +%build + +dotnet publish -c Release --runtime=linux-x64 -p:DefineConstants=ENABLE_GTK -o publish Duplicati.sln + +%install + +# removing non-platform thirdparty binaries: +rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/win-tools +rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/SQLite/win64 +rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/SQLite/win32 +rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/MonoMac.dll +rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/OSX\ Icons +rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/OSXTrayHost + +rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/licenses/MonoMac +rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/licenses/gpg + +# Mono binaries are installed in /usr/lib, not /usr/lib64, even on x86_64: +# https://fedoraproject.org/wiki/Packaging:Mono + +install -d %{buildroot}%{_datadir}/pixmaps/ +install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/ +install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/SVGIcons/ +install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/SVGIcons/dark/ +install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/SVGIcons/light/ +install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/licenses/ +install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/webroot/ +install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/lvm-scripts/ + +install -d %{buildroot}%{_unitdir}/ +install -d %{buildroot}%{_sysconfdir}/sysconfig/ +install -d %{buildroot}%{_bindir}/ + +ln -sf /usr/lib/%{namer}/Duplicati.GUI.TrayIcon %{buildroot}%{_bindir}/%{namer} +ln -sf /usr/lib/%{namer}/Duplicati.CommandLine %{buildroot}%{_bindir}/%{namer}-cli +ln -sf /usr/lib/%{namer}/Duplicati.Server %{buildroot}%{_bindir}/%{namer}-server + +# Install oem overrides +if [ -f "oem-app-name.txt" ]; then install -p -m 644 "oem-app-name.txt" %{buildroot}%{_exec_prefix}/lib/%{namer}/; fi +if [ -f "oem-update-url.txt" ]; then install -p -m 644 "oem-update-url.txt" %{buildroot}%{_exec_prefix}/lib/%{namer}/; fi +if [ -f "oem-update-key.txt" ]; then install -p -m 644 "oem-update-key.txt" %{buildroot}%{_exec_prefix}/lib/%{namer}/; fi +if [ -f "oem-update-readme.txt" ]; then install -p -m 644 "oem-update-readme.txt" %{buildroot}%{_exec_prefix}/lib/%{namer}/; fi +if [ -f "oem-update-installid.txt" ]; then install -p -m 644 "oem-update-installid.txt" %{buildroot}%{_exec_prefix}/lib/%{namer}/; fi + +/bin/bash Installer/fedora/%{namer}-install-recursive.sh "publish/" "%{buildroot}%{_exec_prefix}/lib/%{namer}/" + +install -p Installer/debian/%{namer}.png %{buildroot}%{_datadir}/pixmaps/ + +chmod 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/Duplicati.GUI.TrayIcon +chmod 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/Duplicati.CommandLine +chmod 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/Duplicati.Server +find "%{buildroot}%{_exec_prefix}/lib/%{namer}"/* -type f -name \*.sh | xargs chmod 755 + +desktop-file-install Installer/debian/%{namer}.desktop + +mv Duplicati/Library/Snapshots/lvm-scripts/remove-lvm-snapshot.sh Tools/ +mv Duplicati/Library/Snapshots/lvm-scripts/create-lvm-snapshot.sh Tools/ +mv Duplicati/Library/Snapshots/lvm-scripts/find-volume.sh Tools/ +mv Duplicati/Library/Modules/Builtin/run-script-example.sh Tools/ + +# Install the service: +install -p -D -m 755 Installer/fedora/%{namer}.service %{buildroot}%{_unitdir}/ +install -p -D -m 644 Installer/fedora/%{namer}.default %{buildroot}%{_sysconfdir}/sysconfig/ + + +%post +%systemd_post %{namer}.service + +%preun +%systemd_preun %{namer}.service + +%postun +%systemd_postun %{namer}.service + +%files +%doc changelog.txt Duplicati/license.txt Tools +%{_bindir}/* +%{_datadir}/*/* +%{_exec_prefix}/lib/* +%{_sysconfdir}/sysconfig/* + + +%changelog +* Fri Jan 01 2021 Kenneth Skovhede 2.0 +- Packaged release +- See changelog.txt for changes + diff --git a/Installer/fedora/duplicati.xpm b/Installer/fedora/docker/buildroot/duplicati.xpm similarity index 100% rename from Installer/fedora/duplicati.xpm rename to Installer/fedora/docker/buildroot/duplicati.xpm diff --git a/Installer/fedora/docker/runner.sh b/Installer/fedora/docker/runner.sh new file mode 100755 index 000000000..4c48f6d5d --- /dev/null +++ b/Installer/fedora/docker/runner.sh @@ -0,0 +1,28 @@ +mkdir -p ~/rpmbuild/SOURCES/ + + + +cp /buildroot/duplicati.xpm ~/rpmbuild/SOURCES/ +# cp make-binary-package.sh ~/rpmbuild/SOURCES/duplicati-make-binary-package.sh +# cp duplicati-install-recursive.sh ~/rpmbuild/SOURCES/duplicati-install-recursive.sh +# cp duplicati.service ~/rpmbuild/SOURCES/duplicati.service +# cp duplicati.default ~/rpmbuild/SOURCES/duplicati.default + + +BUILDDATE=$(LANG=C date -R) +GITTAG="1" + +echo Creating Tar... +(cd /sources/ && tar --exclude="./.git" --transform "s,^./,duplicati-${VERSION}/," -cjf ~/rpmbuild/SOURCES/duplicati-${VERSION}.tar.bz2 .) +tar -tf ~/rpmbuild/SOURCES/duplicati-${VERSION}.tar.bz2 | head +echo Done... + +echo "%global _builddate ${BUILDDATE}" >> ~/rpmbuild/SOURCES/duplicati-buildinfo.spec +echo "%global _buildversion ${VERSION}" >> ~/rpmbuild/SOURCES/duplicati-buildinfo.spec +echo "%global _gittag ${GITTAG}" >> ~/rpmbuild/SOURCES/duplicati-buildinfo.spec + +cd /buildroot +dos2unix duplicati.spec +rpmbuild -bb duplicati.spec + +cp /root/rpmbuild/RPMS/*/*.rpm /sources/ \ No newline at end of file diff --git a/Installer/fedora/duplicati-binary.spec b/Installer/fedora/duplicati-binary.spec index 5dd2da99d..e5ef8e957 100644 --- a/Installer/fedora/duplicati-binary.spec +++ b/Installer/fedora/duplicati-binary.spec @@ -11,9 +11,6 @@ # Then load overrides %include %{_topdir}/SOURCES/%{namer}-buildinfo.spec -# The mangler detects executable bits, but `ls` says the files are not executable... -%global __brp_mangle_shebangs_exclude_from ^(.*/(webroot|licenses)/.*)|(.*\\.(config|bat|txt|ps1|desktop))|(.*/utility-scripts/DuplicatiVerify.py)$ - # Make sure it does not break because we have som arch-dependant libraries bundled %define _binaries_in_noarch_packages_terminate_build 0 @@ -41,34 +38,11 @@ Source4: %{namer}.default BuildRequires: desktop-file-utils BuildRequires: dos2unix BuildRequires: systemd +BuildRequires: dotnet Requires: desktop-file-utils Requires: bash -Requires: sqlite >= 3.6.12 -Requires: mono(appindicator-sharp) Requires: libappindicator -Requires: mono-core >= 3.0 -Requires: mono-data-sqlite -Requires: mono(System) -Requires: mono(System.Configuration) -Requires: mono(System.Configuration.Install) -Requires: mono(System.Core) -Requires: mono(System.Data) -Requires: mono(System.Drawing) -Requires: mono(System.Net) -Requires: mono(System.Net.Http) -Requires: mono(System.Net.Http.WebRequest) -Requires: mono(System.Runtime.Serialization) -Requires: mono(System.ServiceModel) -Requires: mono(System.ServiceModel.Discovery) -Requires: mono(System.ServiceProcess) -Requires: mono(System.Transactions) -Requires: mono(System.Web) -Requires: mono(System.Web.Services) -Requires: mono(System.Xml) -Requires: mono(System.Xml.Linq) -Requires: mono(Mono.Security) -Requires: mono(Mono.Posix) Provides: duplicati Provides: duplicati-cli @@ -108,17 +82,14 @@ rm -rf libstorj_uplink.dylib %install -# Mono binaries are installed in /usr/lib, not /usr/lib64, even on x86_64: -# https://fedoraproject.org/wiki/Packaging:Mono - -install -d -m 755 %{buildroot}%{_datadir}/pixmaps/ -install -d -m 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/ -install -d -m 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/SVGIcons/ -install -d -m 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/SVGIcons/dark/ -install -d -m 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/SVGIcons/light/ -install -d -m 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/licenses/ -install -d -m 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/webroot/ -install -d -m 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/lvm-scripts/ +install -d %{buildroot}%{_datadir}/pixmaps/ +install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/ +install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/SVGIcons/ +install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/SVGIcons/dark/ +install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/SVGIcons/light/ +install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/licenses/ +install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/webroot/ +install -d %{buildroot}%{_exec_prefix}/lib/%{namer}/lvm-scripts/ /bin/bash %{_topdir}/SOURCES/%{namer}-install-recursive.sh "." "%{buildroot}%{_exec_prefix}/lib/%{namer}/" @@ -136,10 +107,9 @@ install -p -D -m 755 %{namer}-server-launcher.sh %{buildroot}%{_bindir}/%{namer} install -p %{namer}.png %{buildroot}%{_datadir}/pixmaps/ # And fix permissions -find "%{buildroot}%{_exec_prefix}/lib/%{namer}/" -type f | xargs chmod 644 -find "%{buildroot}%{_exec_prefix}/lib/%{namer}/" -type f -name \*.exe | xargs chmod 755 -find "%{buildroot}%{_exec_prefix}/lib/%{namer}/" -type f -name \*.sh | xargs chmod 755 -find "%{buildroot}%{_exec_prefix}/lib/%{namer}/" -type f -name \*.py | xargs chmod 755 +find "%{buildroot}%{_exec_prefix}/lib/%{namer}"/* -type f -name \*.exe | xargs chmod 755 +find "%{buildroot}%{_exec_prefix}/lib/%{namer}"/* -type f -name \*.sh | xargs chmod 755 +#find "%{buildroot}%{_exec_prefix}/lib/%{namer}"/* -type f -name \*.py | xargs chmod 755 desktop-file-install %{namer}.desktop @@ -174,10 +144,6 @@ install -p -D -m 644 %{_topdir}/SOURCES/%{namer}.default %{_sysconfdir}/sysconfi %changelog -* Thu Jan 21 2021 Kenneth Skovhede - 2.0.0-0.20210121.git -- Updated patch files -- Fixed minor build issues - * Wed Jun 21 2017 Kenneth Skovhede - 2.0.0-0.20170621.git - Added the service file to the install diff --git a/Installer/fedora/duplicati-install-recursive.sh b/Installer/fedora/duplicati-install-recursive.sh old mode 100644 new mode 100755 diff --git a/Installer/fedora/duplicati-make-git-snapshot.sh b/Installer/fedora/duplicati-make-git-snapshot.sh old mode 100644 new mode 100755 diff --git a/Installer/fedora/duplicati.service b/Installer/fedora/duplicati.service index dd4cf3ab7..de8d0e3cb 100644 --- a/Installer/fedora/duplicati.service +++ b/Installer/fedora/duplicati.service @@ -7,6 +7,7 @@ Nice=19 IOSchedulingClass=idle EnvironmentFile=-/etc/sysconfig/duplicati ExecStart=/usr/bin/duplicati-server $DAEMON_OPTS +Restart=on-failure [Install] WantedBy=multi-user.target diff --git a/Installer/fedora/duplicati.spec b/Installer/fedora/duplicati.spec deleted file mode 100755 index b4a1e1abe..000000000 --- a/Installer/fedora/duplicati.spec +++ /dev/null @@ -1,264 +0,0 @@ -# TODO: -# - check where Tools/* scripts should really be -# - try to fix every mono compiler warning -# - fix rpmlint warnings - -# Set up some defaults -%global namer duplicati -%global debug_package %{nil} -%global alphatag .git - -# Then load overrides -%include %{_topdir}/SOURCES/%{namer}-buildinfo.spec - -# The mangler detects executable bits, but `ls` says the files are not executable... -%global __brp_mangle_shebangs_exclude_from ^(.*/(webroot|licenses)/.*)|(.*\\.(config|bat|txt|ps1|desktop))$ - -Name: %{namer} -Version: %{_buildversion} -Release: %{_gittag}%{?alphatag}%{?dist} -Icon: duplicati.xpm -BuildArch: noarch -#Should work, but does not allow building noarch -#ExclusiveArch: % {mono_arches} - -# Disable auto dependencies as it picks up .Net 2.0 profile -# and does not support supplying them with 4.5 -# Also, all thirdparty libraries are given as "provides" but they -# are not installed for use externally -AutoReqProv: no - -Summary: Backup client for encrypted online backups -License: MIT -URL: http://www.duplicati.com -#Source0: http://duplicati.googlecode.com/files/Duplicati%20% {_buildversion}.tgz -Source0: duplicati-%{_builddate}.tar.bz2 - -# based on libdrm's make-git-snapshot.sh -# sh duplicati-make-git-snapshot.sh <_builddate> -Source1: %{namer}-build-package.sh -Source2: %{namer}-make-git-snapshot.sh -Source3: %{namer}-buildinfo.spec - -Patch1: %{namer}-0001-remove-unittest.patch - -BuildRequires: mono-devel gnome-sharp-devel -BuildRequires: desktop-file-utils -BuildRequires: dos2unix -BuildRequires: systemd - -Requires: desktop-file-utils -Requires: bash -Requires: sqlite >= 3.6.12 -Requires: mono(appindicator-sharp) -Requires: libappindicator -Requires: mono-core >= 5.10.0 -Requires: mono-data-sqlite -Requires: mono(System) -Requires: mono(System.Configuration) -Requires: mono(System.Configuration.Install) -Requires: mono(System.Core) -Requires: mono(System.Data) -Requires: mono(System.Drawing) -Requires: mono(System.Net) -Requires: mono(System.Net.Http) -Requires: mono(System.Net.Http.WebRequest) -Requires: mono(System.Runtime.Serialization) -Requires: mono(System.ServiceModel) -Requires: mono(System.ServiceModel.Discovery) -Requires: mono(System.ServiceProcess) -Requires: mono(System.Transactions) -Requires: mono(System.Web) -Requires: mono(System.Web.Services) -Requires: mono(System.Xml) -Requires: mono(System.Xml.Linq) -Requires: mono(Mono.Security) -Requires: mono(Mono.Posix) - -Provides: duplicati -Provides: duplicati-cli -Provides: duplicati-server - -%description -Duplicati is a free backup client that securely stores encrypted, -incremental, compressed backups on cloud storage services and remote file -servers. It supports targets like Amazon S3, Windows Live SkyDrive, -Rackspace Cloud Files or WebDAV, SSH, FTP (and many more). - -Duplicati has built-in AES-256 encryption and backups be can signed using -GNU Privacy Guard. A built-in scheduler makes sure that backups are always -up-to-date. Last but not least, Duplicati provides various options and -tweaks like filters, deletion rules, transfer and bandwidth options to run -backups for specific purposes. - -%prep -%setup -q -n %{namer}-%{_builddate} -dos2unix Duplicati.sln -dos2unix Tools/Verification/DuplicatiVerify.py -%patch1 -p1 - -# removing own duplicati binaries: -rm -f Duplicati/Localization/Duplicati.Library.Utility.dll - -# platform settings: -ln -sf /usr/lib/mono/gtk-sharp-2.0/gtk-sharp.dll \ - ./Duplicati/GUI/Duplicati.GUI.TrayIcon/BuildSupport/gtk-sharp.dll - -ln -sf /usr/lib/mono/gtk-sharp-2.0/glib-sharp.dll \ - ./Duplicati/GUI/Duplicati.GUI.TrayIcon/BuildSupport/glib-sharp.dll - -ln -sf /usr/lib/mono/gtk-sharp-2.0/gdk-sharp.dll \ - ./Duplicati/GUI/Duplicati.GUI.TrayIcon/BuildSupport/gdk-sharp.dll - -ln -sf /usr/lib/mono/gtk-sharp-2.0/atk-sharp.dll \ - ./Duplicati/GUI/Duplicati.GUI.TrayIcon/BuildSupport/atk-sharp.dll - -# rm thirdparty/appindicator-sharp/appindicator-sharp.dll - -find -type f -name "*dll" -or -name "*DLL" -or -name "*exe" - -%build - -nuget restore Duplicati.sln - -msbuild /property:Configuration=Release BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj -mono BuildTools/UpdateVersionStamp/bin/Release/UpdateVersionStamp.exe --version=%{_buildversion} - -msbuild /property:Configuration=Release Duplicati.sln - -# msbuild BuildTools/LocalizationTool/LocalizationTool.sln - -# update l10n - -#./make.sh - - - -%install - -# remove the app-indicator file, it is supposed to be on the system, if it is supported -# rm Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/appindicator-sharp.dll - -# removing non-platform thirdparty binaries: -rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/win-tools -rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/SQLite/win64 -rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/SQLite/win32 -rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/MonoMac.dll -rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/OSX\ Icons -rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/OSXTrayHost - -rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/licenses/MonoMac -rm -rf Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/licenses/gpg - -# Mono binaries are installed in /usr/lib, not /usr/lib64, even on x86_64: -# https://fedoraproject.org/wiki/Packaging:Mono - -install -d -m 755 %{buildroot}%{_datadir}/pixmaps/ -install -d -m 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/ -install -d -m 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/SVGIcons/ -install -d -m 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/SVGIcons/dark/ -install -d -m 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/SVGIcons/light/ -install -d -m 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/licenses/ -install -d -m 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/webroot/ -install -d -m 755 %{buildroot}%{_exec_prefix}/lib/%{namer}/lvm-scripts/ - -install -p -D -m 755 Installer/debian/duplicati-launcher.sh %{buildroot}%{_bindir}/%{namer} -install -p -D -m 755 Installer/debian/duplicati-commandline-launcher.sh %{buildroot}%{_bindir}/%{namer}-cli -install -p -D -m 755 Installer/debian/duplicati-server-launcher.sh %{buildroot}%{_bindir}/%{namer}-server -install -p -m 644 Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/*.dll %{buildroot}%{_exec_prefix}/lib/%{namer}/ -install -p -m 755 Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/*.exe %{buildroot}%{_exec_prefix}/lib/%{namer}/ -install -p -m 644 Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/*.exe.config %{buildroot}%{_exec_prefix}/lib/%{namer}/ -#install -p -m 644 Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/*.dll.config %{buildroot}%{_exec_prefix}/lib/%{namer}/ -install -p -m 644 Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/default_compressed_extensions.txt %{buildroot}%{_exec_prefix}/lib/%{namer}/ -install -p -m 644 Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/acknowledgements.txt %{buildroot}%{_exec_prefix}/lib/%{namer}/ -install -p -m 644 Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/changelog.txt %{buildroot}%{_exec_prefix}/lib/%{namer}/ -install -p Installer/debian/%{namer}.png %{buildroot}%{_datadir}/pixmaps/ -install -p -m 644 Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/dark/* %{buildroot}%{_exec_prefix}/lib/%{namer}/SVGIcons/dark/ -install -p -m 644 Duplicati/GUI/Duplicati.GUI.TrayIcon/SVGIcons/light/* %{buildroot}%{_exec_prefix}/lib/%{namer}/SVGIcons/light/ -install -p -m 755 Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/lvm-scripts/*.sh %{buildroot}%{_exec_prefix}/lib/%{namer}/lvm-scripts/ - - -# Install oem overrides -if [ -f "oem-app-name.txt" ]; then install -p -m 644 "oem-app-name.txt" %{buildroot}%{_exec_prefix}/lib/%{namer}/; fi -if [ -f "oem-update-url.txt" ]; then install -p -m 644 "oem-update-url.txt" %{buildroot}%{_exec_prefix}/lib/%{namer}/; fi -if [ -f "oem-update-key.txt" ]; then install -p -m 644 "oem-update-key.txt" %{buildroot}%{_exec_prefix}/lib/%{namer}/; fi -if [ -f "oem-update-readme.txt" ]; then install -p -m 644 "oem-update-readme.txt" %{buildroot}%{_exec_prefix}/lib/%{namer}/; fi -if [ -f "oem-update-installid.txt" ]; then install -p -m 644 "oem-update-installid.txt" %{buildroot}%{_exec_prefix}/lib/%{namer}/; fi - -/bin/bash Installer/fedora/%{namer}-install-recursive.sh "Duplicati/Server/webroot/" "%{buildroot}%{_exec_prefix}/lib/%{namer}/webroot/" - -/bin/bash Installer/fedora/%{namer}-install-recursive.sh "Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/licenses/" "%{buildroot}%{_exec_prefix}/lib/%{namer}/licenses/" - -desktop-file-install Installer/debian/%{namer}.desktop - -mv Tools/Verification/DuplicatiVerify.py Tools/ -rm -rf Tools/Verification/ -mv Duplicati/Library/Snapshots/lvm-scripts/remove-lvm-snapshot.sh Tools/ -mv Duplicati/Library/Snapshots/lvm-scripts/create-lvm-snapshot.sh Tools/ -mv Duplicati/Library/Snapshots/lvm-scripts/find-volume.sh Tools/ -mv Duplicati/Library/Modules/Builtin/run-script-example.sh Tools/ - -# Install the service: -install -p -D -m 755 Installer/fedora/%{namer}.service %{_unitdir} -install -p -D -m 644 Installer/fedora/%{namer}.default %{_sysconfdir}/sysconfig/ - - -%post -/bin/touch --no-create %{_datadir}/icons/hicolor || : -%{_bindir}/gtk-update-icon-cache \ - --quiet %{_datadir}/icons/hicolor 2> /dev/null|| : -%systemd_post %{namer}.service - -%preun -%systemd_preun %{namer}.service - -%postun -/bin/touch --no-create %{_datadir}/icons/hicolor || : -%{_bindir}/gtk-update-icon-cache \ - --quiet %{_datadir}/icons/hicolor 2> /dev/null|| : -%systemd_postun_with_restart %{namer}.service - -%posttrans -/usr/bin/gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || : - - -%files -%doc changelog.txt Duplicati/license.txt Tools -%{_bindir}/* -%{_datadir}/*/* -%{_exec_prefix}/lib/* - - -%changelog -* Thu Jan 21 2021 Kenneth Skovhede - 2.0.0-0.20210121.git -- Updated patch files -- Fixed minor build issues -- Changed to use msbuild instead of xbuild - -* Wed Jun 21 2017 Kenneth Skovhede - 2.0.0-0.20170621.git -- Added the service file to the install - -* Fri Jan 13 2017 Kenneth Skovhede - 2.0.0-0.20170113.git -- Fixed NuGet restore - -* Sat Apr 23 2016 Kenneth Skovhede - 2.0.0-0.20160423.git -- Updated list of dependencies - -* Thu Mar 27 2014 Kenneth Skovhede - 2.0.0-0.20140326.git -- Moved to /usr/lib -- Fixed minor build issues - -* Wed Mar 26 2014 Kenneth Skovhede - 2.0.0-0.20140326.git -- Updated patch files -- Fixed minor build issues - -* Wed May 29 2013 Ismael Olea - 2.0.0-0.20130529.git -- removed MacOSX support and deps -- first compiler building spec - -* Mon May 13 2013 Ismael Olea - 1.3.4-1 -- removing desktop contents - -* Sun May 12 2013 Ismael Olea - 1.3.4-0 -- first dirty package for upstream compiled binary - diff --git a/Installer/fedora/make-binary-package.sh b/Installer/fedora/make-binary-package.sh old mode 100644 new mode 100755 diff --git a/Tools/SQLiteTool/App.config b/Tools/SQLiteTool/App.config deleted file mode 100644 index bfb669556..000000000 --- a/Tools/SQLiteTool/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Tools/SQLiteTool/Main.cs b/Tools/SQLiteTool/Main.cs index d06437fff..592de0710 100644 --- a/Tools/SQLiteTool/Main.cs +++ b/Tools/SQLiteTool/Main.cs @@ -23,9 +23,9 @@ using System.IO; namespace SQLiteTool { - class MainClass + class Program { - public static void Main(string[] args) + static void Main(string[] args) { if (args.Length != 2) { diff --git a/Tools/SQLiteTool/SQLiteTool.csproj b/Tools/SQLiteTool/SQLiteTool.csproj index 2a0ca7522..24a4a820d 100644 --- a/Tools/SQLiteTool/SQLiteTool.csproj +++ b/Tools/SQLiteTool/SQLiteTool.csproj @@ -1,9 +1,19 @@  + net8.0 Exe + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/Tools/ZipFileDebugger/Program.cs b/Tools/ZipFileDebugger/Program.cs index 4c9b3e191..4303f2c94 100644 --- a/Tools/ZipFileDebugger/Program.cs +++ b/Tools/ZipFileDebugger/Program.cs @@ -22,29 +22,27 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Duplicati.Library.Interface; namespace ZipFileDebugger { - class MainClass + class Program { - public static void Main(string[] _args) + static void Main(string[] args) { - var args = new List(_args); - var opts = Duplicati.Library.Utility.CommandLineParser.ExtractOptions(args, null); + var argslist = new List(args); + var opts = Duplicati.Library.Utility.CommandLineParser.ExtractOptions(argslist, null); - if (args == null || args.Count == 0) + if (argslist == null || argslist.Count == 0) { Console.WriteLine("Usage:"); Console.WriteLine("{0} ", System.Reflection.Assembly.GetEntryAssembly().Location); return; } - Duplicati.Library.Logging.Log.LogLevel = Duplicati.Library.Logging.LogMessageType.Profiling; - Duplicati.Library.Logging.Log.CurrentLog = new Duplicati.Library.Logging.StreamLog(Console.OpenStandardOutput()); - var filecount = 0; var errorcount = 0; - foreach (var file in args) + foreach (var file in argslist) { if (!File.Exists(file)) { @@ -65,33 +63,38 @@ namespace ZipFileDebugger try { filecount++; - using (var zr = new Duplicati.Library.Compression.FileArchiveZip(file, opts)) + using (var filestream = System.IO.File.Open(file, FileMode.Open, FileAccess.Read, + FileShare.None)) { - var files = zr.ListFilesWithSize(null).ToArray(); - Console.WriteLine("Found {0} files in archive, testing read-ability for each", files.Length); - - for (var i = 0; i < files.Length; i++) + using (var zr = new Duplicati.Library.Compression.FileArchiveZip(filestream, ArchiveMode.Read, opts)) { - Console.Write("Opening file #{0} - {1}", i + 1, files[i].Key); - try + var files = zr.ListFilesWithSize(null).ToArray(); + Console.WriteLine("Found {0} files in archive, testing read-ability for each", + files.Length); + + for (var i = 0; i < files.Length; i++) { - using (var ms = new MemoryStream()) - using (var sr = zr.OpenRead(files[i].Key)) + Console.Write("Opening file #{0} - {1}", i + 1, files[i].Key); + try { - sr.CopyTo(ms); + using (var ms = new MemoryStream()) + using (var sr = zr.OpenRead(files[i].Key)) + { + sr.CopyTo(ms); - if (ms.Length == files[i].Value) - Console.WriteLine(" -- success"); - else - Console.WriteLine(" -- bad length: {0} vs {1}", ms.Length, files[i].Value); + if (ms.Length == files[i].Value) + Console.WriteLine(" -- success"); + else + Console.WriteLine(" -- bad length: {0} vs {1}", ms.Length, files[i].Value); + } + } + catch (Exception ex) + { + errors = true; + Console.WriteLine(" -- failed: {0}", ex); } - } - catch (Exception ex) - { - errors = true; - Console.WriteLine(" -- failed: {0}", ex); - } + } } } diff --git a/Tools/ZipFileDebugger/ZipFileDebugger.csproj b/Tools/ZipFileDebugger/ZipFileDebugger.csproj index edc5c2af4..197a80682 100644 --- a/Tools/ZipFileDebugger/ZipFileDebugger.csproj +++ b/Tools/ZipFileDebugger/ZipFileDebugger.csproj @@ -1,12 +1,21 @@  + net8.0 Exe - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/build-release.sh b/build-release.sh old mode 100644 new mode 100755 diff --git a/guiTests/guiTest.py b/guiTests/guiTest.py index 0e9fb93fb..552f3996a 100644 --- a/guiTests/guiTest.py +++ b/guiTests/guiTest.py @@ -24,43 +24,13 @@ parser.add_argument( parser.set_defaults(headless=True) cmdopt = parser.parse_args() -if "TRAVIS_BUILD_NUMBER" in os.environ: - from selenium.webdriver.firefox.options import Options - if "SAUCE_USERNAME" not in os.environ: - print("No sauce labs login credentials found. Stopping tests...") - sys.exit(0) - - capabilities = {'browserName': "firefox"} - capabilities['platform'] = "Windows 7" - capabilities['version'] = "48.0" - capabilities['screenResolution'] = "1280x1024" - capabilities["build"] = os.environ["TRAVIS_BUILD_NUMBER"] - capabilities["tunnel-identifier"] = os.environ["TRAVIS_JOB_NUMBER"] - - # connect to sauce labs - username = os.environ["SAUCE_USERNAME"] - access_key = os.environ["SAUCE_ACCESS_KEY"] - hub_url = "%s:%s@localhost:4445" % (username, access_key) - driver = webdriver.Remote(command_executor="http://%s/wd/hub" % hub_url, desired_capabilities=capabilities) -elif cmdopt.use_chrome: - print("using LOCAL Chrome webdriver") - from selenium.webdriver.chrome.options import Options - import chromedriver_autoinstaller - chromedriver_autoinstaller.install() - chr_opt = Options() - opt = ["--ignore-certificate-errors", "--window-size=1280,800" ] - if cmdopt.headless: opt += ["--headless"] - for o in opt: chr_opt.add_argument(o) - driver = webdriver.Chrome(options=chr_opt) -else: - from selenium.webdriver.firefox.options import Options - # local - print("Using LOCAL Firefox webdriver") - options = Options() - options.set_preference("intl.accept_languages", "en") - options.headless = cmdopt.headless - driver = webdriver.Firefox(options=options) - +# local +print("Using LOCAL webdriver") +profile = webdriver.FirefoxProfile() +profile.set_preference("intl.accept_languages", "en") +options = Options() +options.headless = True +driver = webdriver.Firefox(profile, options=options) def write_random_file(size, filename): if not os.path.exists(os.path.dirname(filename)): @@ -120,7 +90,7 @@ driver.maximize_window() driver.get("http://localhost:8200/ngax/index.html") if "Duplicati" not in driver.title: - raise Exception("Unable to load duplicati GUI!") + raise Exception("Unable to load duplicati GUI! Got: " + driver.title) # Create and hash random files in the source folder write_random_file(1024 * 1024, SOURCE_FOLDER + os.sep + "1MB.test") diff --git a/nuget.config b/nuget.config index 397b203da..1b587ea8a 100644 --- a/nuget.config +++ b/nuget.config @@ -3,4 +3,10 @@ + + + + \ No newline at end of file diff --git a/pipeline/.local_config.sh b/pipeline/.local_config.sh deleted file mode 100644 index 35475b945..000000000 --- a/pipeline/.local_config.sh +++ /dev/null @@ -1,10 +0,0 @@ - -export ROOT_DIR=${SCRIPT_DIR}/../ -# caches -export BUILD_DIR=/tmp/.duplicati_build -export TEST_DIR=/tmp/.duplicati_test -export ARCHIVE_DIR=${ROOT_DIR}/.duplicati_zip -export PACKAGES_DIR=${ROOT_DIR}/.duplicati_packages -export SIGN_DIR=${ROOT_DIR}/.duplicati_sign -export UPLOAD_DIR=${ROOT_DIR}/.duplicati_upload - diff --git a/pipeline/docker-run/docker-run b/pipeline/docker-run/docker-run deleted file mode 100755 index aa3625825..000000000 --- a/pipeline/docker-run/docker-run +++ /dev/null @@ -1,97 +0,0 @@ -#!/bin/bash -. "$( cd "$(dirname "$0")" ; pwd -P )/error_handling.sh" -. "$( cd "$(dirname "$0")" ; pwd -P )/markers.sh" - -function pull_docker_image () { - travis_mark_begin "PULL MINIMAL DOCKER IMAGE" - docker pull $DOCKER_IMAGE - travis_mark_end "PULL MINIMAL DOCKER IMAGE" -} - -function add_option () { - FORWARD_OPTS[${#FORWARD_OPTS[@]}]="$1" - FORWARD_OPTS[${#FORWARD_OPTS[@]}]="$2" -} - -function add_volumes () { - volume_args=() - - volume_args[${#volume_args[@]}]="-v /var/run/docker.sock:/var/run/docker.sock \ - -v ${TARGET_DIR}:/application \ - ${DOCKER_VOLUME} \ - -v $( cd "$(dirname "$0")" ; pwd -P )/../:/pipeline ${DOCKER_SHARED_MEM}" - - for (( i=1; i<${#SOURCE_DIR[@]}+1; i++ )); do - volume_args[${#volume_args[@]}]="-v ${SOURCE_DIR[$i-1]}:/source_$i" - done -} - -function run_with_docker () { - if [ -z ${TARGET_DIR} ]; then - echo "no target cache specified" - exit 1 - fi - - add_volumes - - docker run ${volume_args[@]} \ - --privileged $DOCKER_AS_ROOT --rm $DOCKER_IMAGE "/pipeline/docker-run/runner.sh" "${FORWARD_OPTS[@]}" -} - -function parse_options () { - FORWARD_OPTS=() - SOURCE_DIR=() - - while true ; do - case "$1" in - --image) - DOCKER_IMAGE="$2" - ;; - --sourcedir) - SOURCE_DIR[${#SOURCE_DIR[@]}]="$2" - ;; - --targetdir) - TARGET_DIR="$2" - ;; - --asroot) - DOCKER_AS_ROOT="-u 0" - ;; - --volume) - DOCKER_VOLUME="-v $2" - ;; - --sharedmem) - DOCKER_SHARED_MEM="-v /dev/shm:/dev/shm" - ;; - --*) - if [[ $2 =~ ^--.* || -z $2 ]]; then - FORWARD_OPTS[${#FORWARD_OPTS[@]}]="$1" - else - FORWARD_OPTS[${#FORWARD_OPTS[@]}]="$1" - FORWARD_OPTS[${#FORWARD_OPTS[@]}]="$2" - fi - ;; - esac - - if [[ -z $1 ]]; then - break - elif [[ $2 =~ ^--.* || -z $2 ]]; then - shift - else - shift - shift - fi - done - - - if [ "${TARGET_DIR##${SOURCE_DIR}}" != "${TARGET_DIR}" ]; then - echo "targetdir $TARGET_DIR is not allowed to be a subdir of sourcedir $SOURCE_DIR" - fi - - - add_option "--workingdir" "$TARGET_DIR" - add_option "--sourcedirnum" ${#SOURCE_DIR[@]} -} - -parse_options "$@" -pull_docker_image -run_with_docker \ No newline at end of file diff --git a/pipeline/docker-run/error_handling.sh b/pipeline/docker-run/error_handling.sh deleted file mode 100644 index baab6e68b..000000000 --- a/pipeline/docker-run/error_handling.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -function quit_on_error() { - local parent_lineno="$1" - local message="$2" - local code="${3:-1}" - if [[ -n "$message" ]] ; then - echo "Error in $0 line ${parent_lineno}: ${message}; exiting with status ${code}" - else - echo "Error in $0 line ${parent_lineno}; exiting with status ${code}" - fi - exit "${code}" -} - -set -eE -trap 'quit_on_error $LINENO' ERR -set -o pipefail diff --git a/pipeline/docker-run/markers.sh b/pipeline/docker-run/markers.sh deleted file mode 100644 index f27571163..000000000 --- a/pipeline/docker-run/markers.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -function travis_mark_begin () { - echo "travis_fold:start:$1" - echo "+ START $1" -} - -function travis_mark_end () { - echo "travis_fold:end:$1" - echo "+ DONE $1" -} diff --git a/pipeline/docker-run/runner.sh b/pipeline/docker-run/runner.sh deleted file mode 100755 index 4ed08661c..000000000 --- a/pipeline/docker-run/runner.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/bin/bash -. "$( cd "$(dirname "$0")" ; pwd -P )/error_handling.sh" -. "$( cd "$(dirname "$0")" ; pwd -P )/markers.sh" - -function sync_dirs () { - rsync_delete_option="--delete" - for (( i=1; i<$SOURCE_DIR_NUM+1; i++ )); do - travis_mark_begin "SYNCING DIR source_${i}" - rsync -a $rsync_delete_option "/source_${i}/" "/application/" - unset rsync_delete_option - travis_mark_end "SYNCING DIR source_${i}" - done -} - -function setup () { - travis_mark_begin "INSTALLING PACKAGES IN DOCKER" - - if [ -f /sbin/apk ]; then - apk --update add $DOCKER_PACKAGES - elif [ -f /usr/bin/apt-get ]; then - export DEBIAN_FRONTEND=noninteractive - apt-get update && apt-get install -y $DOCKER_PACKAGES - fi - travis_mark_end "INSTALLING PACKAGES IN DOCKER" -} - -function clean_up () { - if [[ -z $KEEP_TARGET_FILTER ]]; then - return - fi - - cd /application - find . -type f ! -regex "${KEEP_TARGET_FILTER}" -delete - find . -type d -empty -delete - - for (( i=1; i<${#KEEP_SOURCE_FILTER[@]}+1; i++ )); do - cd /source_${i} - find . -type f ! -regex "${KEEP_SOURCE_FILTER[$i-1]}" -delete - find . -type d -empty -delete - done -} - -function parse_options () { - KEEP_SOURCE_FILTER=() - - while true ; do - case "$1" in - --command) - DOCKER_COMMAND="$2" - ;; - --packages) - DOCKER_PACKAGES="$2" - ;; - --sourcedirnum) - SOURCE_DIR_NUM="$2" - ;; - --keeptargetfilter) - KEEP_TARGET_FILTER="$2" - ;; - --keepsourcefilter) - KEEP_SOURCE_FILTER[${#KEEP_SOURCE_FILTER[@]}]="$2" - ;; - --*) - if [[ $2 =~ ^--.* || -z $2 ]]; then - FORWARD_OPTS[${#FORWARD_OPTS[@]}]="$1" - else - FORWARD_OPTS[${#FORWARD_OPTS[@]}]="$1" - FORWARD_OPTS[${#FORWARD_OPTS[@]}]="$2" - fi - ;; - esac - - if [[ -z $1 ]]; then - break - elif [[ $2 =~ ^--.* || -z $2 ]]; then - shift - else - shift - shift - fi - done -} - -function set_user_script_vars () { - option_found=false - for arg in ${FORWARD_OPTS[@]}; do - if [[ $option_found == true ]]; then - eval $option=$arg - export $option - option_found=false - elif [[ $arg =~ ^--.* ]]; then - option_found=true - option=${arg#"--"} - elif [[ $arg =~ ^-.* ]]; then - flag=${arg#"-"} - eval $flag=true - export $flag - fi - done -} - -parse_options "$@" -setup -sync_dirs -cd /application -set_user_script_vars -$DOCKER_COMMAND "${FORWARD_OPTS[@]}" -clean_up \ No newline at end of file diff --git a/pipeline/jobs/_imports.sh b/pipeline/jobs/_imports.sh deleted file mode 100755 index 8d3a04fe8..000000000 --- a/pipeline/jobs/_imports.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -export PATH="$PATH:${ROOT_DIR}/pipeline/docker-run" -. error_handling.sh - -which ts > /dev/null -if [[ $? -ne 0 ]]; then - echo "please install ts. e.g. brew install moreutils/apt-get install moreutils" - exit 1 -fi \ No newline at end of file diff --git a/pipeline/jobs/build_job.sh b/pipeline/jobs/build_job.sh deleted file mode 100755 index 46717ce4f..000000000 --- a/pipeline/jobs/build_job.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -SCRIPT_DIR="$( cd "$(dirname "$0")" ; pwd -P )" -. "${SCRIPT_DIR}/_imports.sh" - -${ROOT_DIR}/pipeline/stage_build/trigger.sh \ ---sourcedir "${ROOT_DIR}" \ ---targetdir "${BUILD_DIR}" $@ | ts diff --git a/pipeline/jobs/selenium_job.sh b/pipeline/jobs/selenium_job.sh deleted file mode 100755 index 32aafd190..000000000 --- a/pipeline/jobs/selenium_job.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -SCRIPT_DIR="$( cd "$(dirname "$0")" ; pwd -P )" -. "${SCRIPT_DIR}/_imports.sh" - -${ROOT_DIR}/pipeline/stage_integrationtests/trigger.sh \ ---sourcedir "${BUILD_DIR}" \ ---targetdir "${TEST_DIR}" $@ | ts diff --git a/pipeline/jobs/unittest_job.sh b/pipeline/jobs/unittest_job.sh deleted file mode 100755 index 719732915..000000000 --- a/pipeline/jobs/unittest_job.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -SCRIPT_DIR="$( cd "$(dirname "$0")" ; pwd -P )" -. "${SCRIPT_DIR}/_imports.sh" - -${ROOT_DIR}/pipeline/stage_unittests/trigger.sh \ ---sourcedir $BUILD_DIR \ ---targetdir $TEST_DIR $@ | ts diff --git a/pipeline/linuxZip/build.sh b/pipeline/linuxZip/build.sh new file mode 100755 index 000000000..6dccde114 --- /dev/null +++ b/pipeline/linuxZip/build.sh @@ -0,0 +1,9 @@ +#!/bin/bash +#This is a helper to make the release linux zip. Mainly for testing outside of ci/cd + +SCRIPTDIR=$( cd "$(dirname "$0")" ; pwd -P ) + +docker build $SCRIPTDIR/docker -t duplicati-build + +export MSYS_NO_PATHCONV=1 +docker run --rm -v $SCRIPTDIR/../../:/sources duplicati-build \ No newline at end of file diff --git a/pipeline/linuxZip/docker/Dockerfile b/pipeline/linuxZip/docker/Dockerfile new file mode 100644 index 000000000..c1147db2c --- /dev/null +++ b/pipeline/linuxZip/docker/Dockerfile @@ -0,0 +1,11 @@ +FROM ubuntu + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y curl zip +RUN curl -sL -o ~/packages-microsoft-prod.deb https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb +RUN dpkg -i ~/packages-microsoft-prod.deb +RUN apt-get update && apt-get install -y dotnet-sdk-6.0 + +ADD runner.sh / +CMD /runner.sh \ No newline at end of file diff --git a/pipeline/linuxZip/docker/runner.sh b/pipeline/linuxZip/docker/runner.sh new file mode 100755 index 000000000..d4e601f46 --- /dev/null +++ b/pipeline/linuxZip/docker/runner.sh @@ -0,0 +1,5 @@ +mkdir /published/ +chmod +S /published/ +dotnet publish -c Release --runtime=linux-x64 -o /published/ /sources/Duplicati.sln + +zip /sources/linux-release.zip /published/ \ No newline at end of file diff --git a/pipeline/selenium/docker/Dockerfile b/pipeline/selenium/docker/Dockerfile new file mode 100644 index 000000000..8d9b72f8f --- /dev/null +++ b/pipeline/selenium/docker/Dockerfile @@ -0,0 +1,10 @@ +FROM selenium/standalone-firefox + +ENV DEBIAN_FRONTEND=noninteractive +RUN wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O ~/packages-microsoft-prod.deb +RUN sudo dpkg -i ~/packages-microsoft-prod.deb +RUN sudo apt-get update && sudo apt-get install -y python3-pip dotnet-sdk-6.0 +RUN pip3 install selenium + +ADD runner.sh / +CMD /runner.sh \ No newline at end of file diff --git a/pipeline/selenium/docker/runner.sh b/pipeline/selenium/docker/runner.sh new file mode 100755 index 000000000..bb6cd50ff --- /dev/null +++ b/pipeline/selenium/docker/runner.sh @@ -0,0 +1,9 @@ +mkdir /home/seluser/published/ +chmod +S /home/seluser/published/ +sudo dotnet publish -o /home/seluser/published/ /sources/Duplicati.sln + +cd +AUTOUPDATER_Duplicati_SKIP_UPDATE=1 "/home/seluser/published/Duplicati.Server" & +timeout 240 bash -c 'until printf "" 2>>/dev/null >>/dev/tcp/$0/$1; do sleep 1; echo Checking if server started...; done' 127.0.0.1 8200 +echo Running Tests... +python3 /sources/guiTests/guiTest.py \ No newline at end of file diff --git a/pipeline/selenium/test.sh b/pipeline/selenium/test.sh new file mode 100755 index 000000000..240f08ff5 --- /dev/null +++ b/pipeline/selenium/test.sh @@ -0,0 +1,6 @@ +SCRIPTDIR=$( cd "$(dirname "$0")" ; pwd -P ) + +docker build $SCRIPTDIR/docker -t duplicati-selenium + +export MSYS_NO_PATHCONV=1 +docker run --rm -v $SCRIPTDIR/../../:/sources duplicati-selenium \ No newline at end of file diff --git a/pipeline/shared/duplicati.sh b/pipeline/shared/duplicati.sh deleted file mode 100644 index 720a37af5..000000000 --- a/pipeline/shared/duplicati.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -. "$( cd "$(dirname "$0")" ; pwd -P )/../docker-run/error_handling.sh" - -export DUPLICATI_ROOT="/application/" -declare -a FORWARD_OPTS - -export UPDATE_SOURCE="${DUPLICATI_ROOT}/Updates/build/${BUILDTAG}_source" -export UPDATE_TARGET="${DUPLICATI_ROOT}/Updates/build/${BUILDTAG}_target" -export MONO_IMAGE="$(cat "${TRAVIS_BUILD_DIR}/Installer/Docker/mono_image.txt")" diff --git a/pipeline/stage_build/job.sh b/pipeline/stage_build/job.sh deleted file mode 100755 index b61f3dd3a..000000000 --- a/pipeline/stage_build/job.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -. /pipeline/docker-run/markers.sh -. /pipeline/shared/duplicati.sh - -function build () { - nuget restore "${DUPLICATI_ROOT}"/Duplicati.sln - - if [ ! -d "${DUPLICATI_ROOT}"/packages/SharpCompress.0.23.0 ]; then - ln -s "${DUPLICATI_ROOT}"/packages/sharpcompress.0.23.0 "${DUPLICATI_ROOT}"/packages/SharpCompress.0.23.0 - fi - - # build version stamper - msbuild -p:Configuration=Release -v:minimal "${DUPLICATI_ROOT}/BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj" - mono "${DUPLICATI_ROOT}/BuildTools/UpdateVersionStamp/bin/Release/UpdateVersionStamp.exe" --version="${releaseversion}" - - # build autoupdate - nuget restore "${DUPLICATI_ROOT}/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.sln" - msbuild -p:Configuration=Release -v:minimal "${DUPLICATI_ROOT}/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.sln" - - # build gpg signing tool - nuget restore "${DUPLICATI_ROOT}/BuildTools/GnupgSigningTool/GnupgSigningTool.sln" - msbuild -p:Configuration=Release -v:minimal "${DUPLICATI_ROOT}/BuildTools/GnupgSigningTool/GnupgSigningTool.sln" - - # build duplicati - msbuild -p:DefineConstants=ENABLE_GTK /p:Configuration=Release "${DUPLICATI_ROOT}/Duplicati.sln" - - cp -r "${DUPLICATI_ROOT}"/Duplicati/Server/webroot "${DUPLICATI_ROOT}"/Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/webroot -} - -travis_mark_begin "BUILDING BINARIES" -build -travis_mark_end "BUILDING BINARIES" diff --git a/pipeline/stage_build/trigger.sh b/pipeline/stage_build/trigger.sh deleted file mode 100755 index 71dc7593d..000000000 --- a/pipeline/stage_build/trigger.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -. error_handling.sh -. ./pipeline/shared/duplicati.sh - -PACKAGES="libgtk2.0-cil rsync" -docker-run --image "${MONO_IMAGE}" \ ---packages "$PACKAGES" \ ---command "/pipeline/stage_build/job.sh" $@ diff --git a/pipeline/stage_integrationtests/job.sh b/pipeline/stage_integrationtests/job.sh deleted file mode 100755 index 94ca797af..000000000 --- a/pipeline/stage_integrationtests/job.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -. /pipeline/docker-run/markers.sh -. /pipeline/shared/duplicati.sh - -function start_test () { - pip3 install selenium - pip3 install --upgrade urllib3 - - # wget "https://github.com/mozilla/geckodriver/releases/download/v0.23.0/geckodriver-v0.23.0-linux32.tar.gz" - # tar -xvzf geckodriver* - # chmod +x geckodriver - # export PATH=$PATH:/duplicati/ - - #echo -n | openssl s_client -connect scan.coverity.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | tee -a /etc/ssl/certs/ca- - mono "${DUPLICATI_ROOT}/Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/Duplicati.Server.exe" & - cd - python3 "${DUPLICATI_ROOT}/guiTests/guiTest.py" -} - -travis_mark_begin "INTEGRATION TESTING" -start_test -travis_mark_end "INTEGRATION TESTING" diff --git a/pipeline/stage_integrationtests/trigger.sh b/pipeline/stage_integrationtests/trigger.sh deleted file mode 100755 index 00414e27f..000000000 --- a/pipeline/stage_integrationtests/trigger.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -. error_handling.sh - -PACKAGES="python3-pip rsync mono-complete" -docker-run --image selenium/standalone-firefox \ ---packages "$PACKAGES" \ ---asroot \ ---sharedmem \ ---command "sudo -u seluser /pipeline/stage_integrationtests/job.sh" $@ diff --git a/pipeline/stage_unittests/job.sh b/pipeline/stage_unittests/job.sh deleted file mode 100755 index fece2e7b0..000000000 --- a/pipeline/stage_unittests/job.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -. /pipeline/docker-run/markers.sh -. /pipeline/shared/duplicati.sh - -function start_test () { - travis_mark_begin "SETUP NUGET" - nuget install NUnit.Runners -Version 3.10.0 -OutputDirectory testrunner - travis_mark_end "SETUP NUGET" - - for CAT in $(echo $testcategories | sed "s/,/ /g") - do - travis_mark_begin "UNIT TESTING CATEGORY $CAT" - mono "${DUPLICATI_ROOT}"/testrunner/NUnit.ConsoleRunner.3.10.0/tools/nunit3-console.exe \ - "${DUPLICATI_ROOT}"/Duplicati/UnitTest/bin/Release/Duplicati.UnitTest.dll --where:cat==$CAT --workers=1 - travis_mark_end "UNIT TESTING CATEGORY $CAT" - done -} - -start_test diff --git a/pipeline/stage_unittests/trigger.sh b/pipeline/stage_unittests/trigger.sh deleted file mode 100755 index 4d3e41444..000000000 --- a/pipeline/stage_unittests/trigger.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -. error_handling.sh -. ./pipeline/shared/duplicati.sh - -PACKAGES="wget unzip rsync" -docker-run --image "${MONO_IMAGE}" \ ---packages "$PACKAGES" \ ---command "/pipeline/stage_unittests/job.sh" $@ diff --git a/pipeline/start.sh b/pipeline/start.sh deleted file mode 100755 index a208fd6b3..000000000 --- a/pipeline/start.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -SCRIPT_DIR="$( cd "$(dirname "$0")" ; pwd -P )" -. "${SCRIPT_DIR}/docker-run/error_handling.sh" -. "${SCRIPT_DIR}/.local_config.sh" - -${ROOT_DIR}/pipeline/jobs/build_job.sh -${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories BulkNormal -${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories BulkNoSize -${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories SVNDataLong,SVNData,RecoveryTool,ProblematicPath,SymLink -${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Border -${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Filter,Targeted,Purge,Serialization,WebApi,Utility,UriUtility,IO,ImportExport,Disruption,RestoreHandler,RepairHandler,DeleteHandler,Controller diff --git a/tabs-to-spaces.sh b/tabs-to-spaces.sh old mode 100644 new mode 100755 diff --git a/thirdparty/DnsLite/DnsLite.cs b/thirdparty/DnsLite/DnsLite.cs deleted file mode 100644 index 93011164a..000000000 --- a/thirdparty/DnsLite/DnsLite.cs +++ /dev/null @@ -1,192 +0,0 @@ -//Dnslite.cs -//From: http://www.csharphelp.com/2005/12/dns-client-utility/ -/** - csc /target:library /out:DnsLib.dll /keyfile:..\..\Duplicati\GUI\Duplicati.snk DnsLite.cs -*/ -using System; -using System.Net; -using System.IO; -using System.Text; -using System.Net.Sockets; -using System.Collections; -namespace DnsLib -{ - public class MXRecord - { - public int preference = -1; - public string exchange = null; - public override string ToString() - { - return "Preference : " + preference + " Exchange : " + exchange; - } - } - public class DnsLite - { - private byte[] data; - private int position, id, length; - private string name; - private ArrayList dnsServers; - private static readonly int DNS_PORT = 53; - readonly Encoding ASCII = Encoding.ASCII; - public DnsLite() - { - id = DateTime.Now.Millisecond * 60; - dnsServers = new ArrayList(); - - } - public void setDnsServers(ArrayList dnsServers) - { - this.dnsServers = dnsServers; - } - public ArrayList getMXRecords(string host) - { - ArrayList mxRecords = null; - for (int i = 0; i < dnsServers.Count; i++) - { - try - { - mxRecords = getMXRecords(host, (string)dnsServers[i]); - break; - } - catch (IOException) - { - continue; - } - } - return mxRecords; - } - private int getNewId() - { - //return a new id - return ++id; - } - public ArrayList getMXRecords(string host, string serverAddress) - { - //opening the UDP socket at DNS server - //use UDPClient, if you are still with Beta1 - UdpClient dnsClient = new UdpClient(serverAddress, DNS_PORT); - //preparing the DNS query packet. - makeQuery(getNewId(), host); - //send the data packet - dnsClient.Send(data, data.Length); - - // 2017-01-09: Fix not sending null to UDPClient.Receive - IPEndPoint endpoint = new IPEndPoint(IPAddress.Loopback, DNS_PORT); - //receive the data packet from DNS server - data = dnsClient.Receive(ref endpoint); - length = data.Length; - //un pack the byte array & makes an array of MXRecord objects. - return makeResponse(); - } - //for packing the information to the format accepted by server - public void makeQuery(int id, String name) - { - data = new byte[512]; - for (int i = 0; i < 512; ++i) - { - data[i] = 0; - } - data[0] = (byte)(id >> 8); - data[1] = (byte)(id & 0xFF); - data[2] = 1; data[3] = 0; - data[4] = 0; data[5] = 1; - data[6] = 0; data[7] = 0; - data[8] = 0; data[9] = 0; - data[10] = 0; data[11] = 0; - string[] tokens = name.Split(new char[] { '.' }); - string label; - position = 12; - for (int j = 0; j < tokens.Length; j++) - { - label = tokens[j]; - data[position++] = (byte)(label.Length & 0xFF); - byte[] b = ASCII.GetBytes(label); - for (int k = 0; k < b.Length; k++) - { - data[position++] = b[k]; - } - } - data[position++] = 0; data[position++] = 0; - data[position++] = 15; data[position++] = 0; - data[position++] = 1; - } - //for un packing the byte array - public ArrayList makeResponse() - { - ArrayList mxRecords = new ArrayList(); - MXRecord mxRecord; - //NOTE: we are ignoring the unnecessary fields. - // and takes only the data required to build MX records. - int qCount = ((data[4] & 0xFF) << 8) | (data[5] & 0xFF); - if (qCount < 0) - { - throw new IOException("invalid question count"); - } - int aCount = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF); - if (aCount < 0) - { - throw new IOException("invalid answer count"); - } - position = 12; - for (int i = 0; i < qCount; ++i) - { - name = ""; - position = proc(position); - position += 4; - } - for (int i = 0; i < aCount; ++i) - { - name = ""; - position = proc(position); - position += 10; - int pref = (data[position++] << 8) | (data[position++] & 0xFF); - name = ""; - position = proc(position); - mxRecord = new MXRecord(); - mxRecord.preference = pref; - mxRecord.exchange = name; - mxRecords.Add(mxRecord); - } - return mxRecords; - } - private int proc(int position) - { - int len = (data[position++] & 0xFF); - if (len == 0) - { - return position; - } - int offset; - do - { - if ((len & 0xC0) == 0xC0) - { - if (position >= length) - { - return -1; - } - offset = ((len & 0x3F) << 8) | (data[position++] & 0xFF); - proc(offset); - return position; - } - else { - if ((position + len) > length) - { - return -1; - } - name += ASCII.GetString(data, position, len); - position += len; - } - if (position > length) - { - return -1; - } - len = data[position++] & 0xFF; - if (len != 0) { - name += "."; - } - }while (len != 0); - return position; - } - } -} \ No newline at end of file diff --git a/thirdparty/DnsLite/download.txt b/thirdparty/DnsLite/download.txt deleted file mode 100644 index a2794e3d6..000000000 --- a/thirdparty/DnsLite/download.txt +++ /dev/null @@ -1 +0,0 @@ -http://www.csharphelp.com/2005/12/dns-client-utility/ \ No newline at end of file diff --git a/thirdparty/DnsLite/licensedata.json b/thirdparty/DnsLite/licensedata.json deleted file mode 100644 index 1c3d768c7..000000000 --- a/thirdparty/DnsLite/licensedata.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "DNS Client Utility", - "description": "An Library to query DNS records", - "link": "http://www.csharphelp.com/2005/12/dns-client-utility/", - "license": "Unknown", - "notes": "" -} \ No newline at end of file diff --git a/thirdparty/FasterHashing/download.txt b/thirdparty/FasterHashing/download.txt deleted file mode 100644 index b9cd1a7a8..000000000 --- a/thirdparty/FasterHashing/download.txt +++ /dev/null @@ -1 +0,0 @@ -https://github.com/kenkendk/FasterHashing \ No newline at end of file diff --git a/thirdparty/FasterHashing/license.txt b/thirdparty/FasterHashing/license.txt deleted file mode 100644 index 27431b657..000000000 --- a/thirdparty/FasterHashing/license.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2017 Kenneth Skovhede - -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. \ No newline at end of file diff --git a/thirdparty/FasterHashing/licensedata.json b/thirdparty/FasterHashing/licensedata.json deleted file mode 100644 index 396c46ed6..000000000 --- a/thirdparty/FasterHashing/licensedata.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "FasterHashing", - "description": "An native hashing library wrapper", - "link": "https://github.com/kenkendk/FasterHashing", - "license": "MIT", - "notes": "" -} \ No newline at end of file diff --git a/thirdparty/FluentFTP/licensedata.json b/thirdparty/FluentFTP/licensedata.json index 94195465b..d14a4adda 100644 --- a/thirdparty/FluentFTP/licensedata.json +++ b/thirdparty/FluentFTP/licensedata.json @@ -1,6 +1,6 @@ { "name": "FluentFTP", - "description": "An FTP and FTPS client for .NET & .NET Standard, optimized for speed.", + "description": "A .Net library for connecting with the FTP protocol", "link": "https://github.com/robinrodricks/FluentFTP", "license": "MIT", "notes": "" diff --git a/thirdparty/SharpAESCrypt/SharpAESCrypt.exe b/thirdparty/SharpAESCrypt/SharpAESCrypt.exe deleted file mode 100644 index 4ee0a6403..000000000 Binary files a/thirdparty/SharpAESCrypt/SharpAESCrypt.exe and /dev/null differ diff --git a/thirdparty/alphafs/AlphaFS.dll b/thirdparty/alphafs/AlphaFS.dll new file mode 100644 index 000000000..6b94121b9 Binary files /dev/null and b/thirdparty/alphafs/AlphaFS.dll differ diff --git a/thirdparty/alphafs/Homepage.txt b/thirdparty/alphafs/Homepage.txt new file mode 100644 index 000000000..5f2402a99 --- /dev/null +++ b/thirdparty/alphafs/Homepage.txt @@ -0,0 +1 @@ +https://github.com/alphaleonis/AlphaFS \ No newline at end of file diff --git a/thirdparty/alphafs/License.txt b/thirdparty/alphafs/License.txt new file mode 100644 index 000000000..6a5afccb6 --- /dev/null +++ b/thirdparty/alphafs/License.txt @@ -0,0 +1,19 @@ +The MIT License (MIT) +===================== + +Copyright (c) 2008-2018 Peter Palotas, Jeffrey Jangli, Alexandr Normuradov + +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. \ No newline at end of file diff --git a/thirdparty/alphafs/build-info.txt b/thirdparty/alphafs/build-info.txt new file mode 100644 index 000000000..2d4317aba --- /dev/null +++ b/thirdparty/alphafs/build-info.txt @@ -0,0 +1 @@ +The build here is taken from 38970857fecc00501431197832fee257f30496f7, and only the netstandard2.0 library is used. \ No newline at end of file diff --git a/thirdparty/alphafs/licensedata.json b/thirdparty/alphafs/licensedata.json new file mode 100644 index 000000000..34c89fb60 --- /dev/null +++ b/thirdparty/alphafs/licensedata.json @@ -0,0 +1,7 @@ +{ + "name": "AlphaFS", + "description": "AlphaFS is a .NET library providing more complete Win32 file system functionality to the .NET platform than the standard System.IO classes.", + "link": "https://github.com/alphaleonis/AlphaFS", + "license": "MIT", + "notes": "" +} \ No newline at end of file diff --git a/thirdparty/alphavss/AlphaVSS.Common.dll b/thirdparty/alphavss/AlphaVSS.Common.dll new file mode 100644 index 000000000..99f64d52f Binary files /dev/null and b/thirdparty/alphavss/AlphaVSS.Common.dll differ diff --git a/thirdparty/alphavss/build-diff.patch b/thirdparty/alphavss/build-diff.patch new file mode 100644 index 000000000..d1fec89fd --- /dev/null +++ b/thirdparty/alphavss/build-diff.patch @@ -0,0 +1,87 @@ +diff --git a/src/AlphaVSS.Common/AlphaVSS.Common.csproj b/src/AlphaVSS.Common/AlphaVSS.Common.csproj +index 4ad0e8f..4039061 100644 +--- a/src/AlphaVSS.Common/AlphaVSS.Common.csproj ++++ b/src/AlphaVSS.Common/AlphaVSS.Common.csproj +@@ -1,7 +1,7 @@ +  + + +- net45;netcoreapp3.1 ++ netstandard2.0 + false + + +@@ -34,6 +34,7 @@ + all + runtime; build; native; contentfiles; analyzers; buildtransitive + ++ + + + +diff --git a/src/AlphaVSS.Common/Classes/VssFactoryProvider.cs b/src/AlphaVSS.Common/Classes/VssFactoryProvider.cs +index be2438a..8aae1f9 100644 +--- a/src/AlphaVSS.Common/Classes/VssFactoryProvider.cs ++++ b/src/AlphaVSS.Common/Classes/VssFactoryProvider.cs +@@ -79,7 +79,7 @@ namespace Alphaleonis.Win32.Vss + if (assemblyName == null) + throw new ArgumentNullException(nameof(assemblyName), $"{nameof(assemblyName)} is null."); + +-#if NETCOREAPP ++// #if NETCOREAPP || NETSTANDARD + Log($"Attempting to locate \"{assemblyName.FullName}\""); + // In .NET Core the native binaries are placed under a runtimes\{rid}\native folder, which is where the platform + // specific binaries are placed in AlphaVSS nuget packages. However the location after building is different if +@@ -99,7 +99,7 @@ namespace Alphaleonis.Win32.Vss + { + // On non-windows systems, the ':' separator is used, but since we only run on windows we don't need to + // take that into consideration. +- searchDirectories = nativeSearchDirectories.Split(';', StringSplitOptions.RemoveEmptyEntries); ++ searchDirectories = nativeSearchDirectories.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + Log($"Using native search directories: {String.Join(", ", searchDirectories.Select(s => $"\"{s}\""))}"); + } + +@@ -116,7 +116,7 @@ namespace Alphaleonis.Win32.Vss + try + { + Log($"Loading \"{fullAssemblyFileName}\"."); +- var result = AssemblyLoadContext.Default.LoadFromAssemblyPath(fullAssemblyFileName); ++ var result = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(fullAssemblyFileName); + Log($"Loaded \"{result.FullName}\" from \"{result.Location}\"."); + return result; + } +@@ -133,20 +133,20 @@ namespace Alphaleonis.Win32.Vss + } + + throw new FileNotFoundException($"Failed to find assembly \"{assemblyName.FullName}\""); +-#elif NETFRAMEWORK +- Log($"Attempting to load assembly \"{assemblyName.FullName}\""); +- try +- { +- var result = Assembly.Load(assemblyName); +- Log($"Loaded \"{result.FullName}\" from \"{result.Location}\"."); +- return result; +- } +- catch (Exception ex) +- { +- Log($"Error loading \"{assemblyName.FullName}\"; {ex}"); +- throw; +- } +-#endif ++// #elif NETFRAMEWORK ++// Log($"Attempting to load assembly \"{assemblyName.FullName}\""); ++// try ++// { ++// var result = Assembly.Load(assemblyName); ++// Log($"Loaded \"{result.FullName}\" from \"{result.Location}\"."); ++// return result; ++// } ++// catch (Exception ex) ++// { ++// Log($"Error loading \"{assemblyName.FullName}\"; {ex}"); ++// throw; ++// } ++// #endif + } + } + } diff --git a/thirdparty/alphavss/build-info.txt b/thirdparty/alphavss/build-info.txt new file mode 100644 index 000000000..b1049e90c --- /dev/null +++ b/thirdparty/alphavss/build-info.txt @@ -0,0 +1,5 @@ +The build here is taken from 13462e657f7993da5c80f835d219bbe82079ce75, and only the netstandard2.0 library is used. +The diff shows the changes to the source code. + +The native libraries are extracted from the NuGet package: +https://www.nuget.org/packages/AlphaVSS.Native.NetCore/2.0.0 diff --git a/thirdparty/alphavss/runtimes/win-x64/AlphaVSS.x64.dll b/thirdparty/alphavss/runtimes/win-x64/AlphaVSS.x64.dll new file mode 100644 index 000000000..a4874cff7 Binary files /dev/null and b/thirdparty/alphavss/runtimes/win-x64/AlphaVSS.x64.dll differ diff --git a/thirdparty/alphavss/runtimes/win-x64/Ijwhost.dll b/thirdparty/alphavss/runtimes/win-x64/Ijwhost.dll new file mode 100644 index 000000000..0909a28a4 Binary files /dev/null and b/thirdparty/alphavss/runtimes/win-x64/Ijwhost.dll differ diff --git a/thirdparty/alphavss/runtimes/win-x86/AlphaVSS.x86.dll b/thirdparty/alphavss/runtimes/win-x86/AlphaVSS.x86.dll new file mode 100644 index 000000000..f9261fad6 Binary files /dev/null and b/thirdparty/alphavss/runtimes/win-x86/AlphaVSS.x86.dll differ diff --git a/thirdparty/alphavss/runtimes/win-x86/Ijwhost.dll b/thirdparty/alphavss/runtimes/win-x86/Ijwhost.dll new file mode 100644 index 000000000..5332ff6d6 Binary files /dev/null and b/thirdparty/alphavss/runtimes/win-x86/Ijwhost.dll differ