2025-01-07 09:40:39 +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.
using Aliyun.OSS ;
2024-02-23 11:12:41 +08:00
using Aliyun.OSS.Common ;
using Duplicati.Library.Common.IO ;
using Duplicati.Library.Interface ;
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Net ;
2025-02-17 16:45:51 +01:00
using System.Runtime.CompilerServices ;
2024-02-23 11:12:41 +08:00
using System.Threading ;
using System.Threading.Tasks ;
namespace Duplicati.Library.Backend.AliyunOSS
{
/// <summary>
/// Aliyun Object Storage Service(OSS) is a massive, secure, low-cost, and highly reliable cloud storage service, offering up to 99.995% service availability.It offers a variety of storage types to choose from, comprehensively optimizing storage costs.
2025-01-03 14:08:29 +01:00
/// en: https://www.alibabacloud.com/en/product/object-storage-service
2024-02-23 11:12:41 +08:00
/// zh: https://www.aliyun.com/product/oss
/// </summary>
public class OSS : IBackend , IStreamingBackend , IRenameEnabledBackend
{
private static readonly string LOGTAG = Logging . Log . LogTagFromType < OSS >();
private const string OSS_BUCKET_NAME = "oss-bucket-name" ;
private const string OSS_ENDPOINT = "oss-endpoint" ;
private const string OSS_ACCESS_KEY_ID = "oss-access-key-id" ;
private const string OSS_ACCESS_KEY_SECRET = "oss-access-key-secret" ;
private AliyunOSSOptions _ossOptions ;
public OSS ()
{ }
public OSS ( string url , Dictionary < string , string > options )
{
_ossOptions = new AliyunOSSOptions ();
var uri = new Utility . Uri ( url ?. Trim ());
2024-02-23 17:54:47 +08:00
var prefix = uri . HostAndPath ?. TrimPath ();
2024-02-23 11:12:41 +08:00
if (! string . IsNullOrEmpty ( prefix ))
{
_ossOptions . Path = prefix ;
}
if ( options . ContainsKey ( OSS_ACCESS_KEY_ID ))
{
_ossOptions . AccessKeyId = options [ OSS_ACCESS_KEY_ID ];
}
if ( options . ContainsKey ( OSS_ACCESS_KEY_SECRET ))
{
_ossOptions . AccessKeySecret = options [ OSS_ACCESS_KEY_SECRET ];
}
if ( options . ContainsKey ( OSS_BUCKET_NAME ))
{
_ossOptions . BucketName = options [ OSS_BUCKET_NAME ];
}
if ( options . ContainsKey ( OSS_ENDPOINT ))
{
_ossOptions . Endpoint = options [ OSS_ENDPOINT ];
}
}
2024-02-23 17:54:47 +08:00
private OssClient GetClient ( bool isUseNewServiceClient = true )
2024-02-23 11:12:41 +08:00
{
var endpoint = _ossOptions . Endpoint ;
var accessKeyId = _ossOptions . AccessKeyId ;
var accessKeySecret = _ossOptions . AccessKeySecret ;
2024-02-23 17:54:47 +08:00
if ( isUseNewServiceClient )
{
return new OssClient ( endpoint , accessKeyId , accessKeySecret );
}
2024-02-23 11:12:41 +08:00
return new OssClient ( endpoint , accessKeyId , accessKeySecret , new ClientConfiguration ()
{
UseNewServiceClient = false
});
}
2025-02-17 16:45:51 +01:00
public async IAsyncEnumerable < IFileEntry > ListAsync ([ EnumeratorCancellation ] CancellationToken cancelToken )
2024-02-23 11:12:41 +08:00
{
var bucketName = _ossOptions . BucketName ;
var prefix = $"{_ossOptions.Path.TrimPath()}" ;
2024-02-23 17:54:47 +08:00
var client = GetClient ( false );
2024-02-23 11:12:41 +08:00
2025-02-17 16:45:51 +01:00
var nextMarker = string . Empty ;
bool isTruncated ;
do
2024-02-23 11:12:41 +08:00
{
2025-02-17 16:45:51 +01:00
var listObjectsRequest = new ListObjectsRequest ( bucketName )
2024-02-23 11:12:41 +08:00
{
2025-02-17 16:45:51 +01:00
MaxKeys = 1000 ,
Marker = nextMarker ,
Prefix = prefix ,
};
ObjectListing result ;
try
{
result = await Task . Run (() => client . ListObjects ( listObjectsRequest ), cancelToken ). ConfigureAwait ( ConfigureAwaitOptions . ForceYielding );
2024-02-23 17:54:47 +08:00
if ( result . HttpStatusCode != HttpStatusCode . OK )
2024-02-23 11:12:41 +08:00
{
throw new Exception ( result . HttpStatusCode . ToString ());
}
2025-02-17 16:45:51 +01:00
}
catch ( OssException ex )
{
Logging . Log . WriteErrorMessage ( LOGTAG , "List" , ex , "List object failed. {0}, {1}" , ex . Message , ex . ErrorCode );
throw ;
}
catch ( Exception ex )
{
Logging . Log . WriteErrorMessage ( LOGTAG , "List" , ex , "List object failed. {0}" , ex . Message );
throw ;
}
2024-02-23 11:12:41 +08:00
2025-02-17 16:45:51 +01:00
foreach ( var summary in result . ObjectSummaries )
{
var fileName = Path . GetFileName ( summary . Key );
var time = summary . LastModified ; // DateTimeOffset.Parse(summary.LastModified).ToLocalTime().DateTime;
yield return new FileEntry ( fileName , summary . Size , time , time );
}
nextMarker = result . NextMarker ;
isTruncated = result . IsTruncated ;
} while ( isTruncated );
2024-02-23 11:12:41 +08:00
}
public async Task PutAsync ( string remotename , string filename , CancellationToken cancelToken )
{
using ( FileStream fs = File . Open ( filename , FileMode . Open , FileAccess . Read , FileShare . Read ))
2024-09-29 22:00:57 +02:00
await PutAsync ( remotename , fs , cancelToken ). ConfigureAwait ( false );
2024-02-23 11:12:41 +08:00
}
2024-09-29 22:00:57 +02:00
public async Task GetAsync ( string remotename , string filename , CancellationToken cancelToken )
2024-02-23 11:12:41 +08:00
{
using ( var fs = File . Open ( filename , FileMode . Create , FileAccess . Write , FileShare . None ))
2024-09-29 22:00:57 +02:00
await GetAsync ( remotename , fs , cancelToken ). ConfigureAwait ( false );
2024-02-23 11:12:41 +08:00
}
2024-09-29 23:04:54 +02:00
public Task DeleteAsync ( string remotename , CancellationToken cancelToken )
2024-02-23 11:12:41 +08:00
{
try
{
var client = GetClient ();
var bucketName = _ossOptions . BucketName ;
var objectName = $"{_ossOptions.Path.TrimPath()}/{remotename}" . TrimPath ();
var res = client . DeleteObject ( bucketName , objectName );
if ( res ?. HttpStatusCode != HttpStatusCode . OK )
{
2024-02-23 17:54:47 +08:00
Logging . Log . WriteInformationMessage ( LOGTAG , "Delete" , "Delete object failed. it may have been deleted" );
2024-02-23 11:12:41 +08:00
}
}
catch ( Exception ex )
{
Logging . Log . WriteErrorMessage ( LOGTAG , "Delete" , ex , "Delete object failed. {0}" , ex . Message );
throw ;
}
2024-09-29 23:04:54 +02:00
return Task . CompletedTask ;
2024-02-23 11:12:41 +08:00
}
2024-10-01 09:30:02 +02:00
public Task TestAsync ( CancellationToken cancelToken )
2024-02-23 11:12:41 +08:00
{
GetClient ();
2024-10-01 09:30:02 +02:00
return Task . CompletedTask ;
2024-02-23 11:12:41 +08:00
}
2024-10-01 09:30:02 +02:00
public Task CreateFolderAsync ( CancellationToken cancelToken )
2024-02-23 11:12:41 +08:00
// No need to create folders
2024-10-01 09:30:02 +02:00
=> Task . CompletedTask ;
2024-02-23 11:12:41 +08:00
public void Dispose ()
{
}
2024-09-29 22:00:57 +02:00
public async Task PutAsync ( string remotename , Stream stream , CancellationToken cancelToken )
2024-02-23 11:12:41 +08:00
{
var bucketName = _ossOptions . BucketName ;
var objectName = $"{_ossOptions.Path.TrimPath()}/{remotename}" . TrimPath ();
var client = GetClient ();
try
{
2024-09-29 22:00:57 +02:00
var objectResult = await Task . Factory . FromAsync (
( cb , state ) => client . BeginPutObject ( bucketName , objectName , stream , cb , state ),
client . EndPutObject ,
null ). ConfigureAwait ( false );
2024-02-23 11:12:41 +08:00
if ( objectResult ?. HttpStatusCode != HttpStatusCode . OK )
throw new Exception ( "Put object failed" );
}
catch ( Exception ex )
{
throw new Exception ( $"Put object failed, {ex.Message}" );
}
}
2024-09-29 22:00:57 +02:00
public async Task GetAsync ( string remotename , Stream stream , CancellationToken cancelToken )
2024-02-23 11:12:41 +08:00
{
var bucketName = _ossOptions . BucketName ;
var objectName = $"{_ossOptions.Path.TrimPath()}/{remotename}" . TrimPath ();
2024-02-23 17:54:47 +08:00
var client = GetClient ( false );
2024-02-23 11:12:41 +08:00
try
{
2024-09-29 22:00:57 +02:00
var obj = await Task . Factory . FromAsync (
( cb , state ) => client . BeginGetObject ( bucketName , objectName , null , null ),
client . EndGetObject ,
null ). ConfigureAwait ( false );
2024-02-23 11:12:41 +08:00
if ( obj . HttpStatusCode != HttpStatusCode . OK )
throw new Exception ( "Get failed" );
using ( var requestStream = obj . Content )
2024-09-29 22:00:57 +02:00
await Library . Utility . Utility . CopyStreamAsync ( requestStream , stream , cancelToken ). ConfigureAwait ( false );
2024-02-23 11:12:41 +08:00
}
catch ( Exception ex )
{
Logging . Log . WriteErrorMessage ( LOGTAG , "Get" , ex , "Get failed: {0}" , ex . Message );
throw ;
}
}
2024-09-29 23:21:50 +02:00
public async Task RenameAsync ( string oldname , string newname , CancellationToken cancelToken )
2024-02-23 11:12:41 +08:00
{
var bucketName = _ossOptions . BucketName ;
var sourceBucket = bucketName ;
var targetBucket = bucketName ;
var sourceObject = $"{_ossOptions.Path.TrimPath()}/{oldname}" . TrimPath ();
var targetObject = $"{_ossOptions.Path.TrimPath()}/{newname}" . TrimPath ();
var client = GetClient ();
try
{
// copy file
var req = new CopyObjectRequest ( sourceBucket , sourceObject , targetBucket , targetObject );
2024-09-29 23:21:50 +02:00
var res = await Task . Factory . FromAsync (
( cb , state ) => client . BeginCopyObject ( req , cb , state ),
client . EndCopyResult ,
null ). ConfigureAwait ( false );
2024-02-23 11:12:41 +08:00
if ( res ?. HttpStatusCode != HttpStatusCode . OK )
{
throw new Exception ( "file rename failed" );
}
// del old file
2024-09-29 23:21:50 +02:00
await DeleteAsync ( oldname , cancelToken ). ConfigureAwait ( false );
2024-02-23 11:12:41 +08:00
}
catch ( OssException ex )
{
Logging . Log . WriteErrorMessage ( LOGTAG , "Rename" , ex , "Rename failed: {0} to {1}, {2}" , oldname , newname , ex . Message );
throw ;
}
catch ( Exception ex )
{
Logging . Log . WriteErrorMessage ( LOGTAG , "Rename" , ex , "Rename failed: {0} to {1}, {2}" , oldname , newname , ex . Message );
throw ;
}
}
public string DisplayName => Strings . OSSBackend . DisplayName ;
public string Description => Strings . OSSBackend . Description ;
public string ProtocolKey => "aliyunoss" ;
public IList < ICommandLineArgument > SupportedCommands
{
get
{
2025-01-03 14:08:29 +01:00
return new List < ICommandLineArgument >([
2024-02-23 11:12:41 +08:00
new CommandLineArgument ( OSS_ACCESS_KEY_ID , CommandLineArgument . ArgumentType . String , Strings . OSSBackend . OSSAccessKeyIdDescriptionShort , Strings . OSSBackend . OSSAccessKeyIdDescriptionLong ),
new CommandLineArgument ( OSS_ACCESS_KEY_SECRET , CommandLineArgument . ArgumentType . Password , Strings . OSSBackend . OSSAccessKeySecretDescriptionShort , Strings . OSSBackend . OSSAccessKeySecretDescriptionLong ),
new CommandLineArgument ( OSS_BUCKET_NAME , CommandLineArgument . ArgumentType . String , Strings . OSSBackend . OSSBucketNameDescriptionShort , Strings . OSSBackend . OSSBucketNameDescriptionLong ),
new CommandLineArgument ( OSS_ENDPOINT , CommandLineArgument . ArgumentType . String , Strings . OSSBackend . OSSEndpointDescriptionShort , Strings . OSSBackend . OSSEndpointDescriptionLong )
2025-01-03 14:08:29 +01:00
]);
2024-02-23 11:12:41 +08:00
}
}
2024-10-02 09:37:52 +02:00
public Task < string []> GetDNSNamesAsync ( CancellationToken cancelToken ) => Task . FromResult ( Array . Empty < string >());
2024-02-23 11:12:41 +08:00
}
}