mirror of https://github.com/Squidex/squidex.git
9 changed files with 410 additions and 2 deletions
@ -0,0 +1,219 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Security.Cryptography.X509Certificates; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver; |
||||
|
using MongoDB.Driver.GridFS; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Assets |
||||
|
{ |
||||
|
public class MongoGridFsAssetStore : IAssetStore, IInitializable |
||||
|
{ |
||||
|
private readonly string path; |
||||
|
private readonly IGridFSBucket bucket; |
||||
|
private readonly DirectoryInfo directory; |
||||
|
|
||||
|
public MongoGridFsAssetStore(IGridFSBucket bucket, string path) |
||||
|
{ |
||||
|
Guard.NotNull(bucket, nameof(bucket)); |
||||
|
Guard.NotNullOrEmpty(path, nameof(path)); |
||||
|
|
||||
|
this.bucket = bucket; |
||||
|
this.path = path; |
||||
|
|
||||
|
directory = new DirectoryInfo(path); |
||||
|
} |
||||
|
|
||||
|
public void Initialize() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
// test bucket
|
||||
|
bucket.Database.ListCollections(); |
||||
|
|
||||
|
if (!directory.Exists) |
||||
|
{ |
||||
|
directory.Create(); |
||||
|
} |
||||
|
} |
||||
|
catch (MongoException ex) |
||||
|
{ |
||||
|
throw new ConfigurationException( |
||||
|
$"Cannot connect to Mongo GridFS bucket '${bucket.Options.BucketName}'.", ex); |
||||
|
} |
||||
|
catch (IOException ex) |
||||
|
{ |
||||
|
if (!directory.Exists) |
||||
|
{ |
||||
|
throw new ConfigurationException($"Cannot access directory '{directory.FullName}'", ex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public string GenerateSourceUrl(string id, long version, string suffix) |
||||
|
{ |
||||
|
var file = GetFile(id, version, suffix); |
||||
|
|
||||
|
return file.FullName; |
||||
|
} |
||||
|
|
||||
|
public async Task CopyAsync(string name, string id, long version, string suffix, |
||||
|
CancellationToken ct = default(CancellationToken)) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var file = GetFile(name); |
||||
|
|
||||
|
using (var stream = new MemoryStream()) |
||||
|
{ |
||||
|
await bucket.DownloadToStreamByNameAsync(file.Name, stream, cancellationToken: ct); |
||||
|
await bucket.UploadFromStreamAsync(file.Name, stream, cancellationToken: ct); |
||||
|
} |
||||
|
|
||||
|
file.CopyTo(GetPath(id, version, suffix)); |
||||
|
} |
||||
|
catch (FileNotFoundException ex) |
||||
|
{ |
||||
|
throw new AssetNotFoundException($"Asset {name} not found.", ex); |
||||
|
} |
||||
|
catch (GridFSException ex) |
||||
|
{ |
||||
|
throw new AssetNotFoundException($"Asset {name} not found.", ex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task DownloadAsync(string id, long version, string suffix, Stream stream, |
||||
|
CancellationToken ct = default(CancellationToken)) |
||||
|
{ |
||||
|
var file = GetFile(id, version, suffix); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
if (file.Exists) |
||||
|
{ |
||||
|
using (var fileStream = file.OpenRead()) |
||||
|
{ |
||||
|
await fileStream.CopyToAsync(stream); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// file not found locally
|
||||
|
// read from GridFS
|
||||
|
await bucket.DownloadToStreamByNameAsync(file.Name, stream, cancellationToken: ct); |
||||
|
|
||||
|
// add to local assets
|
||||
|
using (var fileStream = file.OpenWrite()) |
||||
|
{ |
||||
|
await stream.CopyToAsync(fileStream); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
throw new AssetNotFoundException($"Asset {id}, {version} not found.", ex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Task UploadAsync(string name, Stream stream, CancellationToken ct = default(CancellationToken)) |
||||
|
=> UploadFileCoreAsync(GetFile(name), stream); |
||||
|
|
||||
|
public Task UploadAsync(string id, long version, string suffix, Stream stream, |
||||
|
CancellationToken ct = default(CancellationToken)) |
||||
|
=> UploadFileCoreAsync(GetFile(id, version, suffix), stream); |
||||
|
|
||||
|
public Task DeleteAsync(string name) |
||||
|
=> DeleteCoreAsync(GetFile(name)); |
||||
|
|
||||
|
public Task DeleteAsync(string id, long version, string suffix) |
||||
|
=> DeleteCoreAsync(GetFile(id, version, suffix)); |
||||
|
|
||||
|
private async Task DeleteCoreAsync(FileInfo file) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var filter = |
||||
|
Builders<GridFSFileInfo>.Filter.And( |
||||
|
Builders<GridFSFileInfo>.Filter.Eq(x => x.Filename, file.Name) |
||||
|
); |
||||
|
using (var cursor = await bucket.FindAsync(filter)) |
||||
|
{ |
||||
|
await cursor.ForEachAsync(fileInfo => bucket.DeleteAsync(fileInfo.Id)); |
||||
|
} |
||||
|
|
||||
|
file.Delete(); |
||||
|
} |
||||
|
catch (FileNotFoundException ex) |
||||
|
{ |
||||
|
throw new AssetNotFoundException($"Asset {file.Name} not found.", ex); |
||||
|
} |
||||
|
catch (GridFSException ex) |
||||
|
{ |
||||
|
throw new GridFSException( |
||||
|
$"Cannot delete file {file.Name} into Mongo GridFS bucket '{bucket.Options.BucketName}'.", ex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task UploadFileCoreAsync(FileInfo file, Stream stream, |
||||
|
CancellationToken ct = default(CancellationToken)) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
// upload file to GridFS first
|
||||
|
await bucket.UploadFromStreamAsync(file.Name, stream, cancellationToken: ct); |
||||
|
|
||||
|
// create file locally
|
||||
|
// even if this stage will fail, file will be recreated on the next Download call
|
||||
|
using (var fileStream = file.OpenWrite()) |
||||
|
{ |
||||
|
await stream.CopyToAsync(fileStream); |
||||
|
} |
||||
|
} |
||||
|
catch (IOException ex) |
||||
|
{ |
||||
|
throw new IOException($"Cannot write file '{file.Name}' into directory '{directory.FullName}'.", ex); |
||||
|
} |
||||
|
catch (GridFSException ex) |
||||
|
{ |
||||
|
throw new GridFSException( |
||||
|
$"Cannot upload file {file.Name} into Mongo GridFS bucket '{bucket.Options.BucketName}'.", |
||||
|
ex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private FileInfo GetFile(string id, long version, string suffix) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(id, nameof(id)); |
||||
|
|
||||
|
return GetFile(GetPath(id, version, suffix)); |
||||
|
} |
||||
|
|
||||
|
private FileInfo GetFile(string name) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(name, nameof(name)); |
||||
|
|
||||
|
return new FileInfo(GetPath(name)); |
||||
|
} |
||||
|
|
||||
|
private string GetPath(string name) |
||||
|
{ |
||||
|
return Path.Combine(directory.FullName, name); |
||||
|
} |
||||
|
|
||||
|
private string GetPath(string id, long version, string suffix) |
||||
|
{ |
||||
|
return Path.Combine(directory.FullName, |
||||
|
string.Join("_", |
||||
|
new[] { id, version.ToString(), suffix }.ToList().Where(x => !string.IsNullOrWhiteSpace(x)))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netstandard2.0</TargetFramework> |
||||
|
<RootNamespace>Squidex.Infrastructure</RootNamespace> |
||||
|
</PropertyGroup> |
||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> |
||||
|
<DebugType>full</DebugType> |
||||
|
<DebugSymbols>True</DebugSymbols> |
||||
|
</PropertyGroup> |
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="MongoDB.Driver" Version="2.5.1" /> |
||||
|
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.5.1" /> |
||||
|
<PackageReference Include="RefactoringEssentials" Version="5.6.0" /> |
||||
|
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> |
||||
|
</ItemGroup> |
||||
|
<PropertyGroup> |
||||
|
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet> |
||||
|
</PropertyGroup> |
||||
|
<ItemGroup> |
||||
|
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" /> |
||||
|
</ItemGroup> |
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> |
||||
|
</ItemGroup> |
||||
|
</Project> |
||||
@ -0,0 +1,101 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using FakeItEasy; |
||||
|
using MongoDB.Driver; |
||||
|
using MongoDB.Driver.GridFS; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Assets |
||||
|
{ |
||||
|
public class MongoGridFsAssetStoreTests : AssetStoreTests<MongoGridFsAssetStore> |
||||
|
{ |
||||
|
private readonly string testFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); |
||||
|
private readonly IGridFSBucket bucket = A.Fake<IGridFSBucket>(); |
||||
|
|
||||
|
public override MongoGridFsAssetStore CreateStore() |
||||
|
{ |
||||
|
return new MongoGridFsAssetStore(bucket, testFolder); |
||||
|
} |
||||
|
|
||||
|
public override void Dispose() |
||||
|
{ |
||||
|
if (Directory.Exists(testFolder)) |
||||
|
{ |
||||
|
Directory.Delete(testFolder, true); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_calculate_source_url() |
||||
|
{ |
||||
|
Sut.Initialize(); |
||||
|
|
||||
|
var id = Id(); |
||||
|
|
||||
|
Assert.Equal(Path.Combine(testFolder, $"{id}_1"), Sut.GenerateSourceUrl(id, 1, null)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public override Task Should_throw_exception_if_asset_to_download_is_not_found() |
||||
|
{ |
||||
|
var id = Id(); |
||||
|
|
||||
|
A.CallTo(() => |
||||
|
bucket.DownloadToStreamByNameAsync($"{id}_1_suffix", A<MemoryStream>.Ignored, null, |
||||
|
A<CancellationToken>.Ignored)) |
||||
|
.Throws<AssetNotFoundException>(); |
||||
|
|
||||
|
((IInitializable)Sut).Initialize(); |
||||
|
|
||||
|
return Assert.ThrowsAsync<AssetNotFoundException>(() => |
||||
|
Sut.DownloadAsync(id, 1, "suffix", new MemoryStream())); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_try_to_download_asset_if_is_not_found_locally() |
||||
|
{ |
||||
|
var id = Id(); |
||||
|
using (var stream = new MemoryStream()) |
||||
|
{ |
||||
|
((IInitializable)Sut).Initialize(); |
||||
|
|
||||
|
await Sut.DownloadAsync(id, 1, "suffix", stream); |
||||
|
|
||||
|
A.CallTo(() => |
||||
|
bucket.DownloadToStreamByNameAsync($"{id}_1_suffix", stream, |
||||
|
A<GridFSDownloadByNameOptions>.Ignored, |
||||
|
A<CancellationToken>.Ignored)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_try_to_upload_asset_and_save_locally() |
||||
|
{ |
||||
|
var id = Id(); |
||||
|
var file = new FileInfo(testFolder + Path.DirectorySeparatorChar + $"{id}_1_suffix"); |
||||
|
using (var stream = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 })) |
||||
|
{ |
||||
|
((IInitializable)Sut).Initialize(); |
||||
|
|
||||
|
await Sut.UploadAsync(id, 1, "suffix", stream); |
||||
|
|
||||
|
A.CallTo(() => |
||||
|
bucket.UploadFromStreamAsync($"{id}_1_suffix", stream, A<GridFSUploadOptions>.Ignored, |
||||
|
A<CancellationToken>.Ignored)) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
Assert.True(file.Exists); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue