Browse Source

File name slug.

pull/352/head
Sebastian Stehle 7 years ago
parent
commit
48864adbfe
  1. 2
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs
  2. 4
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
  3. 35
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  4. 23
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
  5. 22
      src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs
  6. 2
      src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  7. 15
      src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
  8. 8
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs
  9. 5
      src/Squidex.Infrastructure/StringExtensions.cs
  10. 14
      src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  11. 6
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  12. 3
      src/Squidex/Config/Domain/EntitiesServices.cs
  13. 2
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs
  14. 7
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs
  15. 6
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  16. 1
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  17. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs
  18. 8
      tools/Migrate_01/MigrationPath.cs
  19. 36
      tools/Migrate_01/Migrations/CreateAssetSlugs.cs

2
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/IAssetInfo.cs

@ -22,5 +22,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
int? PixelHeight { get; }
string FileName { get; }
string FileNameSlug { get; }
}
}

4
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs

@ -38,6 +38,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
[BsonElement]
public string FileName { get; set; }
[BsonIgnoreIfDefault]
[BsonElement]
public string FileNameSlug { get; set; }
[BsonRequired]
[BsonElement]
public long FileSize { get; set; }

35
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -35,15 +35,20 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
protected override Task SetupCollectionAsync(IMongoCollection<MongoAssetEntity> collection, CancellationToken ct = default)
{
return collection.Indexes.CreateOneAsync(
new CreateIndexModel<MongoAssetEntity>(
Index
.Ascending(x => x.AppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.FileName)
.Ascending(x => x.Tags)
.Descending(x => x.LastModified)),
cancellationToken: ct);
return collection.Indexes.CreateManyAsync(
new[]
{
new CreateIndexModel<MongoAssetEntity>(
Index
.Ascending(x => x.AppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.FileName)
.Ascending(x => x.Tags)
.Descending(x => x.LastModified)),
new CreateIndexModel<MongoAssetEntity>(
Index.Ascending(x => x.FileNameSlug))
},
ct);
}
public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, Query query)
@ -97,6 +102,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
}
}
public async Task<IAssetEntity> FindAssetAsync(string slug)
{
using (Profiler.TraceMethod<MongoAssetRepository>())
{
var assetEntity =
await Collection.Find(x => x.FileNameSlug == slug)
.FirstOrDefaultAsync();
return assetEntity;
}
}
public async Task<IAssetEntity> FindAssetAsync(Guid id)
{
using (Profiler.TraceMethod<MongoAssetRepository>())

23
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs

@ -8,10 +8,12 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
@ -29,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
if (existing != null)
{
return (SimpleMapper.Map(existing, new AssetState()), existing.Version);
return (Map(existing), existing.Version);
}
return (null, EtagVersion.NotFound);
@ -49,14 +51,25 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
}
}
Task ISnapshotStore<AssetState, Guid>.ReadAllAsync(Func<AssetState, long, Task> callback, CancellationToken ct)
async Task ISnapshotStore<AssetState, Guid>.ReadAllAsync(Func<AssetState, long, Task> callback, CancellationToken ct)
{
throw new NotSupportedException();
using (Profiler.TraceMethod<MongoAssetRepository>())
{
await Collection.Find(new BsonDocument()).ForEachPipelineAsync(x => callback(Map(x), x.Version), ct);
}
}
async Task ISnapshotStore<AssetState, Guid>.RemoveAsync(Guid key)
{
using (Profiler.TraceMethod<MongoAssetRepository>())
{
await Collection.DeleteOneAsync(x => x.Id == key);
}
}
Task ISnapshotStore<AssetState, Guid>.RemoveAsync(Guid key)
private static AssetState Map(MongoAssetEntity existing)
{
throw new NotSupportedException();
return SimpleMapper.Map(existing, new AssetState());
}
}
}

22
src/Squidex.Domain.Apps.Entities/Assets/AssetSlug.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Assets
{
public static class AssetSlug
{
private static readonly HashSet<char> Dot = new HashSet<char>(new[] { '.' });
public static string ToAssetSlug(this string value)
{
return value.Slugify(Dot);
}
}
}

2
src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs

@ -19,6 +19,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids);
Task<IAssetEntity> FindAssetAsync(string slug);
Task<IAssetEntity> FindAssetAsync(Guid id);
Task RemoveAsync(Guid appId);

15
src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs

@ -29,6 +29,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
[DataMember]
public string MimeType { get; set; }
[DataMember]
public string FileNameSlug { get; set; }
[DataMember]
public long FileVersion { get; set; }
@ -62,6 +65,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
{
SimpleMapper.Map(@event, this);
FileName = @event.FileName;
FileNameSlug = @event.FileName.ToAssetSlug();
TotalSize += @event.FileSize;
}
@ -72,14 +78,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
TotalSize += @event.FileSize;
}
protected void On(AssetTagged @event)
protected void On(AssetRenamed @event)
{
Tags = @event.Tags;
FileName = @event.FileName;
FileNameSlug = @event.FileName.ToAssetSlug();
}
protected void On(AssetRenamed @event)
protected void On(AssetTagged @event)
{
FileName = @event.FileName;
Tags = @event.Tags;
}
protected void On(AssetDeleted @event)

8
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs

@ -99,6 +99,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = "The file name."
});
AddField(new FieldType
{
Name = "fileNameSlug",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.FileNameSlug),
Description = "The file name as slug."
});
AddField(new FieldType
{
Name = "fileType",

5
src/Squidex.Infrastructure/StringExtensions.cs

@ -510,6 +510,11 @@ namespace Squidex.Infrastructure
public static string Slugify(this string value, ISet<char> preserveHash = null, bool singleCharDiactric = false, char separator = '-')
{
if (value == null)
{
return null;
}
var result = new StringBuilder(value.Length);
var lastChar = (char)0;

14
src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -10,6 +10,7 @@ using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
@ -61,14 +62,23 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Route("assets/{id}/{*more}")]
[ProducesResponseType(typeof(FileResult), 200)]
[ApiCosts(0.5)]
public async Task<IActionResult> GetAssetContent(Guid id, string more,
public async Task<IActionResult> GetAssetContent(string id, string more,
[FromQuery] long version = EtagVersion.Any,
[FromQuery] int? width = null,
[FromQuery] int? height = null,
[FromQuery] int? quality = null,
[FromQuery] string mode = null)
{
var entity = await assetRepository.FindAssetAsync(id);
IAssetEntity entity;
if (Guid.TryParse(id, out var guid))
{
entity = await assetRepository.FindAssetAsync(guid);
}
else
{
entity = await assetRepository.FindAssetAsync(id);
}
if (entity == null || entity.FileVersion < version || width == 0 || height == 0 || quality == 0)
{

6
src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs

@ -29,6 +29,12 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
[Required]
public string FileName { get; set; }
/// <summary>
/// The file name as a slug.
/// </summary>
[Required]
public string FileNameSlug { get; set; }
/// <summary>
/// The mime type.
/// </summary>

3
src/Squidex/Config/Domain/EntitiesServices.cs

@ -268,6 +268,9 @@ namespace Squidex.Config.Domain
services.AddTransientAs<ClearSchemas>()
.As<IMigration>();
services.AddTransientAs<CreateAssetSlugs>()
.As<IMigration>();
services.AddTransientAs<PopulateGrainIndexes>()
.As<IMigration>();

2
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs

@ -28,6 +28,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
public string FileName { get; set; }
public string FileNameSlug { get; set; }
public long FileSize { get; set; }
public bool IsImage { get; set; }

7
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs

@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
[Fact]
public async Task Rename_should_create_events()
{
var command = new RenameAsset { FileName = "my-new-image.png" };
var command = new RenameAsset { FileName = "My New Image.png" };
await ExecuteCreateAsync();
@ -119,11 +119,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.Equal("my-new-image.png", sut.Snapshot.FileName);
Assert.Equal("My New Image.png", sut.Snapshot.FileName);
Assert.Equal("my-new-image.png", sut.Snapshot.FileNameSlug);
LastEvents
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetRenamed { FileName = "my-new-image.png" })
CreateAssetEvent(new AssetRenamed { FileName = "My New Image.png" })
);
}

6
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

@ -51,6 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl
mimeType
fileName
fileNameSlug
fileSize
fileVersion
isImage
@ -85,6 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl = $"assets/source/{asset.Id}",
mimeType = "image/png",
fileName = "MyFile.png",
fileNameSlug = "myfile.png",
fileSize = 1024,
fileVersion = 123,
isImage = true,
@ -117,6 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl
mimeType
fileName
fileNameSlug
fileSize
fileVersion
isImage
@ -155,6 +158,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl = $"assets/source/{asset.Id}",
mimeType = "image/png",
fileName = "MyFile.png",
fileNameSlug = "myfile.png",
fileSize = 1024,
fileVersion = 123,
isImage = true,
@ -189,6 +193,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl
mimeType
fileName
fileNameSlug
fileSize
fileVersion
isImage
@ -219,6 +224,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sourceUrl = $"assets/source/{asset.Id}",
mimeType = "image/png",
fileName = "MyFile.png",
fileNameSlug = "myfile.png",
fileSize = 1024,
fileVersion = 123,
isImage = true,

1
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -170,6 +170,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
LastModified = now,
LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"),
FileName = "MyFile.png",
FileNameSlug = "myfile.png",
FileSize = 1024,
FileVersion = 123,
MimeType = "image/png",

2
tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs

@ -37,6 +37,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData
public string FileName { get; set; }
public string FileNameSlug { get; set; }
public long FileSize { get; set; }
public long FileVersion { get; set; }

8
tools/Migrate_01/MigrationPath.cs

@ -17,7 +17,7 @@ namespace Migrate_01
{
public sealed class MigrationPath : IMigrationPath
{
private const int CurrentVersion = 15;
private const int CurrentVersion = 16;
private readonly IServiceProvider serviceProvider;
public MigrationPath(IServiceProvider serviceProvider)
@ -103,6 +103,12 @@ namespace Migrate_01
yield return serviceProvider.GetService<RestructureContentCollection>();
}
// Version 16: Introduce file name slugs for assets.
if (version < 16)
{
yield return serviceProvider.GetService<CreateAssetSlugs>();
}
yield return serviceProvider.GetRequiredService<StartEventConsumers>();
}
}

36
tools/Migrate_01/Migrations/CreateAssetSlugs.cs

@ -0,0 +1,36 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.States;
namespace Migrate_01.Migrations
{
public sealed class CreateAssetSlugs : IMigration
{
private readonly ISnapshotStore<AssetState, Guid> stateForAssets;
public CreateAssetSlugs(ISnapshotStore<AssetState, Guid> stateForAssets)
{
this.stateForAssets = stateForAssets;
}
public Task UpdateAsync()
{
return stateForAssets.ReadAllAsync(async (state, version) =>
{
state.FileNameSlug = state.FileName.ToAssetSlug();
await stateForAssets.WriteAsync(state.Id, state, version, version);
});
}
}
}
Loading…
Cancel
Save