2020-03-13 16:56:28 -04:00
|
|
|
|
using Amazon.Runtime;
|
|
|
|
|
using Amazon.S3;
|
2020-03-13 11:18:25 -04:00
|
|
|
|
using Amazon.S3.Model;
|
|
|
|
|
using PoweredSoft.Storage.Core;
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Text;
|
2020-08-20 17:28:42 -04:00
|
|
|
|
using System.Text.RegularExpressions;
|
2020-03-13 11:18:25 -04:00
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
namespace PoweredSoft.Storage.S3
|
|
|
|
|
{
|
|
|
|
|
public class S3StorageProvider : IStorageProvider
|
|
|
|
|
{
|
2020-03-13 16:13:49 -04:00
|
|
|
|
protected readonly string endpoint;
|
|
|
|
|
protected readonly string bucketName;
|
|
|
|
|
protected readonly string accessKey;
|
|
|
|
|
protected readonly string secret;
|
2020-03-13 11:18:25 -04:00
|
|
|
|
|
2020-03-13 16:57:09 -04:00
|
|
|
|
protected S3UsEast1RegionalEndpointValue? s3UsEast1RegionalEndpointValue = null;
|
2020-03-13 16:56:28 -04:00
|
|
|
|
protected bool forcePathStyle = false;
|
|
|
|
|
|
2020-03-13 11:18:25 -04:00
|
|
|
|
public S3StorageProvider(string endpoint, string bucketName, string accessKey, string secret)
|
|
|
|
|
{
|
|
|
|
|
this.endpoint = endpoint;
|
|
|
|
|
this.bucketName = bucketName;
|
|
|
|
|
this.accessKey = accessKey;
|
|
|
|
|
this.secret = secret;
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-13 16:56:28 -04:00
|
|
|
|
public void SetForcePathStyle(bool forcePathStyle)
|
|
|
|
|
{
|
|
|
|
|
this.forcePathStyle = forcePathStyle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetS3UsEast1RegionalEndpointValue(S3UsEast1RegionalEndpointValue value)
|
|
|
|
|
{
|
|
|
|
|
this.s3UsEast1RegionalEndpointValue = value;
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-13 16:13:49 -04:00
|
|
|
|
protected virtual IAmazonS3 GetClient()
|
2020-03-13 11:18:25 -04:00
|
|
|
|
{
|
|
|
|
|
var config = new AmazonS3Config
|
|
|
|
|
{
|
2020-03-13 16:56:28 -04:00
|
|
|
|
USEast1RegionalEndpointValue = s3UsEast1RegionalEndpointValue,
|
|
|
|
|
ServiceURL = endpoint,
|
|
|
|
|
ForcePathStyle = forcePathStyle
|
2020-03-13 11:18:25 -04:00
|
|
|
|
};
|
|
|
|
|
var client = new AmazonS3Client(this.accessKey, this.secret, config);
|
|
|
|
|
return client;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task<IDirectoryInfo> CreateDirectoryAsync(string path)
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult<IDirectoryInfo>(new S3NotExistingDirectoryInfo(path));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Can only delete 1000 at a time.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="path"></param>
|
|
|
|
|
/// <param name="force"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public async Task DeleteDirectoryAsync(string path, bool force = false)
|
|
|
|
|
{
|
|
|
|
|
using var client = GetClient();
|
|
|
|
|
var files = await this.GetS3FilesAsync(prefix: path, delimiter: null);
|
|
|
|
|
var next = files.AsQueryable();
|
|
|
|
|
while(next.Any())
|
|
|
|
|
{
|
|
|
|
|
var next1000 = next.Take(1000);
|
|
|
|
|
var keys = next1000.Select(s3o => new KeyVersion { Key = s3o.Key }).ToList();
|
|
|
|
|
await client.DeleteObjectsAsync(new DeleteObjectsRequest
|
|
|
|
|
{
|
|
|
|
|
BucketName = this.bucketName,
|
|
|
|
|
Objects = keys
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
next = next.Skip(1000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task DeleteFileAsync(string path)
|
|
|
|
|
{
|
|
|
|
|
using var client = GetClient();
|
|
|
|
|
var response = await client.DeleteObjectAsync(new DeleteObjectRequest
|
|
|
|
|
{
|
|
|
|
|
BucketName = this.bucketName,
|
|
|
|
|
Key = path
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<bool> FileExistsAsync(string path)
|
|
|
|
|
{
|
|
|
|
|
var item = await GetS3FileByPath(path);
|
|
|
|
|
return item != null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task<List<IDirectoryInfo>> GetDirectories(string path)
|
|
|
|
|
{
|
|
|
|
|
return Task.FromResult(new List<IDirectoryInfo>());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<byte[]> GetFileBytesAsync(string path)
|
|
|
|
|
{
|
|
|
|
|
using var fileStream = await this.GetFileStreamAsync(path);
|
|
|
|
|
using var memoryStream = new MemoryStream();
|
|
|
|
|
await fileStream.CopyToAsync(memoryStream);
|
|
|
|
|
return memoryStream.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<string> GetFileContentAsync(string path, Encoding encoding)
|
|
|
|
|
{
|
|
|
|
|
using var fileStream = await this.GetFileStreamAsync(path);
|
|
|
|
|
using var streamReader = new StreamReader(fileStream, encoding);
|
|
|
|
|
return await streamReader.ReadToEndAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<List<IFileInfo>> GetFilesAsync(string path, string pattern = null, SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
|
|
|
|
{
|
|
|
|
|
if (pattern != null)
|
|
|
|
|
throw new NotSupportedException();
|
|
|
|
|
|
|
|
|
|
string finalPath = SantizeDirectoryRequest(path);
|
|
|
|
|
var s3Files = await this.GetS3FilesAsync(prefix: finalPath, delimiter: "/");
|
|
|
|
|
var ret = s3Files.Select(s3 => new S3FileInfo(s3)).AsEnumerable<IFileInfo>().ToList();
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string SantizeDirectoryRequest(string path)
|
|
|
|
|
{
|
|
|
|
|
string finalPath;
|
|
|
|
|
if (path == "/")
|
|
|
|
|
finalPath = "";
|
|
|
|
|
else
|
|
|
|
|
finalPath = $"{path?.TrimEnd('/')}/";
|
|
|
|
|
return finalPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task<Stream> GetFileStreamAsync(string path)
|
|
|
|
|
{
|
|
|
|
|
using var client = GetClient();
|
|
|
|
|
return client.GetObjectStreamAsync(this.bucketName, path, null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected virtual async Task<IEnumerable<S3Object>> GetS3FilesAsync(string prefix = null, string delimiter = null)
|
|
|
|
|
{
|
|
|
|
|
using var client = GetClient();
|
|
|
|
|
|
|
|
|
|
var items = new List<S3Object>();
|
|
|
|
|
string nextKey = null;
|
|
|
|
|
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
var response = await client.ListObjectsV2Async(new ListObjectsV2Request
|
|
|
|
|
{
|
|
|
|
|
BucketName = this.bucketName,
|
|
|
|
|
Prefix = prefix,
|
|
|
|
|
Delimiter = delimiter,
|
|
|
|
|
MaxKeys = 1000,
|
|
|
|
|
ContinuationToken = nextKey
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
items.AddRange(response.S3Objects);
|
|
|
|
|
nextKey = response.NextContinuationToken;
|
|
|
|
|
|
|
|
|
|
} while (nextKey != null);
|
|
|
|
|
|
|
|
|
|
return items;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<List<IDirectoryOrFile>> GetListAsync(string path)
|
|
|
|
|
{
|
|
|
|
|
var files = await this.GetFilesAsync(path);
|
|
|
|
|
return files.Cast<IDirectoryOrFile>().ToList();
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-27 16:04:07 -04:00
|
|
|
|
public async Task<IFileInfo> WriteFileAsync(string sourcePath, string path, IWriteFileOptions options)
|
2020-03-13 11:18:25 -04:00
|
|
|
|
{
|
2020-10-27 16:04:07 -04:00
|
|
|
|
using var fileStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read);
|
|
|
|
|
return await WriteFileAsync(fileStream, path, options);
|
2020-03-13 11:18:25 -04:00
|
|
|
|
}
|
|
|
|
|
|
2020-10-27 16:04:07 -04:00
|
|
|
|
public Task<IFileInfo> WriteFileAsync(byte[] bytes, string path, IWriteFileOptions options)
|
2020-03-13 11:18:25 -04:00
|
|
|
|
{
|
2020-10-27 16:04:07 -04:00
|
|
|
|
return WriteFileAsync(new MemoryStream(bytes), path, options);
|
2020-03-13 11:18:25 -04:00
|
|
|
|
}
|
|
|
|
|
|
2020-10-27 16:04:07 -04:00
|
|
|
|
public async Task<IFileInfo> WriteFileAsync(Stream stream, string path, IWriteFileOptions options)
|
2020-03-13 11:18:25 -04:00
|
|
|
|
{
|
2020-10-27 16:04:07 -04:00
|
|
|
|
if (options is null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(options));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!options.OverrideIfExists && await FileExistsAsync(path))
|
2020-03-13 11:18:25 -04:00
|
|
|
|
throw new FileAlreadyExistsException(path);
|
|
|
|
|
|
|
|
|
|
using var client = GetClient();
|
|
|
|
|
var request = new PutObjectRequest
|
|
|
|
|
{
|
|
|
|
|
BucketName = this.bucketName,
|
|
|
|
|
InputStream = stream,
|
|
|
|
|
Key = path
|
|
|
|
|
};
|
|
|
|
|
|
2020-10-27 16:04:07 -04:00
|
|
|
|
if (options is IS3FileWriteOptions s3FileWriteOptions)
|
|
|
|
|
{
|
|
|
|
|
if (s3FileWriteOptions.Acl != null)
|
|
|
|
|
request.CannedACL = new S3CannedACL(s3FileWriteOptions.Acl);
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-13 11:18:25 -04:00
|
|
|
|
var result = await client.PutObjectAsync(request);
|
|
|
|
|
var file = await GetFileInfoByPath(path);
|
|
|
|
|
return file;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<S3Object> GetS3FileByPath(string path)
|
|
|
|
|
{
|
|
|
|
|
var files = await this.GetS3FilesAsync(path);
|
|
|
|
|
var s3o = files.FirstOrDefault();
|
|
|
|
|
return s3o;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<IFileInfo> GetFileInfoByPath(string path)
|
|
|
|
|
{
|
|
|
|
|
var s3o = await GetS3FileByPath(path);
|
|
|
|
|
if (s3o == null)
|
|
|
|
|
throw new FileDoesNotExistException(path);
|
|
|
|
|
|
|
|
|
|
var ret = new S3FileInfo(s3o);
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
2020-08-20 17:28:42 -04:00
|
|
|
|
|
|
|
|
|
public string SanitizeFileName(string key, string replacement)
|
|
|
|
|
{
|
2020-08-20 19:44:08 -04:00
|
|
|
|
string pattern = @"[^a-zA-Z0-9.!\-_*'()]";
|
2020-08-20 17:28:42 -04:00
|
|
|
|
string substitution = replacement;
|
|
|
|
|
string input = key;
|
|
|
|
|
RegexOptions options = RegexOptions.Multiline;
|
|
|
|
|
|
|
|
|
|
Regex regex = new Regex(pattern, options);
|
|
|
|
|
|
|
|
|
|
string result = regex.Replace(input, substitution);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool IsFileNameAllowed(string fileName)
|
|
|
|
|
{
|
2020-08-20 19:44:08 -04:00
|
|
|
|
string pattern = @"[^a-zA-Z0-9.!\-_*'()]";
|
2020-08-20 17:28:42 -04:00
|
|
|
|
RegexOptions options = RegexOptions.Multiline;
|
|
|
|
|
Regex regex = new Regex(pattern, options);
|
|
|
|
|
var hasMatches = regex.IsMatch(fileName);
|
|
|
|
|
return false == hasMatches;
|
|
|
|
|
}
|
2020-10-27 16:04:07 -04:00
|
|
|
|
|
|
|
|
|
public Task<IFileInfo> WriteFileAsync(string sourcePath, string path, bool overrideIfExists = true)
|
|
|
|
|
{
|
|
|
|
|
return WriteFileAsync(sourcePath, path, new DefaultWriteOptions
|
|
|
|
|
{
|
|
|
|
|
OverrideIfExists = overrideIfExists
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task<IFileInfo> WriteFileAsync(byte[] bytes, string path, bool overrideIfExists = true)
|
|
|
|
|
{
|
|
|
|
|
return WriteFileAsync(bytes, path, new DefaultWriteOptions
|
|
|
|
|
{
|
|
|
|
|
OverrideIfExists = overrideIfExists
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task<IFileInfo> WriteFileAsync(Stream stream, string path, bool overrideIfExists = true)
|
|
|
|
|
{
|
|
|
|
|
return WriteFileAsync(stream, path, new DefaultWriteOptions
|
|
|
|
|
{
|
|
|
|
|
OverrideIfExists = overrideIfExists
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-03-13 11:18:25 -04:00
|
|
|
|
}
|
|
|
|
|
}
|