Browse Source

Add Mongo GridFS support

pull/296/head
razims 8 years ago
parent
commit
9e4b6e5d4a
  1. 15
      Squidex.sln
  2. 219
      src/Squidex.Infrastructure.MongoGridFs/Assets/MongoGridFsAssetStore.cs
  3. 25
      src/Squidex.Infrastructure.MongoGridFs/Squidex.Infrastructure.MongoGridFs.csproj
  4. 25
      src/Squidex/Config/Domain/AssetServices.cs
  5. 2
      src/Squidex/Squidex.csproj
  6. 20
      src/Squidex/appsettings.json
  7. 4
      tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs
  8. 101
      tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs
  9. 1
      tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj

15
Squidex.sln

@ -63,6 +63,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrate_01", "tools\Migrate
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Tests", "tests\Squidex.Tests\Squidex.Tests.csproj", "{7E8CC864-4C6E-496F-A672-9F9AD8874835}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Infrastructure.MongoGridFs", "src\Squidex.Infrastructure.MongoGridFs\Squidex.Infrastructure.MongoGridFs.csproj", "{2498B95C-0A55-4352-B32F-0860BE08317F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -321,6 +323,18 @@ Global
{7E8CC864-4C6E-496F-A672-9F9AD8874835}.Release|x64.Build.0 = Release|Any CPU
{7E8CC864-4C6E-496F-A672-9F9AD8874835}.Release|x86.ActiveCfg = Release|Any CPU
{7E8CC864-4C6E-496F-A672-9F9AD8874835}.Release|x86.Build.0 = Release|Any CPU
{2498B95C-0A55-4352-B32F-0860BE08317F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2498B95C-0A55-4352-B32F-0860BE08317F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2498B95C-0A55-4352-B32F-0860BE08317F}.Debug|x64.ActiveCfg = Debug|Any CPU
{2498B95C-0A55-4352-B32F-0860BE08317F}.Debug|x64.Build.0 = Debug|Any CPU
{2498B95C-0A55-4352-B32F-0860BE08317F}.Debug|x86.ActiveCfg = Debug|Any CPU
{2498B95C-0A55-4352-B32F-0860BE08317F}.Debug|x86.Build.0 = Debug|Any CPU
{2498B95C-0A55-4352-B32F-0860BE08317F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2498B95C-0A55-4352-B32F-0860BE08317F}.Release|Any CPU.Build.0 = Release|Any CPU
{2498B95C-0A55-4352-B32F-0860BE08317F}.Release|x64.ActiveCfg = Release|Any CPU
{2498B95C-0A55-4352-B32F-0860BE08317F}.Release|x64.Build.0 = Release|Any CPU
{2498B95C-0A55-4352-B32F-0860BE08317F}.Release|x86.ActiveCfg = Release|Any CPU
{2498B95C-0A55-4352-B32F-0860BE08317F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -349,6 +363,7 @@ Global
{AA003372-CD8D-4DBC-962C-F61E0C93CF05} = {C9809D59-6665-471E-AD87-5AC624C65892}
{7DA5B308-D950-4496-93D5-21D6C4D91644} = {C9809D59-6665-471E-AD87-5AC624C65892}
{A4823E14-C0E5-4A4D-B28F-27424C25C3C7} = {94207AA6-4923-4183-A558-E0F8196B8CA3}
{2498B95C-0A55-4352-B32F-0860BE08317F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08}

219
src/Squidex.Infrastructure.MongoGridFs/Assets/MongoGridFsAssetStore.cs

@ -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))));
}
}
}

25
src/Squidex.Infrastructure.MongoGridFs/Squidex.Infrastructure.MongoGridFs.csproj

@ -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>

25
src/Squidex/Config/Domain/AssetServices.cs

@ -7,6 +7,8 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
using MongoDB.Driver.GridFS;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Assets.ImageSharp;
@ -44,6 +46,29 @@ namespace Squidex.Config.Domain
services.AddSingletonAs(c => new AzureBlobAssetStore(connectionString, containerName))
.As<IAssetStore>()
.As<IInitializable>();
},
["MongoDb"] = () =>
{
var connectionString = config.GetRequiredValue("assetStore:mongoDb:connectionString");
var mongoDatabaseName = config.GetRequiredValue("assetStore:mongoDb:database");
var mongoGridFsBucketName = config.GetRequiredValue("assetStore:mongoDb:bucket");
var localPath = config.GetRequiredValue("assetStore:mongoDb:path");
services.AddSingletonAs(c =>
{
var mongoClient = Singletons<IMongoClient>.GetOrAdd(connectionString, s => new MongoClient(s));
var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName);
var gridFsbucket = new GridFSBucket(mongoDatabase, new GridFSBucketOptions()
{
BucketName = mongoGridFsBucketName,
ChunkSizeBytes = 255 * 1024
// Defaults to 255KB, provisionary set here to avoid future changes in default values
});
return new MongoGridFsAssetStore(gridFsbucket, localPath);
})
.As<IAssetStore>()
.As<IInitializable>();
}
});

2
src/Squidex/Squidex.csproj

@ -44,6 +44,7 @@
<ProjectReference Include="..\Squidex.Infrastructure.Azure\Squidex.Infrastructure.Azure.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.GetEventStore\Squidex.Infrastructure.GetEventStore.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.GoogleCloud\Squidex.Infrastructure.GoogleCloud.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.MongoGridFs\Squidex.Infrastructure.MongoGridFs.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.RabbitMq\Squidex.Infrastructure.RabbitMq.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.MongoDb\Squidex.Infrastructure.MongoDb.csproj" />
@ -76,6 +77,7 @@
<PackageReference Include="Microsoft.Orleans.Core.Abstractions" Version="2.0.0-ci.20180515.5" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.0.0-ci.20180515.5" />
<PackageReference Include="MongoDB.Driver" Version="2.5.1" />
<PackageReference Include="MongoDB.Driver.GridFS" Version="2.5.1" />
<PackageReference Include="NJsonSchema" Version="9.10.35" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0" />
<PackageReference Include="NSwag.AspNetCore" Version="11.15.4" />

20
src/Squidex/appsettings.json

@ -85,6 +85,26 @@
*/
"connectionString": "UseDevelopmentStorage=true"
},
"mongoDb": {
/*
* The connection string to your Mongo Server.
*
* Read More: https://docs.mongodb.com/manual/reference/connection-string/
*/
"connectionString": "mongodb://localhost",
/*
* The name of the event store database.
*/
"database": "SquidexAssets",
/*
* The name of the Mongo Grid FS bucket.
*/
"bucket": "fs",
/*
* The relative or absolute path to the folder to store the local copy of assets.
*/
"path": "Assets"
},
/*
* Allow to expose the url in graph ql url.
*/

4
tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs

@ -31,7 +31,7 @@ namespace Squidex.Infrastructure.Assets
public abstract void Dispose();
[Fact]
public Task Should_throw_exception_if_asset_to_download_is_not_found()
public virtual Task Should_throw_exception_if_asset_to_download_is_not_found()
{
((IInitializable)Sut).Initialize();
@ -111,7 +111,7 @@ namespace Squidex.Infrastructure.Assets
await Sut.DeleteAsync(tempId, 0, null);
}
private static string Id()
protected static string Id()
{
return Guid.NewGuid().ToString();
}

101
tests/Squidex.Infrastructure.Tests/Assets/MongoGridFsAssetStoreTests.cs

@ -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);
}
}
}
}

1
tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj

@ -8,6 +8,7 @@
<ProjectReference Include="..\..\src\Squidex.Infrastructure.Azure\Squidex.Infrastructure.Azure.csproj" />
<ProjectReference Include="..\..\src\Squidex.Infrastructure.GoogleCloud\Squidex.Infrastructure.GoogleCloud.csproj" />
<ProjectReference Include="..\..\src\Squidex.Infrastructure.MongoDb\Squidex.Infrastructure.MongoDb.csproj" />
<ProjectReference Include="..\..\src\Squidex.Infrastructure.MongoGridFs\Squidex.Infrastructure.MongoGridFs.csproj" />
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>

Loading…
Cancel
Save