diff --git a/ReleaseBuilder/.vscode/launch.json b/ReleaseBuilder/.vscode/launch.json index e90b71406..a5338a087 100644 --- a/ReleaseBuilder/.vscode/launch.json +++ b/ReleaseBuilder/.vscode/launch.json @@ -31,6 +31,9 @@ "--targets", "win-arm64-agent.msi", "--targets", "linux-x64-gui.zip", "--targets", "linux-arm64-gui.zip", + "--targets", "linux-x64-gui.appimage", + "--targets", "linux-arm64-gui.appimage", + "--targets", "linux-arm7-gui.appimage", "--targets", "osx-x64-gui.dmg", "--targets", "osx-arm64-gui.dmg", "--targets", "osx-x64-gui.pkg", diff --git a/ReleaseBuilder/Build/Command.CreatePackage.cs b/ReleaseBuilder/Build/Command.CreatePackage.cs index 32d92cd7c..d9a7c4db9 100644 --- a/ReleaseBuilder/Build/Command.CreatePackage.cs +++ b/ReleaseBuilder/Build/Command.CreatePackage.cs @@ -39,6 +39,40 @@ public static partial class Command /// The created package file path public record BuiltPackage(PackageTarget Target, string CreatedFile); + /// + /// Generates the metainfo content with the releases block injected + /// + /// The base directory + /// The path to the source metainfo file + /// The modified metainfo content + public static string GetMetainfoWithReleases(string baseDir, string metainfoPath) + { + var versionPath = Path.Combine(baseDir, "ReleaseBuilder", "build_version.txt"); + var changelogPath = Path.Combine(baseDir, "ReleaseBuilder", "changelog-news.txt"); + + var version = File.ReadAllText(versionPath).Trim(); + var changelog = File.ReadAllText(changelogPath).Trim(); + var metainfo = File.ReadAllText(metainfoPath); + + var sb = new StringBuilder(); + sb.AppendLine(" "); + sb.AppendLine($" "); + sb.AppendLine(" "); + sb.AppendLine($"

{System.Security.SecurityElement.Escape(changelog)}

"); + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine("
"); + + // Insert before + var insertIndex = metainfo.LastIndexOf(""); + if (insertIndex >= 0) + { + metainfo = metainfo.Insert(insertIndex, sb.ToString()); + } + + return metainfo; + } + /// /// Builds the packages for the specified build targets. /// @@ -140,6 +174,10 @@ public static partial class Command await BuildZipPackage(Path.Combine(buildRoot, target.BuildTargetString), $"duplicati-{rtcfg.ReleaseInfo.ReleaseName}-{target.BuildTargetString}", tempFile, target, rtcfg); break; + case PackageType.AppImage: + await BuildAppImagePackage(baseDir, buildRoot, tempFile, target, rtcfg); + break; + case PackageType.MSI: await BuildMsiPackage(baseDir, buildRoot, tempFile, target, rtcfg); break; @@ -253,6 +291,170 @@ public static partial class Command } } + /// + /// Builds an AppImage package asynchronously. + /// + /// The base directory. + /// The build root directory. + /// The AppImage file to generate. + /// The package target. + /// The runtime configuration. + /// A representing the asynchronous operation. + static async Task BuildAppImagePackage(string baseDir, string buildRoot, string appImageFile, PackageTarget target, RuntimeConfig rtcfg) + { + if (target.OS != OSType.Linux) + throw new Exception($"AppImage is only supported for Linux targets, got: {target.OS}"); + + if (target.Interface != InterfaceType.GUI) + throw new Exception($"AppImage is only supported for GUI interface, got: {target.Interface}"); + + var tmpRoot = Path.Combine(buildRoot, "tmp-appimage"); + if (Directory.Exists(tmpRoot)) + Directory.Delete(tmpRoot, true); + Directory.CreateDirectory(tmpRoot); + + var appDir = Path.Combine(tmpRoot, "AppDir"); + var appDirUsr = Path.Combine(appDir, "usr"); + var appDirLib = Path.Combine(appDirUsr, "lib", "duplicati"); + var appDirBin = Path.Combine(appDirUsr, "bin"); + Directory.CreateDirectory(appDirLib); + Directory.CreateDirectory(appDirBin); + + EnvHelper.CopyDirectory( + Path.Combine(buildRoot, target.BuildTargetString), + appDirLib, + recursive: true); + + await PackageSupport.InstallPackageIdentifier(appDirLib, target); + await PackageSupport.RenameExecutables(appDirLib); + await PackageSupport.SetPermissionFlags(appDirLib, rtcfg); + + var availableExecutables = ExecutableRenames.Values + .Where(x => File.Exists(Path.Combine(appDirLib, x))) + .ToList(); + + if (!availableExecutables.Any()) + throw new Exception("No executables found in AppImage payload"); + + if (OperatingSystem.IsWindows()) + throw new PlatformNotSupportedException("AppImage packaging is not supported on Windows, use WSL or Docker"); + + foreach (var executable in availableExecutables) + { + var linkPath = Path.Combine(appDirBin, executable); + await ProcessHelper.Execute([ + "ln", "-s", + Path.Combine("..", "lib", "duplicati", executable), + linkPath + ]); + } + + var sharedDir = Path.Combine(baseDir, "ReleaseBuilder", "Resources", "shared"); + var appImageResourcesDir = Path.Combine(baseDir, "ReleaseBuilder", "Resources", "appimage"); + + File.Copy( + Path.Combine(sharedDir, "desktop", "duplicati.desktop"), + Path.Combine(appDir, "com.duplicati.app.desktop"), + true + ); + + var applicationsDir = Path.Combine(appDirUsr, "share", "applications"); + Directory.CreateDirectory(applicationsDir); + File.Copy( + Path.Combine(sharedDir, "desktop", "duplicati.desktop"), + Path.Combine(applicationsDir, "com.duplicati.app.desktop"), + true + ); + + var metainfoDir = Path.Combine(appDirUsr, "share", "metainfo"); + Directory.CreateDirectory(metainfoDir); + + // Use the correct reverse-DNS filename for metadata and desktop file to satisfy appstreamcli + var metainfoSrc = Path.Combine(sharedDir, "metainfo", "com.duplicati.app.metainfo.xml"); + var metainfoDest = Path.Combine(metainfoDir, "com.duplicati.app.appdata.xml"); + var metainfoContent = GetMetainfoWithReleases(baseDir, metainfoSrc); + // Update launchable to match new desktop filename + metainfoContent = metainfoContent.Replace("duplicati.desktop", "com.duplicati.app.desktop"); + File.WriteAllText(metainfoDest, metainfoContent); + + if (OperatingSystem.IsWindows()) + throw new PlatformNotSupportedException("Creating unresolved symlinks is not supported on Windows, use WSL or Docker"); + + var iconSource = Path.Combine(sharedDir, "pixmaps", "duplicati.png"); + File.Copy(iconSource, Path.Combine(appDir, "duplicati.png"), true); + File.Copy(iconSource, Path.Combine(appDir, ".DirIcon"), true); + + var defaultExecutable = availableExecutables.Contains("duplicati") + ? "duplicati" + : availableExecutables.First(); + + var appRunTemplate = File.ReadAllText(Path.Combine(appImageResourcesDir, "AppRun.template")); + var appRunPayload = appRunTemplate + .Replace("%EXECUTABLE_LIST%", string.Join("\n", availableExecutables.Select(x => $" echo \" - {x}\""))) + .Replace("%INVOCATION_CASES%", string.Join("\n", availableExecutables.Select(x => $" {x}) exec \"${{HERE}}/usr/bin/{x}\" \"$@\" ;;"))) + .Replace("%FLAG_CASES%", string.Join("\n", availableExecutables.Select(x => + $" --{x})\n shift\n exec \"${{HERE}}/usr/bin/{x}\" \"$@\"\n ;;"))) + .Replace("%DEFAULT_EXECUTABLE%", defaultExecutable); + + File.WriteAllText(Path.Combine(appDir, "AppRun"), appRunPayload); + + if (!OperatingSystem.IsWindows()) + { + var filemode755 = UnixFileMode.UserRead + | UnixFileMode.UserWrite + | UnixFileMode.UserExecute + | UnixFileMode.GroupRead + | UnixFileMode.GroupExecute + | UnixFileMode.OtherRead + | UnixFileMode.OtherExecute; + + File.SetUnixFileMode(Path.Combine(appDir, "AppRun"), filemode755); + } + + File.Copy( + Path.Combine(appImageResourcesDir, "Dockerfile.build"), + Path.Combine(tmpRoot, "Dockerfile"), + true + ); + + await ProcessHelper.Execute([ + "docker", "build", + "-t", "duplicati/appimage-build:latest", + tmpRoot + ], workingDirectory: tmpRoot); + + var appImageArch = target.Arch switch + { + ArchType.x64 => "x86_64", + ArchType.Arm64 => "aarch64", + ArchType.Arm7 => "armhf", + _ => throw new Exception($"Unsupported AppImage architecture: {target.Arch}") + }; + + var appImageName = Path.GetFileName(appImageFile); + var appImageTempPath = Path.Combine(tmpRoot, appImageName); + if (File.Exists(appImageTempPath)) + File.Delete(appImageTempPath); + + await ProcessHelper.Execute([ + "docker", "run", + "--workdir", "/build", + "--volume", $"{tmpRoot}:/build:rw", + "-e", "APPIMAGE_EXTRACT_AND_RUN=1", + "-e", $"ARCH={appImageArch}", + "duplicati/appimage-build:latest", + "/usr/local/bin/appimagetool", + "/build/AppDir", + $"/build/{appImageName}" + ]); + + if (File.Exists(appImageFile)) + File.Delete(appImageFile); + + File.Move(appImageTempPath, appImageFile); + Directory.Delete(tmpRoot, true); + } + /// /// Builds an MSI package asynchronously. /// @@ -904,6 +1106,11 @@ public static partial class Command Path.Combine(pkgroot, "usr", "share", "applications", "duplicati.desktop") )); + var metainfoDest = Path.Combine(pkgroot, "usr", "share", "metainfo", "com.duplicati.app.metainfo.xml"); + var metainfoDir = Path.GetDirectoryName(metainfoDest)!; + if (!Directory.Exists(metainfoDir)) Directory.CreateDirectory(metainfoDir); + File.WriteAllText(metainfoDest, GetMetainfoWithReleases(baseDir, Path.Combine(sharedDir, "metainfo", "com.duplicati.app.metainfo.xml"))); + supportFiles.AddRange( new[] { "duplicati.png", "duplicati.svg", "duplicati.xpm" } .Select(f => ( @@ -1211,6 +1418,7 @@ public static partial class Command File.Copy(Path.Combine(sharedDir, "pixmaps", "duplicati.xpm"), Path.Combine(sources, "duplicati.xpm")); File.Copy(Path.Combine(sharedDir, "pixmaps", "duplicati.png"), Path.Combine(sources, "duplicati.png")); File.Copy(Path.Combine(sharedDir, "desktop", "duplicati.desktop"), Path.Combine(sources, "duplicati.desktop")); + File.WriteAllText(Path.Combine(sources, "com.duplicati.app.metainfo.xml"), CreatePackage.GetMetainfoWithReleases(baseDir, Path.Combine(sharedDir, "metainfo", "com.duplicati.app.metainfo.xml"))); File.Copy(Path.Combine(resourcesDir, "duplicati-install-recursive.sh"), Path.Combine(sources, "duplicati-install-recursive.sh")); if (target.Interface != InterfaceType.Agent) { diff --git a/ReleaseBuilder/Build/Command.cs b/ReleaseBuilder/Build/Command.cs index ce94a6100..ff4e720e1 100644 --- a/ReleaseBuilder/Build/Command.cs +++ b/ReleaseBuilder/Build/Command.cs @@ -83,6 +83,16 @@ public static partial class Command "Duplicati.WindowsService.csproj" }; + /// + /// The packages that require Docker to build + /// + private static readonly IReadOnlySet DockerDependingTargets = new HashSet { + PackageType.Docker, + PackageType.Deb, + PackageType.RPM, + PackageType.AppImage + }; + /// /// Some executables have shorter names that follow the Linux convention of all-lowercase /// @@ -596,7 +606,7 @@ public static partial class Command if (!rtcfg.UseDockerBuild) { - var unsupportedBuilds = buildTargets.Where(x => x.Package == PackageType.Docker || x.Package == PackageType.Deb || x.Package == PackageType.RPM).ToList(); + var unsupportedBuilds = buildTargets.Where(x => DockerDependingTargets.Contains(x.Package)).ToList(); if (unsupportedBuilds.Any()) throw new Exception($"The following packages cannot be built without Docker: {string.Join(", ", unsupportedBuilds.Select(x => x.PackageTargetString))}"); } diff --git a/ReleaseBuilder/PackageTarget.cs b/ReleaseBuilder/PackageTarget.cs index e4d053f97..fcb249952 100644 --- a/ReleaseBuilder/PackageTarget.cs +++ b/ReleaseBuilder/PackageTarget.cs @@ -74,6 +74,10 @@ public enum PackageType /// Zip, /// + /// The AppImage package + /// + AppImage, + /// /// The Windows installer format /// MSI, @@ -171,6 +175,7 @@ public record PackageTarget(OSType OS, ArchType Arch, InterfaceType Interface, P => package switch { PackageType.Zip => "zip", + PackageType.AppImage => "appimage", PackageType.MSI => "msi", PackageType.Deb => "deb", PackageType.RPM => "rpm", diff --git a/ReleaseBuilder/Program.cs b/ReleaseBuilder/Program.cs index fe23993f1..3a30155fe 100644 --- a/ReleaseBuilder/Program.cs +++ b/ReleaseBuilder/Program.cs @@ -1,122 +1,125 @@ -// Copyright (C) 2025, 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.CommandLine; - -namespace ReleaseBuilder; - -/// -/// Entry point for the executable -/// -class Program -{ - /// - /// The supported build packages - /// - public static readonly IReadOnlyList SupportedPackageTargets = new[] { - "win-x64-gui.zip", - "win-x64-gui.msi", - "win-x86-gui.zip", - "win-x86-gui.msi", - "win-arm64-gui.zip", - "win-arm64-gui.msi", - "win-x64-agent.msi", - "win-arm64-agent.msi", - "win-x64-agent.zip", - "win-arm64-agent.zip", - - "linux-x64-gui.zip", - "linux-x64-gui.deb", - "linux-x64-gui.rpm", - "linux-x64-cli.zip", - "linux-x64-cli.deb", - "linux-x64-cli.rpm", - "linux-x64-cli.docker", - "linux-x64-cli.spk", - "linux-x64-agent.zip", - "linux-x64-agent.deb", - "linux-x64-agent.rpm", - "linux-x64-agent.docker", - - "linux-arm7-gui.zip", - "linux-arm7-gui.deb", - "linux-arm7-cli.zip", - "linux-arm7-cli.deb", - "linux-arm7-cli.docker", - "linux-arm7-cli.spk", - "linux-arm7-agent.zip", - "linux-arm7-agent.deb", - - "linux-arm64-gui.zip", - "linux-arm64-gui.deb", - "linux-arm64-gui.rpm", - "linux-arm64-cli.zip", - "linux-arm64-cli.deb", - "linux-arm64-cli.rpm", - "linux-arm64-cli.docker", - "linux-arm64-cli.spk", - "linux-arm64-agent.zip", - "linux-arm64-agent.deb", - "linux-arm64-agent.rpm", - "linux-arm64-agent.docker", - - "osx-x64-gui.dmg", - "osx-x64-gui.pkg", - "osx-x64-agent.pkg", - "osx-x64-cli.pkg", - "osx-arm64-gui.dmg", - "osx-arm64-gui.pkg", - "osx-arm64-agent.pkg", - "osx-arm64-cli.pkg", - } - .Select(x => PackageTarget.ParsePackageId(x)) - .Distinct() - .ToList(); - - /// - /// The return code of the application; shared state - /// - public static int? ReturnCode = 0; - - /// - /// Invokes the builder - /// - /// - /// - static async Task Main(string[] args) - { - try - { - ReturnCode = await new RootCommand("Build tool for Duplicati") - { - Build.Command.Create(), - CreateKey.Command.Create(), - }.InvokeAsync(args); - } - catch (Exception ex) - { - Console.WriteLine(ex.ToString()); - ReturnCode = 1; - } - - return ReturnCode ?? 2; - } +// Copyright (C) 2025, 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.CommandLine; + +namespace ReleaseBuilder; + +/// +/// Entry point for the executable +/// +class Program +{ + /// + /// The supported build packages + /// + public static readonly IReadOnlyList SupportedPackageTargets = new[] { + "win-x64-gui.zip", + "win-x64-gui.msi", + "win-x86-gui.zip", + "win-x86-gui.msi", + "win-arm64-gui.zip", + "win-arm64-gui.msi", + "win-x64-agent.msi", + "win-arm64-agent.msi", + "win-x64-agent.zip", + "win-arm64-agent.zip", + + "linux-x64-gui.zip", + "linux-x64-gui.deb", + "linux-x64-gui.rpm", + "linux-x64-gui.appimage", + "linux-x64-cli.zip", + "linux-x64-cli.deb", + "linux-x64-cli.rpm", + "linux-x64-cli.docker", + "linux-x64-cli.spk", + "linux-x64-agent.zip", + "linux-x64-agent.deb", + "linux-x64-agent.rpm", + "linux-x64-agent.docker", + + "linux-arm7-gui.zip", + "linux-arm7-gui.deb", + "linux-arm7-gui.appimage", + "linux-arm7-cli.zip", + "linux-arm7-cli.deb", + "linux-arm7-cli.docker", + "linux-arm7-cli.spk", + "linux-arm7-agent.zip", + "linux-arm7-agent.deb", + + "linux-arm64-gui.zip", + "linux-arm64-gui.deb", + "linux-arm64-gui.rpm", + "linux-arm64-gui.appimage", + "linux-arm64-cli.zip", + "linux-arm64-cli.deb", + "linux-arm64-cli.rpm", + "linux-arm64-cli.docker", + "linux-arm64-cli.spk", + "linux-arm64-agent.zip", + "linux-arm64-agent.deb", + "linux-arm64-agent.rpm", + "linux-arm64-agent.docker", + + "osx-x64-gui.dmg", + "osx-x64-gui.pkg", + "osx-x64-agent.pkg", + "osx-x64-cli.pkg", + "osx-arm64-gui.dmg", + "osx-arm64-gui.pkg", + "osx-arm64-agent.pkg", + "osx-arm64-cli.pkg", + } + .Select(x => PackageTarget.ParsePackageId(x)) + .Distinct() + .ToList(); + + /// + /// The return code of the application; shared state + /// + public static int? ReturnCode = 0; + + /// + /// Invokes the builder + /// + /// + /// + static async Task Main(string[] args) + { + try + { + ReturnCode = await new RootCommand("Build tool for Duplicati") + { + Build.Command.Create(), + CreateKey.Command.Create(), + }.InvokeAsync(args); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + ReturnCode = 1; + } + + return ReturnCode ?? 2; + } } \ No newline at end of file diff --git a/ReleaseBuilder/Resources/appimage/AppRun.template b/ReleaseBuilder/Resources/appimage/AppRun.template new file mode 100644 index 000000000..717558259 --- /dev/null +++ b/ReleaseBuilder/Resources/appimage/AppRun.template @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +HERE="$(dirname "$(readlink -f "${0}")")" +export PATH="${HERE}/usr/bin/:${PATH}" + +show_help() { + echo "Usage: $(basename "${ARGV0:-$0}") [--] [args]" + echo + echo "Available executables:" +%EXECUTABLE_LIST% + echo + echo "You can invoke the AppImage via a symlink name or with --." +} + +if [[ "${1}" == "--help" || "${1}" == "-h" ]]; then + show_help + exit 0 +fi + +INVOCATION_NAME="$(basename "${ARGV0:-$0}")" +case "$INVOCATION_NAME" in +%INVOCATION_CASES% +esac + +case "$1" in +%FLAG_CASES% +esac + +exec "${HERE}/usr/bin/%DEFAULT_EXECUTABLE%" "$@" diff --git a/ReleaseBuilder/Resources/appimage/Dockerfile.build b/ReleaseBuilder/Resources/appimage/Dockerfile.build new file mode 100644 index 000000000..cdc15a3ce --- /dev/null +++ b/ReleaseBuilder/Resources/appimage/Dockerfile.build @@ -0,0 +1,28 @@ +# We remove the hardcoded platform here so it builds for the host's native arch by default +FROM ubuntu:latest + +# Use the automatic Buildx variable +ARG TARGETARCH + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + appstream \ + ca-certificates \ + curl \ + file \ + gnupg \ + libglib2.0-0 \ + squashfs-tools \ + && rm -rf /var/lib/apt/lists/* + +RUN case "${TARGETARCH}" in \ + "amd64") ARCH="x86_64" ;; \ + "arm64") ARCH="aarch64" ;; \ + *) echo "Unsupported architecture: ${TARGETARCH}"; exit 1 ;; \ + esac \ + && curl -L -o /usr/local/bin/appimagetool \ + "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${ARCH}.AppImage" \ + && chmod +x /usr/local/bin/appimagetool + +# Necessary for AppImages to run in many container environments +ENV APPIMAGE_EXTRACT_AND_RUN=1 \ No newline at end of file diff --git a/ReleaseBuilder/Resources/shared/desktop/duplicati.desktop b/ReleaseBuilder/Resources/shared/desktop/duplicati.desktop index ba427606f..667d4089f 100644 --- a/ReleaseBuilder/Resources/shared/desktop/duplicati.desktop +++ b/ReleaseBuilder/Resources/shared/desktop/duplicati.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Categories=System;Archiving;FileTools;Filesystem; +Categories=System;Archiving;FileTools; Type=Application Name=Duplicati GenericName= Backup tool diff --git a/ReleaseBuilder/Resources/shared/metainfo/com.duplicati.app.metainfo.xml b/ReleaseBuilder/Resources/shared/metainfo/com.duplicati.app.metainfo.xml new file mode 100644 index 000000000..743a7275a --- /dev/null +++ b/ReleaseBuilder/Resources/shared/metainfo/com.duplicati.app.metainfo.xml @@ -0,0 +1,40 @@ + + + com.duplicati.app + CC0-1.0 + MIT + Duplicati + Create and maintain local and remote backup copies of your data + + + Duplicati Team + + + + + +

+ Duplicati is a free backup client that securely stores encrypted, + incremental, compressed backups on cloud storage services and remote file servers. +

+

+ It works with Amazon S3, Windows Live SkyDrive, Google Drive, + OneDrive or WebDAV, SSH, FTP (and many more). +

+

+ Duplicati has built-in AES-256 encryption and backups can be signed using GNU Privacy Guard. + A built-in scheduler and a Web-based interface makes it easy to maintain a regular backup. +

+
+ + duplicati.desktop + + https://www.duplicati.com/ + https://github.com/duplicati/duplicati/issues + https://github.com/duplicati/duplicati + + + duplicati + + +
\ No newline at end of file