// 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.Common.IO; using Duplicati.Library.Interface; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace Duplicati.Library.Backend { public class Idrivee2Backend : IBackend, IStreamingBackend { private static readonly string LOGTAG = Logging.Log.LogTagFromType(); static Idrivee2Backend() { } private readonly string m_prefix; private readonly string m_bucket; private IS3Client m_s3Client; public Idrivee2Backend() { } public Idrivee2Backend(string url, Dictionary options) { var uri = new Utility.Uri(url); m_bucket = uri.Host; m_prefix = uri.Path; m_prefix = m_prefix.Trim(); if (m_prefix.Length != 0) { m_prefix = Util.AppendDirSeparator(m_prefix, "/"); } string accessKeyId = null; string accessKeySecret = null; if (options.ContainsKey("auth-username")) accessKeyId = options["auth-username"]; if (options.ContainsKey("auth-password")) accessKeySecret = options["auth-password"]; if (options.ContainsKey("access_key_id")) accessKeyId = options["access_key_id"]; if (options.ContainsKey("secret_access_key")) accessKeySecret = options["secret_access_key"]; if (string.IsNullOrEmpty(accessKeyId)) throw new UserInformationException(Strings.Idrivee2Backend.NoKeyIdError, "Idrivee2NoKeyId"); if (string.IsNullOrEmpty(accessKeySecret)) throw new UserInformationException(Strings.Idrivee2Backend.NoKeySecretError, "Idrivee2NoKeySecret"); string host= GetRegionEndpoint("https://api.idrivee2.com/api/service/get_region_end_point/" + accessKeyId); m_s3Client = new S3AwsClient(accessKeyId, accessKeySecret, null, host, null, true, false, options); } public string GetRegionEndpoint(string url) { try { System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(url); req.Method = System.Net.WebRequestMethods.Http.Get; Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req); using (System.Net.HttpWebResponse resp = (System.Net.HttpWebResponse)areq.GetResponse()) { int code = (int)resp.StatusCode; if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically throw new Exception("Failed to fetch region endpoint"); using (var s = areq.GetResponseStream()) { using (var reader = new StreamReader(s)) { string endpoint = reader.ReadToEnd(); return endpoint; } } } } catch (System.Net.WebException wex) { //Convert to better exception throw new Exception("Failed to fetch region endpoint"); } } #region IBackend Members public string DisplayName { get { return Strings.Idrivee2Backend.DisplayName; } } public string ProtocolKey => "e2"; public bool SupportsStreaming => true; public IEnumerable List() { foreach (IFileEntry file in Connection.ListBucket(m_bucket, m_prefix)) { ((FileEntry)file).Name = file.Name.Substring(m_prefix.Length); if (file.Name.StartsWith("/", StringComparison.Ordinal) && !m_prefix.StartsWith("/", StringComparison.Ordinal)) ((FileEntry)file).Name = file.Name.Substring(1); yield return file; } } public async Task PutAsync(string remotename, string localname, CancellationToken cancelToken) { using (FileStream fs = File.Open(localname, FileMode.Open, FileAccess.Read, FileShare.Read)) await PutAsync(remotename, fs, cancelToken); } public async Task PutAsync(string remotename, Stream input, CancellationToken cancelToken) { await Connection.AddFileStreamAsync(m_bucket, GetFullKey(remotename), input, cancelToken); } public void Get(string remotename, string localname) { using (var fs = File.Open(localname, FileMode.Create, FileAccess.Write, FileShare.None)) Get(remotename, fs); } public void Get(string remotename, Stream output) { Connection.GetFileStream(m_bucket, GetFullKey(remotename), output); } public void Delete(string remotename) { Connection.DeleteObject(m_bucket, GetFullKey(remotename)); } public IList SupportedCommands { get { var defaults = new Amazon.S3.AmazonS3Config() { // If this is not set, accessing the property will trigger an expensive operation (~30 seconds) // to get the region endpoint. This stalls the supported commands list. UseArnRegion = false }; var exts = typeof(Amazon.S3.AmazonS3Config).GetProperties().Where(x => x.CanRead && x.CanWrite && (x.PropertyType == typeof(string) || x.PropertyType == typeof(bool) || x.PropertyType == typeof(int) || x.PropertyType == typeof(long) || x.PropertyType.IsEnum)) .Select(x => (ICommandLineArgument)new CommandLineArgument( "s3-ext-" + x.Name.ToLowerInvariant(), x.PropertyType == typeof(bool) ? CommandLineArgument.ArgumentType.Boolean : x.PropertyType.IsEnum ? CommandLineArgument.ArgumentType.Enumeration : CommandLineArgument.ArgumentType.String, x.Name, string.Format("Extended option {0}", x.Name), string.Format("{0}", x.GetValue(defaults)), null, x.PropertyType.IsEnum ? Enum.GetNames(x.PropertyType) : null)); var normal = new ICommandLineArgument[] { new CommandLineArgument("access_key_secret", CommandLineArgument.ArgumentType.Password, Strings.Idrivee2Backend.KeySecretDescriptionShort, Strings.Idrivee2Backend.KeySecretDescriptionLong, null, new[]{"auth-password"}, null), new CommandLineArgument("access_key_id", CommandLineArgument.ArgumentType.String, Strings.Idrivee2Backend.KeyIDDescriptionShort, Strings.Idrivee2Backend.KeyIDDescriptionLong,null, new[]{"auth-username"}, null) }; return normal.Union(exts).ToList(); } } public string Description { get { return Strings.Idrivee2Backend.Description; } } public void Test() { this.TestList(); } public void CreateFolder() { //S3 does not complain if the bucket already exists Connection.AddBucket(m_bucket); } #endregion #region IRenameEnabledBackend Members public void Rename(string source, string target) { Connection.RenameFile(m_bucket, GetFullKey(source), GetFullKey(target)); } #endregion #region IDisposable Members public void Dispose() { m_s3Client?.Dispose(); m_s3Client = null; } #endregion private IS3Client Connection => m_s3Client; public string[] DNSName { get { return new[] { m_s3Client.GetDnsHost() }; } } private string GetFullKey(string name) { //AWS SDK encodes the filenames correctly return m_prefix + name; } } }