Added pCloud Integration

Added pCloud backend with custom interface for editing destination.
This commit is contained in:
Marcelo Ceccon
2024-11-18 13:07:01 +01:00
committed by Marcelo C.
parent 81ed27aaa2
commit 4beafc0cc7
22 changed files with 1471 additions and 12 deletions
+13
View File
@@ -160,6 +160,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.Secre
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.SecretTool", "Executables\net8\Duplicati.CommandLine.SecretTool\Duplicati.CommandLine.SecretTool.csproj", "{E1D32EF7-368E-4148-9367-B328E78C871B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.pCloud", "Duplicati\Library\Backend\pCloud\Duplicati.Library.Backend.pCloud.csproj", "{CA1A0EE8-DFFF-42AF-B15D-EF538357A2DE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "rapidtest", "rapidtest\rapidtest.csproj", "{04FE4498-5772-4E67-A9E4-24D9EAF81960}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -454,6 +458,14 @@ Global
{E1D32EF7-368E-4148-9367-B328E78C871B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E1D32EF7-368E-4148-9367-B328E78C871B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E1D32EF7-368E-4148-9367-B328E78C871B}.Release|Any CPU.Build.0 = Release|Any CPU
{CA1A0EE8-DFFF-42AF-B15D-EF538357A2DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CA1A0EE8-DFFF-42AF-B15D-EF538357A2DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CA1A0EE8-DFFF-42AF-B15D-EF538357A2DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CA1A0EE8-DFFF-42AF-B15D-EF538357A2DE}.Release|Any CPU.Build.0 = Release|Any CPU
{04FE4498-5772-4E67-A9E4-24D9EAF81960}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04FE4498-5772-4E67-A9E4-24D9EAF81960}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04FE4498-5772-4E67-A9E4-24D9EAF81960}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04FE4498-5772-4E67-A9E4-24D9EAF81960}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -511,6 +523,7 @@ Global
{A2957269-C11F-44CE-B355-CC9BA342295D} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{D6417FD3-1645-4AB4-9F42-A1EC4A262B37} = {D19A38DD-68F1-4EF5-BF5F-8966CE0D9A5B}
{E1D32EF7-368E-4148-9367-B328E78C871B} = {6B46F6B1-1898-49B8-ADA7-5CAF68EB77E3}
{CA1A0EE8-DFFF-42AF-B15D-EF538357A2DE} = {E1A9B303-F281-45C5-A4F6-CADD9DE3F3C4}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8B40BAFE-D862-4397-9495-8F5EAF5CE80C}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Library</OutputType>
<Copyright>Copyright © 2024 Team Duplicati, MIT license</Copyright>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Interface\Duplicati.Library.Interface.csproj" />
<ProjectReference Include="..\..\Localization\Duplicati.Library.Localization.csproj" />
<ProjectReference Include="..\..\Utility\Duplicati.Library.Utility.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.Analyzers.Compatibility" Version="0.2.12-alpha">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
@@ -0,0 +1,46 @@
// 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.
#nullable enable
namespace Duplicati.Library.Backend.pCloud;
/// <summary>
/// Response from pCloud's create folder API endpoint
///
/// The all lower case is to match the JSON response from the pCloud API
/// </summary>
internal record pClouCreateFolderResponse
{
/// <summary>
/// Result code from the API call. 0 indicates success
/// </summary>
public int result { get; init; }
/// <summary>
/// Indicates if a new folder was created (false if folder already existed)
/// </summary>
public bool created { get; init; }
/// <summary>
/// Detailed metadata about the created or existing folder
/// </summary>
public PCloudFolderMetadata? metadata { get; init; }
}
@@ -0,0 +1,90 @@
// 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.
namespace Duplicati.Library.Backend.pCloud;
/// <summary>
/// Metadata information for a pCloud folder
///
/// The all lower case is to match the JSON response from the pCloud API
/// </summary>
internal record PCloudFolderMetadata
{
/// <summary>
/// Name of the folder
/// </summary>
public string name { get; init; } = string.Empty;
/// <summary>
/// Creation timestamp in format "Day, DD MMM YYYY HH:MM:SS +0000"
/// </summary>
public string created { get; init; } = string.Empty;
/// <summary>
/// Indicates if the folder belongs to the authenticated user
/// </summary>
public bool ismine { get; init; }
/// <summary>
/// Indicates if the folder has a thumbnail
/// </summary>
public bool thumb { get; init; }
/// <summary>
/// Last modification timestamp in format "Day, DD MMM YYYY HH:MM:SS +0000"
/// </summary>
public string modified { get; init; } = string.Empty;
/// <summary>
/// Number of comments on the folder
/// </summary>
public int comments { get; init; }
/// <summary>
/// Unique identifier for the folder, prefixed with 'd'
/// </summary>
public string id { get; init; } = string.Empty;
/// <summary>
/// Indicates if the folder is shared with other users
/// </summary>
public bool isshared { get; init; }
/// <summary>
/// Icon type for the folder
/// </summary>
public string icon { get; init; } = string.Empty;
/// <summary>
/// Always true for folder metadata
/// </summary>
public bool isfolder { get; init; }
/// <summary>
/// Numeric ID of the parent folder
/// </summary>
public ulong parentfolderid { get; init; }
/// <summary>
/// Numeric ID of this folder
/// </summary>
public ulong folderid { get; init; }
}
@@ -0,0 +1,38 @@
// 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.
namespace Duplicati.Library.Backend.pCloud;
/// <summary>
/// Checksum information for uploaded file
/// </summary>
internal record pCloudChecksum
{
/// <summary>
/// SHA1 hash of the file
/// </summary>
public string sha1 { get; init; } = string.Empty;
/// <summary>
/// SHA256 hash of the file
/// </summary>
public string sha256 { get; init; } = string.Empty;
}
@@ -0,0 +1,46 @@
// 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.
#nullable enable
namespace Duplicati.Library.Backend.pCloud;
/// <summary>
/// Response from pCloud's create folder API endpoint
///
/// The all lower case is to match the JSON response from the pCloud API
/// </summary>
internal record pCloudDeleteResponse
{
/// <summary>
/// Result code from the API call. 0 indicates success
/// </summary>
public int result { get; init; }
/// <summary>
/// Presumed to the be the ID of the deleted file
/// </summary>
public string? id { get; init; }
/// <summary>
/// Detailed metadata about the created or existing folder
/// </summary>
public pCloudFileMetadata? metadata { get; init; }
}
@@ -0,0 +1,48 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
namespace Duplicati.Library.Backend.pCloud;
/// <summary>
/// Response from pCloud's download file API endpoint, containing file download information
///
/// The all lower case is to match the JSON response from the pCloud API
/// </summary>
internal record pCloudDownloadResponse
{
/// <summary>
/// Result code from the API call. 0 indicates success
/// </summary>
public int result { get; init; }
/// <summary>
/// List of available download hosts/servers
/// </summary>
public List<string> hosts { get; init; } = new();
/// <summary>
/// Download path to be used with the host
/// </summary>
public string path { get; init; } = string.Empty;
}
@@ -0,0 +1,49 @@
// 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;
namespace Duplicati.Library.Backend.pCloud;
/// <summary>
/// Error codes/strings for pcloud. They will not be subject to translation so messages
/// can be googled to help users diagnose issues.
///
/// </summary>
internal static class pCloudErrorList
{
internal static readonly Dictionary<int, string> ErrorMessages = new()
{
{ 1000, "Log in required." },
{ 1001, "No full path or name/folderid provided." },
{ 1004, "No fileid or path provided." },
{ 1005, "Unknown content-type requested." },
{ 2000, "Log in failed." },
{ 2002, "A component of parent directory does not exist." },
{ 2003, "Access denied. You do not have permissions to preform this operation." },
{ 2009, "File not found." },
{ 2010, "Invalid path." },
{ 2094, "Invalid 'access_token' provided. Please check you are using the correct server (eapi.pcloud.com for EU or api.pcloud.com for non EU)"},
{ 2011, "Requested speed limit too low, see minspeed for minimum." },
{ 4000, "Too many login tries from this IP address." },
{ 5002, "Internal error, no servers available. Try again later." }
};
}
@@ -0,0 +1,56 @@
// 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.Library.Interface;
namespace Duplicati.Library.Backend.pCloud;
/// <summary>
/// Represents a file entry or folder in pCloud
/// </summary>
internal class pCloudFileEntry : IFileEntry
{
/// <summary>
/// Indicates it is a folder on the remote filesystem
/// </summary>
public bool IsFolder { get; set; }
/// <summary>
/// Last access date of the file
/// </summary>
public DateTime LastAccess { get; set; }
/// <summary>
/// Last modification of the file
/// </summary>
public DateTime LastModification { get; set; }
/// <summary>
/// Filename or folder name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Size of the object in bytes
/// </summary>
public long Size { get; set; }
}
@@ -0,0 +1,123 @@
// 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.
namespace Duplicati.Library.Backend.pCloud;
/// <summary>
/// Metadata information for a pCloud file
/// </summary>
internal record pCloudFileMetadata
{
/// <summary>
/// Always false for file metadata
/// </summary>
public bool isfolder { get; init; }
/// <summary>
/// Icon type representing the file type
/// </summary>
public string icon { get; init; } = string.Empty;
/// <summary>
/// Size of the file in bytes
/// </summary>
public long size { get; init; }
/// <summary>
/// Name of the file including extension
/// </summary>
public string name { get; init; } = string.Empty;
/// <summary>
/// Category of the file (e.g., 1 for images)
/// </summary>
public int category { get; init; }
/// <summary>
/// MIME type of the file
/// </summary>
public string contenttype { get; init; } = string.Empty;
/// <summary>
/// ID of the parent folder containing this file
/// </summary>
public ulong parentfolderid { get; init; }
/// <summary>
/// Indicates if the file has been deleted
/// </summary>
public bool isdeleted { get; init; }
/// <summary>
/// Hash value of the file content
/// </summary>
public ulong hash { get; init; }
/// <summary>
/// Indicates if the file belongs to the authenticated user
/// </summary>
public bool ismine { get; init; }
/// <summary>
/// Indicates if the file is shared with other users
/// </summary>
public bool isshared { get; init; }
/// <summary>
/// Unique identifier for the file, prefixed with 'f'
/// </summary>
public string id { get; init; } = string.Empty;
/// <summary>
/// Height of the image in pixels (only for image files)
/// </summary>
public int height { get; init; }
/// <summary>
/// Width of the image in pixels (only for image files)
/// </summary>
public int width { get; init; }
/// <summary>
/// Last modification timestamp in format "Day, DD MMM YYYY HH:MM:SS +0000"
/// </summary>
public string modified { get; init; } = string.Empty;
/// <summary>
/// Indicates if the file has a thumbnail
/// </summary>
public bool thumb { get; init; }
/// <summary>
/// Creation timestamp in format "Day, DD MMM YYYY HH:MM:SS +0000"
/// </summary>
public string created { get; init; } = string.Empty;
/// <summary>
/// Numeric ID of the file
/// </summary>
public ulong fileid { get; init; }
/// <summary>
/// Full path to the file
/// </summary>
public string path { get; init; } = string.Empty;
}
@@ -0,0 +1,109 @@
// 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.
#nullable enable
namespace Duplicati.Library.Backend.pCloud;
/// <summary>
/// Content item (file or folder) within a pCloud folder
/// </summary>
internal record pCloudFolderContent
{
/// <summary>
/// Full path to the item
/// </summary>
public string path { get; init; } = string.Empty;
/// <summary>
/// Name of the file or folder
/// </summary>
public string name { get; init; } = string.Empty;
/// <summary>
/// Creation timestamp in format "Day, DD MMM YYYY HH:MM:SS +0000"
/// </summary>
public string created { get; init; } = string.Empty;
/// <summary>
/// Indicates if the item belongs to the authenticated user
/// </summary>
public bool ismine { get; init; }
/// <summary>
/// Indicates if the item has a thumbnail
/// </summary>
public bool thumb { get; init; }
/// <summary>
/// Last modification timestamp in format "Day, DD MMM YYYY HH:MM:SS +0000"
/// </summary>
public string modified { get; init; } = string.Empty;
/// <summary>
/// Number of comments on the item
/// </summary>
public int comments { get; init; }
/// <summary>
/// Unique identifier for the item, prefixed with 'd' for folders or 'f' for files
/// </summary>
public string id { get; init; } = string.Empty;
/// <summary>
/// Indicates if the item is shared with other users
/// </summary>
public bool isshared { get; init; }
/// <summary>
/// Icon type for the item
/// </summary>
public string icon { get; init; } = string.Empty;
/// <summary>
/// True if the item is a folder, false if it's a file
/// </summary>
public bool isfolder { get; init; }
/// <summary>
/// Folder ID if the item is a folder (null otherwise)
/// </summary>
public long? folderid { get; init; }
/// <summary>
/// File ID if the item is a file (null otherwise)
/// </summary>
public ulong? fileid { get; init; }
/// <summary>
/// Size in bytes if the item is a file (null otherwise)
/// </summary>
public long? size { get; init; }
/// <summary>
/// Content type if the item is a file (null otherwise)
/// </summary>
public string? contenttype { get; init; }
/// <summary>
/// ID of the parent folder containing this item
/// </summary>
public long parentfolderid { get; init; }
}
@@ -0,0 +1,39 @@
// 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.
#nullable enable
namespace Duplicati.Library.Backend.pCloud;
/// <summary>
/// Response from pCloud's list folder API endpoint
/// </summary>
internal record pCloudListFolderResponse
{
/// <summary>
/// Result code from the API call. 0 indicates success
/// </summary>
public int result { get; init; }
/// <summary>
/// Detailed metadata about the folder and its contents
/// </summary>
public pCloudListFoldersMetadata? metadata { get; init; }
}
@@ -0,0 +1,95 @@
// 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;
namespace Duplicati.Library.Backend.pCloud;
/// <summary>
/// Metadata information for a pCloud folder listing, including its contents
/// </summary>
internal record pCloudListFoldersMetadata
{
/// <summary>
/// Full path to the folder
/// </summary>
public string path { get; init; } = string.Empty;
/// <summary>
/// Name of the folder
/// </summary>
public string name { get; init; } = string.Empty;
/// <summary>
/// Creation timestamp in format "Day, DD MMM YYYY HH:MM:SS +0000"
/// </summary>
public string created { get; init; } = string.Empty;
/// <summary>
/// Indicates if the folder belongs to the authenticated user
/// </summary>
public bool ismine { get; init; }
/// <summary>
/// Indicates if the folder has a thumbnail
/// </summary>
public bool thumb { get; init; }
/// <summary>
/// Last modification timestamp in format "Day, DD MMM YYYY HH:MM:SS +0000"
/// </summary>
public string modified { get; init; } = string.Empty;
/// <summary>
/// Number of comments on the folder
/// </summary>
public int comments { get; init; }
/// <summary>
/// Unique identifier for the folder, prefixed with 'd'
/// </summary>
public string id { get; init; } = string.Empty;
/// <summary>
/// Indicates if the folder is shared with other users
/// </summary>
public bool isshared { get; init; }
/// <summary>
/// Icon type for the folder
/// </summary>
public string icon { get; init; } = string.Empty;
/// <summary>
/// Always true for folder metadata
/// </summary>
public bool isfolder { get; init; }
/// <summary>
/// Numeric ID of this folder
/// </summary>
public long folderid { get; init; }
/// <summary>
/// List of files and folders contained within this folder
/// </summary>
public List<pCloudFolderContent> contents { get; init; } = new();
}
@@ -0,0 +1,50 @@
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
namespace Duplicati.Library.Backend.pCloud;
/// <summary>
/// Response from pCloud's upload file API endpoint
/// </summary>
internal record pCloudUploadResponse
{
/// <summary>
/// Result code from the API call. 0 indicates success
/// </summary>
public int result { get; init; }
/// <summary>
/// Array of metadata for uploaded files
/// </summary>
public List<pCloudFileMetadata> metadata { get; init; } = new();
/// <summary>
/// Checksums for uploaded files
/// </summary>
public List<pCloudChecksum> checksums { get; init; } = new();
/// <summary>
/// List of file IDs for uploaded files
/// </summary>
public List<ulong> fileids { get; init; } = new();
}
@@ -0,0 +1,35 @@
// 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.Localization.Short;
namespace Duplicati.Library.Backend.Strings {
internal static class pCloudBackend {
public static string Description => LC.L(@"This backend can read and write data to pCloud with native API. Allowed format is ""pcloud://api.pcloud.com"".");
public static string DisplayName => LC.L(@"pCloud (Native API)");
public static string AuthPasswordDescriptionLong => LC.L(@"The oAuth token used to connect to the server. This may also be supplied as the environment variable ""AUTHID"".");
public static string AuthPasswordDescriptionShort => LC.L(@"Supply the oAuth token used to connect to the server");
public static string NoServerSpecified => LC.L(@"No server specified, must be either api.pcloud.com or eapi.pcloud.com for European hosting");
public static string InvalidServerSpecified => LC.L(@"Invalid server specified, must be either api.pcloud.com or eapi.pcloud.com for European hosting");
public static string FailedWithUnexpectedErrorCode(string operation, int resultcode) => LC.L(@"Operation {0} failed with unexpected result code: {1}", operation, resultcode);
}
}
@@ -0,0 +1,544 @@
// 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.Library.Interface;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Duplicati.Library.Backend.pCloud;
using Duplicati.Library.Utility;
using Uri = System.Uri;
namespace Duplicati.Library.Backend;
/// <summary>
/// Native pCloud Backend API implementation
/// </summary>
public class pCloudBackend : IStreamingBackend
{
/// <summary>
/// Implementation of interface property for the backend key
/// </summary>
public string ProtocolKey => "pcloud";
/// <summary>
/// Implementation of interface property for the backend display name
/// </summary>
public string DisplayName => Strings.pCloudBackend.DisplayName;
/// <summary>
/// Implementation of interface property for the backend description
/// </summary>
public string Description => Strings.pCloudBackend.Description;
/// <summary>
/// The default timeout in seconds for PUT/GET file operations
/// </summary>
private const int LONG_OPERATION_TIMEOUT_SECONDS = 30000;
/// <summary>
/// The default timeout in seconds for LIST/CreateFolder operations
/// </summary>
private const int SHORT_OPERATION_TIMEOUT_SECONDS = 30;
/// <summary>
/// The server URL to be used (pcloud uses 2 different endpoints depending if its an european or non european hosting)
/// </summary>
private string _ServerUrl;
/// <summary>
/// Bearer token to be using in the API
/// </summary>
private string _Token;
/// <summary>
/// Remote path/folder to use used in the backend
/// </summary>
private string _Path;
/// <summary>
/// Hostname only (no ports or paths) to be used on DNS resolutions.
/// </summary>
private string _DnsName;
/// <summary>
/// Variable being used to cache the folder ID, as it is required to upload files
/// and the only way to obtain is with an API call. The cache is to avoid multiple
/// requests
/// </summary>
private ulong? _CachedFolderID;
private const string AUTHENTICATION_OPTION = "authid";
private static readonly Dictionary<string, string> PCLOUD_SERVERS = new(StringComparer.OrdinalIgnoreCase)
{
{ "pCloud Global", "api.pcloud.com" },
{ "pCloud (EU)", "eapi.pcloud.com" },
};
/// <summary>
/// Empty constructor is required for the backend to be loaded by the backend factory
/// </summary>
public pCloudBackend()
{
}
/// <summary>
/// Actual constructor for the backend that accepts the url and options
/// </summary>
/// <param name="url">URL in Duplicati Uri format</param>
/// <param name="options">options to be used in the backend</param>
public pCloudBackend(string url, Dictionary<string, string> options)
{
var uri = new Utility.Uri(url);
uri.RequireHost();
_DnsName = uri.Host;
if (options.TryGetValue(AUTHENTICATION_OPTION, out var option))
_Token = option;
if (!PCLOUD_SERVERS.ContainsValue(uri.Host))
throw new UserInformationException(Strings.pCloudBackend.InvalidServerSpecified,
"InvalidpCloudServerSpecified");
if (string.IsNullOrWhiteSpace(uri.Host))
throw new UserInformationException(Strings.pCloudBackend.NoServerSpecified, "NopCloudServerSpecified");
_Path = uri.Path;
_ServerUrl = uri.Host;
}
/// <summary>
/// Implementation of interface property to return supported command parameters
/// </summary>
public IList<ICommandLineArgument> SupportedCommands
{
get
{
return new List<ICommandLineArgument>(new ICommandLineArgument[]
{
new CommandLineArgument(AUTHENTICATION_OPTION,
CommandLineArgument.ArgumentType.Password,
Strings.pCloudBackend.AuthPasswordDescriptionShort,
Strings.pCloudBackend.AuthPasswordDescriptionLong),
});
}
}
/// <summary>
/// Implementation of interface method for listing remote folder contents.
/// The root parameter is used to list the root folder, as the pCloud API
/// when using oauth tokens creates an isolated folder Applications/ApplicationName
///
/// When root = true, this is the directory it will be listed.
/// When root = false, it will list the _Path directory if it exists, which is
/// where the Duplicati backup files are stored.
/// </summary>
/// <param name="root">True to list absolute root, false to list _Path directory</param>
/// <returns></returns>
private IEnumerable<IFileEntry> List(bool root)
{
if (_CachedFolderID == null && string.IsNullOrWhiteSpace(_Path) == false && root == false)
CreateFolderAsync(new CancellationToken()).GetAwaiter().GetResult();
ulong folderId = root ? 0 : _CachedFolderID ?? 0;
var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfter(TimeSpan.FromSeconds(SHORT_OPERATION_TIMEOUT_SECONDS));
using var requestResources = CreateRequest($"/listfolder?folderid={folderId}", HttpMethod.Get);
using var response = requestResources.HttpClient
.SendAsync(requestResources.RequestMessage, HttpCompletionOption.ResponseContentRead,
timeoutToken.Token).ConfigureAwait(false).GetAwaiter().GetResult();
response.EnsureSuccessStatusCode();
var content = response.Content.ReadAsStringAsync(timeoutToken.Token).Result;
var listFolderResponse = JsonSerializer.Deserialize<pCloudListFolderResponse>(content);
if (pCloudErrorList.ErrorMessages.TryGetValue(listFolderResponse.result, out var message))
throw new Exception(message);
if (listFolderResponse.result != 0)
throw new Exception(Strings.pCloudBackend.FailedWithUnexpectedErrorCode("list", listFolderResponse.result));
if (listFolderResponse?.metadata?.contents == null)
return new List<IFileEntry>();
return listFolderResponse.metadata.contents
.Select<pCloudFolderContent, IFileEntry>(item => new pCloudFileEntry
{
IsFolder = item.isfolder,
Name = item.name,
Size = item.size ?? 0,
LastAccess = DateTime.Parse(item.created),
LastModification = DateTime.Parse(item.modified)
})
.ToList();
}
/// <summary>
/// Implementation of interface method for listing remote folder contents
/// </summary>
/// <returns>List of IFileEntry with directory listing result</returns>
public IEnumerable<IFileEntry> List()
{
return List(false);
}
/// <summary>
/// Upload files to remote location
/// </summary>
/// <param name="remotename">Filename at remote location</param>
/// <param name="localname">Filename to read from</param>
/// <param name="cancellationToken">CancellationToken that is combined with internal timeout token</param>
/// <exception cref="FileMissingException">FileMissingException when file is not found</exception>
/// <exception cref="Exception">Exceptions arising from either code execution</exception>
public async Task PutAsync(string remotename, string localname, CancellationToken cancellationToken)
{
await using var fs = File.Open(localname,
FileMode.Open, FileAccess.Read, FileShare.Read);
await PutAsync(remotename, fs, cancellationToken);
}
/// <summary>
/// Upload files to remote location
/// </summary>
/// <param name="remotename">Filename at remote location</param>
/// <param name="input">Stream to read from</param>
/// <param name="cancellationToken">CancellationToken that is combined with internal timeout token</param>
/// <exception cref="FileMissingException">FileMissingException when file is not found</exception>
/// <exception cref="Exception">Exceptions arising from either code execution</exception>
public async Task PutAsync(string remotename, Stream input, CancellationToken cancellationToken)
{
// If we don't have the folder ID cached, lets fetch it with the create folder api
if (_CachedFolderID == null)
await CreateFolderAsync(cancellationToken);
using var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfter(TimeSpan.FromSeconds(LONG_OPERATION_TIMEOUT_SECONDS));
using var combinedTokens =
CancellationTokenSource.CreateLinkedTokenSource(timeoutToken.Token, cancellationToken);
var encodedPath = Uri.EscapeDataString(remotename);
using var requestResources =
CreateRequest($"/uploadfile?folderid={_CachedFolderID}&filename={encodedPath}&nopartial=1",
HttpMethod.Post);
input.Position = 0;
requestResources.RequestMessage.Content = new StreamContent(input);
requestResources.RequestMessage.Content.Headers.ContentLength = input.Length;
requestResources.RequestMessage.Content.Headers.ContentType =
new MediaTypeHeaderValue("application/octet-stream");
using var response = await requestResources.HttpClient.SendAsync(
requestResources.RequestMessage,
HttpCompletionOption.ResponseContentRead, combinedTokens.Token);
var content = await response.Content.ReadAsStringAsync(combinedTokens.Token);
var uploadResponse = JsonSerializer.Deserialize<pCloudUploadResponse>(content);
if (pCloudErrorList.ErrorMessages.TryGetValue(uploadResponse.result, out var message))
throw new Exception(message);
if (uploadResponse.result != 0)
throw new Exception(Strings.pCloudBackend.FailedWithUnexpectedErrorCode("upload", uploadResponse.result));
}
/// <summary>
/// Download files from remote
/// </summary>
/// <param name="remotename">Filename at remote location</param>
/// <param name="output">Destination stream to write to</param>
/// <param name="cancellationToken">CancellationToken that is combined with internal timeout token</param>
/// <exception cref="FileMissingException">FileMissingException when file is not found</exception>
/// <exception cref="Exception">Exceptions arising from either code execution</exception>
public async Task GetAsync(string remotename, string localname, CancellationToken cancellationToken)
{
await using var fs = File.Open(localname,
FileMode.Create, FileAccess.Write,
FileShare.None);
await GetAsync(remotename, fs, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Obtains the file id from the filename to be used in the download API
///
/// </summary>
/// <param name="filename">Filename at remote, path is automatically concatenated if needed</param>
/// <returns></returns>
/// <exception cref="FileMissingException">FileMissingException when file is not found</exception>
/// <exception cref="Exception">Exceptions arising from either code execution</exception>
private async Task<string> GetFileId(string filename)
{
var path = string.IsNullOrWhiteSpace(_Path) ? "" : $"/{_Path}";
var encodedPath = Uri.EscapeDataString($"{path}/{filename}");
using var requestResources = CreateRequest($"/getfilelink?path={encodedPath}", HttpMethod.Get);
using var response = await requestResources.HttpClient.SendAsync(
requestResources.RequestMessage,
HttpCompletionOption.ResponseContentRead);
if (!response.IsSuccessStatusCode)
{
throw new Exception($"Failed to get download link. Status: {response.StatusCode}");
}
var content = await response.Content.ReadAsStringAsync();
var getFileIdResponse = JsonSerializer.Deserialize<pCloudDownloadResponse>(content);
if (pCloudErrorList.ErrorMessages.TryGetValue(getFileIdResponse.result, out var message))
throw new FileMissingException(message);
if (getFileIdResponse.result != 0)
throw new Exception(
Strings.pCloudBackend.FailedWithUnexpectedErrorCode("getfileid", getFileIdResponse.result));
return $"https://{getFileIdResponse.hosts[0]}{getFileIdResponse.path}";
}
/// <summary>
/// Download files from remote
/// </summary>
/// <param name="remotename">Filename at remote location</param>
/// <param name="output">Destination stream to write to</param>
/// <param name="cancellationToken">CancellationToken that is combined with internal timeout token</param>
/// <exception cref="FileMissingException">FileMissingException when file is not found</exception>
/// <exception cref="Exception">Exceptions arising from either code execution</exception>
public async Task GetAsync(string remotename, Stream output, CancellationToken cancellationToken)
{
try
{
using var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfter(TimeSpan.FromSeconds(LONG_OPERATION_TIMEOUT_SECONDS));
using var combinedTokens =
CancellationTokenSource.CreateLinkedTokenSource(timeoutToken.Token, cancellationToken);
using var requestResources = CreateRequest(string.Empty, HttpMethod.Get);
requestResources.RequestMessage.RequestUri = new Uri(await GetFileId(remotename));
await requestResources.HttpClient.DownloadFile(requestResources.RequestMessage, output, null,
timeoutToken.Token);
}
catch (HttpRequestException wex)
{
/*
* Known Behaviour on pCloud
*
* If the temporary link is no longer valid, it will return a 401
* if the url is corrupted/wrong it will return a 500
*/
switch (wex)
{
case { StatusCode: HttpStatusCode.Unauthorized }:
throw new FileMissingException("Temporary link is no longer valid", wex);
case { StatusCode: HttpStatusCode.InternalServerError }:
throw new FileMissingException("Temporary link is corrupted", wex);
default:
throw;
}
}
}
/// <summary>
/// Delete remote file if it exists, if now, throws FileMissingException
/// </summary>
/// <param name="remotename">filename to be deleted on the remote</param>
/// <param name="cancellationToken">CancellationToken that is combined with internal timeout token</param>
/// <returns></returns>
/// <exception cref="FileMissingException">FileMissingException when file is not found</exception>
/// <exception cref="Exception">Exceptions arising from either code execution or business logic when return code from pcloud indicates an error.</exception>
public Task DeleteAsync(string remotename, CancellationToken cancellationToken)
{
if (!String.IsNullOrWhiteSpace(_Path))
remotename = $"/{_Path}/{remotename}";
using var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfter(TimeSpan.FromSeconds(SHORT_OPERATION_TIMEOUT_SECONDS));
using var combinedTokens =
CancellationTokenSource.CreateLinkedTokenSource(timeoutToken.Token, cancellationToken);
using var requestResources = CreateRequest($"/deletefile?path={remotename}", HttpMethod.Get);
using var response = requestResources.HttpClient.SendAsync(
requestResources.RequestMessage,
HttpCompletionOption.ResponseContentRead, combinedTokens.Token).GetAwaiter().GetResult();
response.EnsureSuccessStatusCode();
var content = response.Content.ReadAsStringAsync(combinedTokens.Token).GetAwaiter().GetResult();
var deleteFileResponse = JsonSerializer.Deserialize<pCloudDeleteResponse>(content);
// If no error code is matched, result was == 0 so it successfully created the folder
if (deleteFileResponse.result == 2009)
throw new FileMissingException();
if (pCloudErrorList.ErrorMessages.TryGetValue(deleteFileResponse.result, out var message))
throw new Exception(message);
if (deleteFileResponse.result != 0)
throw new Exception(
Strings.pCloudBackend.FailedWithUnexpectedErrorCode("delete", deleteFileResponse.result));
return Task.CompletedTask;
}
/// <summary>
/// Implementation of interface function to return hosnames used by the backend
/// </summary>
/// <param name="cancellationToken">CancellationToken, in this call not used.</param>
/// <returns></returns>
public Task<string[]> GetDNSNamesAsync(CancellationToken cancellationToken) => Task.FromResult(new[] { _DnsName });
/// <summary>
/// Performs basic backend conectivity tests, in this case, checks
/// that the configured path, if configured, exists
/// </summary>
/// <param name="cancellationToken">CancellationToken, in this call not used.</param>
/// <returns></returns>
/// <exception cref="FolderMissingException">Thrown when configured path does not exist</exception>
public Task TestAsync(CancellationToken cancellationToken)
{
// List the scoped root directory and check if _Path folder exists
if (!List(true).Any(x => x.IsFolder && x.Name == _Path) && !string.IsNullOrWhiteSpace(_Path))
throw new FolderMissingException();
return Task.CompletedTask;
}
/// <summary>
/// Create remote folder
/// </summary>
/// <param name="cancellationToken">CancellationToken that will be combined with internal timeout token</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public Task CreateFolderAsync(CancellationToken cancellationToken)
{
using var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfter(TimeSpan.FromSeconds(SHORT_OPERATION_TIMEOUT_SECONDS));
using var combinedTokens =
CancellationTokenSource.CreateLinkedTokenSource(timeoutToken.Token, cancellationToken);
using var requestResources = CreateRequest($"/createfolderifnotexists?folderid=0&name={_Path}", HttpMethod.Get);
using var response = requestResources.HttpClient.SendAsync(
requestResources.RequestMessage,
HttpCompletionOption.ResponseContentRead, combinedTokens.Token).GetAwaiter().GetResult();
response.EnsureSuccessStatusCode();
var content = response.Content.ReadAsStringAsync(combinedTokens.Token).GetAwaiter().GetResult();
var createFolderResponse = JsonSerializer.Deserialize<pClouCreateFolderResponse>(content);
if (pCloudErrorList.ErrorMessages.TryGetValue(createFolderResponse.result, out var message))
throw new Exception(message);
/*
* If no error code is matched, result was == 0 so it successfully created the folder
* We must cache the folderID, as that will be used to upload files, reason being that
* for oauth clients, pCloud creates a scoped folder Applications/ApplicationName
* and the folders created with /createfolderifnotexists api are created under that folder.
*
* However, when uploading files, the folderid is required (and surprisingly if not provided
* it will create the file outside the scoped folder, at the root of the drive)
*/
if (createFolderResponse.result == 0) _CachedFolderID = createFolderResponse.metadata?.folderid;
return Task.CompletedTask;
}
/// <summary>
/// Wrapper for the tupple of HttpClient and HttpRequestMessage used in web requests.
/// </summary>
/// <param name="HttpClient">The HTTPClient</param>
/// <param name="RequestMessage">The HttpRequestMessage object</param>
private record RequestResources(HttpClient HttpClient, HttpRequestMessage RequestMessage) : IDisposable
{
public void Dispose()
{
try
{
RequestMessage?.Dispose();
}
catch
{
// ignored
}
try
{
HttpClient?.Dispose();
}
catch
{
// ignored
}
}
}
/// <summary>
/// Helper method to create request resources with the bearer token and default headers
/// </summary>
/// <param name="url">url to be appended after host</param>
/// <param name="method">Http Method</param>
/// <returns></returns>
private RequestResources CreateRequest(string url, HttpMethod method = null)
{
HttpClient httpClient;
HttpClientHandler httpHandler = new HttpClientHandler();
httpClient = HttpClientHelper.CreateClient(httpHandler);
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer",
_Token
);
// Set the timeout to infinite, all methods are called with cancelationTokens.
httpClient.Timeout = Timeout.InfiniteTimeSpan;
var request = new HttpRequestMessage(HttpMethod.Get, $"https://{_ServerUrl}/{url}");
request.Headers.Add(HttpRequestHeader.UserAgent.ToString(),
"Duplicati pCloud Client v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
if (method != null)
request.Method = method;
return new RequestResources(httpClient, request);
}
/// <summary>
/// Implementation of Dispose pattern enforced by interface
/// in this case, we don't need to dispose anything
/// </summary>
public void Dispose()
{
}
}
+2 -1
View File
@@ -65,7 +65,8 @@ public static class BackendModules
IsStorjSupported? new Backend.Storj.Storj() : null,
new Backend.TahoeBackend(),
new Backend.TencentCOS.COS(),
new Backend.WEBDAV()
new Backend.WEBDAV(),
new Backend.pCloudBackend()
}
.Where(x => x != null)
.ToList();
@@ -20,6 +20,7 @@
<ProjectReference Include="..\Backend\Mega\Duplicati.Library.Backend.Mega.csproj" />
<ProjectReference Include="..\Backend\OneDrive\Duplicati.Library.Backend.OneDrive.csproj" />
<ProjectReference Include="..\Backend\OpenStack\Duplicati.Library.Backend.OpenStack.csproj" />
<ProjectReference Include="..\Backend\pCloud\Duplicati.Library.Backend.pCloud.csproj" />
<ProjectReference Include="..\Backend\Rclone\Duplicati.Library.Backend.Rclone.csproj" />
<ProjectReference Include="..\Backend\S3\Duplicati.Library.Backend.S3.csproj" />
<ProjectReference Include="..\Backend\SharePoint\Duplicati.Library.Backend.SharePoint.csproj" />
@@ -71,7 +71,6 @@ public static class HttpClientExtensions
/// <returns></returns>
public static async Task DownloadFile(this HttpClient client, HttpRequestMessage request, Stream fileStream, Action<long> progressReportingAction = null, CancellationToken cancellationToken = default)
{
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
@@ -30,11 +30,12 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.templates['dropbox'] = 'templates/backends/oauth.html';
EditUriBackendConfig.templates['sia'] = 'templates/backends/sia.html';
EditUriBackendConfig.templates['storj'] = 'templates/backends/storj.html';
EditUriBackendConfig.templates['rclone'] = 'templates/backends/rclone.html';
EditUriBackendConfig.templates['rclone'] = 'templates/backends/rclone.html';
EditUriBackendConfig.templates['cos'] = 'templates/backends/cos.html';
EditUriBackendConfig.templates['aliyunoss'] = 'templates/backends/aliyunoss.html';
EditUriBackendConfig.templates['aliyunoss'] = 'templates/backends/aliyunoss.html';
EditUriBackendConfig.templates['e2'] = 'templates/backends/e2.html';
EditUriBackendConfig.templates['pcloud'] = 'templates/backends/pcloud.html';
EditUriBackendConfig.testers['s3'] = function(scope, callback) {
if (scope.s3_server != 's3.amazonaws.com')
@@ -276,6 +277,8 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.loaders['jottacloud'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.loaders['box'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.loaders['dropbox'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.loaders['pcloud'] = function() {
return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.loaders['openstack'] = function (scope) {
if (scope.openstack_providers == null) {
@@ -333,8 +336,8 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
this['oauth-base'].apply(this, arguments);
};
// Parsers take a decomposed uri input and sets up the scope variables
// Parsers take a decomposed uri input and sets up the scope variables
EditUriBackendConfig.parsers['file'] = function (scope, module, server, port, path, options) {
if (scope.Path == null && scope.Server == null) {
var ix = scope.uri.indexOf('?');
@@ -419,8 +422,8 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
scope.AuthID = options['--authid'];
delete options['--authid'];
EditUriBackendConfig.mergeServerAndPath(scope);
if (module !== 'pcloud')
EditUriBackendConfig.mergeServerAndPath(scope);
};
EditUriBackendConfig.parsers['googledrive'] = function() { return this['oauth-base'].apply(this, arguments); };
@@ -430,7 +433,8 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.parsers['jottacloud'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.parsers['box'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.parsers['dropbox'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.parsers['pcloud'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.parsers['openstack'] = function (scope, module, server, port, path, options) {
scope.openstack_domainname = options['--openstack-domain-name'];
scope.openstack_server = scope.openstack_server_custom = options['--openstack-authuri'];
@@ -651,6 +655,20 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
return url;
};
EditUriBackendConfig.builders['pcloud'] = function (scope) {
var opts = {
'authid': scope.AuthID
}
EditUriBackendConfig.merge_in_advanced_options(scope, opts, false);
return AppUtils.format('{0}://{1}/{2}{3}',
scope.Backend.Key,
scope.Server,
scope.Path || '',
AppUtils.encodeDictAsUrl(opts)
);
};
EditUriBackendConfig.builders['googledrive'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.builders['onedrive'] = function() { return this['oauth-base'].apply(this, arguments); };
EditUriBackendConfig.builders['onedrivev2'] = function() { return this['oauth-base'].apply(this, arguments); };
@@ -952,7 +970,17 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.validaters['cloudfiles'] = EditUriBackendConfig.validaters['ssh'];
EditUriBackendConfig.validaters['tahoe'] = EditUriBackendConfig.validaters['ssh'];
EditUriBackendConfig.validaters['pcloud'] = function (scope, continuation) {
var res =
EditUriBackendConfig.require_server(scope) &&
EditUriBackendConfig.require_field(scope, 'AuthID', gettextCatalog.getString('AuthID'));
if (res)
EditUriBackendConfig.recommend_path(scope, function() {
continuation();
});
};
EditUriBackendConfig.validaters['authid-base'] = function (scope, continuation) {
var res =
EditUriBackendConfig.require_field(scope, 'AuthID', gettextCatalog.getString('AuthID'));
@@ -972,7 +1000,8 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo,
EditUriBackendConfig.validaters['onedrive'] = EditUriBackendConfig.validaters['authid-base'];
EditUriBackendConfig.validaters['onedrivev2'] = EditUriBackendConfig.validaters['authid-base'];
EditUriBackendConfig.validaters['sharepoint'] = EditUriBackendConfig.validaters['authid-base'];
EditUriBackendConfig.validaters['pcloud'] = EditUriBackendConfig.validaters['pcloud'];
EditUriBackendConfig.validaters['msgroup'] = function (scope, continuation) {
EditUriBackendConfig.validaters['authid-base'](scope, function () {
@@ -76,7 +76,8 @@ backupApp.service('SystemInfo', function($rootScope, $timeout, $cookies, AppServ
'jottacloud': null,
'rclone': null,
'cos': null,
'aliyunoss': null
'aliyunoss': null,
'pcloud': null
}
};
@@ -0,0 +1,25 @@
<div class="input text">
<label for="pcloud_server" translate>Server</label>
<select name="pcloud_server"
id="pcloud_server"
ng-model="$parent.Server">
<option value="api.pcloud.com" ng-selected="!$parent.Server || $parent.Server === 'api.pcloud.com'">
{{'PCloud Global (api.pcloud.com)' | translate}}
</option>
<option value="eapi.pcloud.com" ng-selected="$parent.Server === 'eapi.pcloud.com'">
{{'pCloud EU (eapi.pcloud.com)' | translate}}
</option>
</select>
</div>
<div class="input text">
<label for="oauth_path" translate>Path on server</label>
<input type="text" name="oauth_path" id="oauth_path" ng-model="$parent.Path" placeholder="{{'Destination path' | translate}}" />
</div>
<div class="input text">
<label for="oauth_authid">
<a ng-show="oauth_in_progress" href="{{oauth_start_link}}" target="_blank" translate>AuthID</a>
<a ng-hide="oauth_in_progress" href ng-click="oauth_start_token_creation()" translate>AuthID</a>
</label>
<input type="text" name="oauth_authid" id="oauth_authid" ng-model="$parent.AuthID" placeholder="{{'Click the AuthID link to create an AuthID' | translate}}" />
</div>