Files
duplicati/Duplicati/Library/Backend/Rclone/Rclone.cs
T

323 lines
12 KiB
C#
Raw Normal View History

2025-02-17 16:45:51 +01:00
// 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.
2024-02-28 15:45:30 +01:00
2019-06-11 19:28:53 -07:00
using Duplicati.Library.Common.IO;
2018-05-27 13:27:24 -07:00
using Duplicati.Library.Interface;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
2018-05-27 13:27:24 -07:00
using System.IO;
2025-02-17 16:45:51 +01:00
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
2018-05-27 13:27:24 -07:00
namespace Duplicati.Library.Backend
{
2019-06-11 19:28:53 -07:00
// ReSharper disable once UnusedMember.Global
// This class is instantiated dynamically in the BackendLoader.
2018-05-27 13:27:24 -07:00
public class Rclone : IBackend
{
private const string OPTION_LOCAL_REPO = "rclone-local-repository";
private const string OPTION_REMOTE_REPO = "rclone-remote-repository";
private const string OPTION_REMOTE_PATH = "rclone-remote-path";
private const string OPTION_RCLONE = "rclone-option";
private const string OPTION_RCLONE_EXECUTABLE = "rclone-executable";
private const string RCLONE_ERROR_DIRECTORY_NOT_FOUND = "directory not found";
private const string RCLONE_ERROR_CONFIG_NOT_FOUND = "didn't find section in config file";
private readonly string local_repo;
private readonly string remote_repo;
private readonly string remote_path;
private readonly string opt_rclone;
private readonly string rclone_executable;
public Rclone()
{
2018-05-27 13:27:24 -07:00
}
public Rclone(string url, Dictionary<string, string> options)
{
var uri = new Utility.Uri(url);
/*should check here if program is installed */
2018-05-27 13:27:24 -07:00
local_repo = options.GetValueOrDefault(OPTION_LOCAL_REPO, local_repo);
remote_repo = options.GetValueOrDefault(OPTION_REMOTE_REPO, remote_repo);
remote_path = options.GetValueOrDefault(OPTION_REMOTE_PATH, remote_path);
opt_rclone = options.GetValueOrDefault(OPTION_RCLONE, opt_rclone) ?? "";
rclone_executable = options.GetValueOrDefault(OPTION_RCLONE_EXECUTABLE, rclone_executable);
2018-05-27 13:27:24 -07:00
if (string.IsNullOrWhiteSpace(local_repo))
local_repo = "local";
if (string.IsNullOrWhiteSpace(remote_repo))
remote_repo = uri.Host;
if (string.IsNullOrWhiteSpace(remote_path))
remote_path = uri.Path;
if (string.IsNullOrWhiteSpace(rclone_executable))
rclone_executable = "rclone";
2018-05-27 13:27:24 -07:00
#if DEBUG
2020-01-25 17:07:02 -08:00
Console.WriteLine("Constructor {0}: {1}:{2} {3}", local_repo, remote_repo, remote_path, opt_rclone);
2018-05-27 13:27:24 -07:00
#endif
}
#region IBackendInterface Members
2018-05-27 13:27:24 -07:00
public string DisplayName
{
get { return Strings.Rclone.DisplayName; }
}
2018-05-27 13:27:24 -07:00
public string ProtocolKey
{
get { return "rclone"; }
}
private async Task<string> RcloneCommandExecuter(String command, String arguments, CancellationToken cancelToken)
2018-05-27 13:27:24 -07:00
{
StringBuilder outputBuilder = new StringBuilder();
StringBuilder errorBuilder = new StringBuilder();
Process process;
ProcessStartInfo psi = new ProcessStartInfo
{
Arguments = $"{arguments} {opt_rclone}",
2018-05-27 13:27:24 -07:00
CreateNoWindow = true,
FileName = command,
RedirectStandardError = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
UseShellExecute = false,
WindowStyle = ProcessWindowStyle.Hidden
};
#if DEBUG
2020-01-25 17:07:02 -08:00
Console.Error.WriteLine("command executing: {0} {1}", psi.FileName, psi.Arguments);
2018-05-27 13:27:24 -07:00
#endif
process = new Process
{
StartInfo = psi,
// enable raising events because Process does not raise events by default
EnableRaisingEvents = true
};
// attach the event handler for OutputDataReceived before starting the process
process.OutputDataReceived += new System.Diagnostics.DataReceivedEventHandler
(
delegate (object sender, System.Diagnostics.DataReceivedEventArgs e)
{
if (!String.IsNullOrEmpty(e.Data))
{
#if DEBUG
// Console.Error.WriteLine(String.Format("output {0}", e.Data));
2018-05-27 13:27:24 -07:00
#endif
// append the new data to the data already read-in
outputBuilder.Append(e.Data);
2018-10-10 21:22:31 -07:00
}
2018-05-27 13:27:24 -07:00
}
);
2018-05-27 13:27:24 -07:00
process.ErrorDataReceived += new System.Diagnostics.DataReceivedEventHandler
(
delegate (object sender, System.Diagnostics.DataReceivedEventArgs e)
{
if (!String.IsNullOrEmpty(e.Data))
{
#if DEBUG
2020-01-25 17:07:02 -08:00
Console.Error.WriteLine("error {0}", e.Data);
2018-05-27 13:27:24 -07:00
#endif
errorBuilder.Append(e.Data);
}
}
);
// start the process
// then begin asynchronously reading the output
// then wait for the process to exit
// then cancel asynchronously reading the output
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
while (!process.HasExited)
{
await Task.Delay(500).ConfigureAwait(false);
if (cancelToken.IsCancellationRequested)
{
process.Kill();
process.WaitForExit();
}
}
2018-05-27 13:27:24 -07:00
process.CancelOutputRead();
process.CancelErrorRead();
if (errorBuilder.ToString().Contains(RCLONE_ERROR_DIRECTORY_NOT_FOUND))
{
throw new FolderMissingException(errorBuilder.ToString());
}
if (errorBuilder.ToString().Contains(RCLONE_ERROR_CONFIG_NOT_FOUND))
{
throw new Exception($"Missing config file? {errorBuilder}");
2018-05-27 13:27:24 -07:00
}
if (errorBuilder.Length > 0)
{
2018-05-27 13:27:24 -07:00
throw new Exception(errorBuilder.ToString());
}
2019-07-08 08:27:43 +02:00
2018-05-27 13:27:24 -07:00
return outputBuilder.ToString();
}
2025-02-17 16:45:51 +01:00
/// <inheritdoc />
public async IAsyncEnumerable<IFileEntry> ListAsync([EnumeratorCancellation] CancellationToken cancelToken)
2018-05-27 13:27:24 -07:00
{
2025-02-17 16:45:51 +01:00
string str_result;
2018-05-27 13:27:24 -07:00
try
{
2025-02-17 16:45:51 +01:00
str_result = await RcloneCommandExecuter(rclone_executable, $"lsjson {remote_repo}:{remote_path}", cancelToken).ConfigureAwait(false);
2018-05-27 13:27:24 -07:00
// this will give an error if the executable does not exist.
}
catch (FolderMissingException ex)
{
throw new FolderMissingException(ex);
}
using (JsonReader jsonReader = new JsonTextReader(new StringReader(str_result)))
{
//no date parsing by JArray needed, will be parsed later
jsonReader.DateParseHandling = DateParseHandling.None;
var array = JArray.Load(jsonReader);
foreach (JObject item in array)
{
#if DEBUG
2018-05-27 13:17:30 -07:00
Console.Error.WriteLine(item);
2018-05-27 13:27:24 -07:00
#endif
FileEntry fe = new FileEntry(
item.GetValue("Name").Value<string>(),
item.GetValue("Size").Value<long>(),
DateTime.Parse(item.GetValue("ModTime").Value<string>()),
DateTime.Parse(item.GetValue("ModTime").Value<string>())
)
{
IsFolder = item.GetValue("IsDir").Value<bool>()
};
yield return fe;
}
}
}
public async Task PutAsync(string remotename, string filename, CancellationToken cancelToken)
2018-05-27 13:27:24 -07:00
{
try
{
await RcloneCommandExecuter(rclone_executable, $"copyto {local_repo}:{filename} {remote_repo}:{remote_path}/{remotename}", cancelToken).ConfigureAwait(false);
2018-05-27 13:27:24 -07:00
}
catch (FolderMissingException ex)
{
throw new FileMissingException(ex);
}
}
public async Task GetAsync(string remotename, string filename, CancellationToken cancelToken)
2018-05-27 13:27:24 -07:00
{
try
{
await RcloneCommandExecuter(rclone_executable, $"copyto {remote_repo}:{Path.Combine(this.remote_path, remotename)} {local_repo}:{filename}", cancelToken).ConfigureAwait(false);
2018-05-27 13:27:24 -07:00
}
catch (FolderMissingException ex)
{
2018-05-27 13:27:24 -07:00
throw new FileMissingException(ex);
}
}
2024-09-29 23:04:54 +02:00
public async Task DeleteAsync(string remotename, CancellationToken cancelToken)
2018-05-27 13:27:24 -07:00
{
//this will actually delete the folder if remotename is a folder...
// Will give a "directory not found" error if the file does not exist, need to change that to a missing file exception
try
{
2024-09-29 23:04:54 +02:00
await RcloneCommandExecuter(rclone_executable, $"delete {remote_repo}:{Path.Combine(remote_path, remotename)}", cancelToken).ConfigureAwait(false);
2018-05-27 13:27:24 -07:00
}
catch (FolderMissingException ex)
{
2018-05-27 13:27:24 -07:00
throw new FileMissingException(ex);
}
}
public IList<ICommandLineArgument> SupportedCommands
{
get
{
return new List<ICommandLineArgument>([
2018-05-27 13:27:24 -07:00
new CommandLineArgument(OPTION_LOCAL_REPO, CommandLineArgument.ArgumentType.String, Strings.Rclone.RcloneLocalRepoShort, Strings.Rclone.RcloneLocalRepoLong, "local"),
new CommandLineArgument(OPTION_REMOTE_REPO, CommandLineArgument.ArgumentType.String, Strings.Rclone.RcloneRemoteRepoShort, Strings.Rclone.RcloneRemoteRepoLong, "remote"),
new CommandLineArgument(OPTION_REMOTE_PATH, CommandLineArgument.ArgumentType.String, Strings.Rclone.RcloneRemotePathShort, Strings.Rclone.RcloneRemotePathLong, "backup"),
new CommandLineArgument(OPTION_RCLONE, CommandLineArgument.ArgumentType.String, Strings.Rclone.RcloneOptionRcloneShort, Strings.Rclone.RcloneOptionRcloneLong, ""),
new CommandLineArgument(OPTION_RCLONE_EXECUTABLE, CommandLineArgument.ArgumentType.String, Strings.Rclone.RcloneExecutableShort, Strings.Rclone.RcloneExecutableLong, "rclone")
]);
2018-05-27 13:27:24 -07:00
}
}
public string Description
{
get
{
return Strings.Rclone.Description;
}
}
2024-10-02 09:37:52 +02:00
public Task<string[]> GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(new[] { remote_repo });
2018-05-27 13:27:24 -07:00
public Task TestAsync(CancellationToken cancelToken)
2025-02-17 16:45:51 +01:00
=> this.TestListAsync(cancelToken);
2018-05-27 13:27:24 -07:00
public Task CreateFolderAsync(CancellationToken cancelToken)
2018-05-27 13:27:24 -07:00
{
return RcloneCommandExecuter(rclone_executable, $"mkdir {remote_repo}:{remote_path}", cancelToken);
2018-05-27 13:27:24 -07:00
}
#endregion
2018-05-27 13:27:24 -07:00
#region IDisposable Members
2018-05-27 13:27:24 -07:00
public void Dispose()
{
}
#endregion
2018-05-27 13:27:24 -07:00
}
}