mirror of https://github.com/Squidex/squidex.git
Browse Source
* Cache improvements. * Also trim windows path separator. * Improve scheduler.pull/955/head
committed by
GitHub
24 changed files with 633 additions and 274 deletions
@ -0,0 +1,94 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Text.RegularExpressions; |
|||
using Microsoft.Extensions.FileProviders; |
|||
using Microsoft.Extensions.Primitives; |
|||
|
|||
namespace Squidex.Web; |
|||
|
|||
public sealed class IgnoreHashFileProvider : IFileProvider |
|||
{ |
|||
private readonly char[] pathSeparators = { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, '\\' }; |
|||
private readonly Dictionary<string, string> map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); |
|||
private readonly IFileProvider inner; |
|||
|
|||
public IgnoreHashFileProvider(IFileProvider inner) |
|||
{ |
|||
this.inner = inner; |
|||
|
|||
var regex = new Regex("^(?<Name>[^.]+)\\.[0-9a-f]{4,}\\.(?<Extension>.+)$"); |
|||
|
|||
void MapDirectory(string path) |
|||
{ |
|||
foreach (var file in inner.GetDirectoryContents(path)) |
|||
{ |
|||
if (file.IsDirectory) |
|||
{ |
|||
MapDirectory(Combine(path, file.Name)); |
|||
continue; |
|||
} |
|||
|
|||
var match = regex.Match(file.Name); |
|||
|
|||
if (match.Success) |
|||
{ |
|||
var nameWithouthHash = $"{match.Groups["Name"].Value}.{match.Groups["Extension"].Value}"; |
|||
|
|||
var pathHashed = Combine(path, file.Name); |
|||
var pathNormal = Combine(path, nameWithouthHash); |
|||
|
|||
map[pathNormal] = pathHashed; |
|||
} |
|||
} |
|||
} |
|||
|
|||
MapDirectory(string.Empty); |
|||
} |
|||
|
|||
public IFileInfo GetFileInfo(string subpath) |
|||
{ |
|||
var file = inner.GetFileInfo(subpath); |
|||
|
|||
if (!file.Exists) |
|||
{ |
|||
subpath = subpath.TrimStart(pathSeparators).Replace('\\', '/'); |
|||
|
|||
if (map.TryGetValue(subpath, out var withHash)) |
|||
{ |
|||
file = inner.GetFileInfo(withHash); |
|||
} |
|||
} |
|||
|
|||
return file; |
|||
} |
|||
|
|||
public IDirectoryContents GetDirectoryContents(string subpath) |
|||
{ |
|||
return inner.GetDirectoryContents(subpath); |
|||
} |
|||
|
|||
public IChangeToken Watch(string filter) |
|||
{ |
|||
return inner.Watch(filter); |
|||
} |
|||
|
|||
private static string Combine(string path1, string path2) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(path1)) |
|||
{ |
|||
return path2; |
|||
} |
|||
|
|||
if (string.IsNullOrWhiteSpace(path2)) |
|||
{ |
|||
return path1; |
|||
} |
|||
|
|||
return $"{path1}/{path2}"; |
|||
} |
|||
} |
|||
@ -0,0 +1,242 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.Extensions.FileProviders; |
|||
|
|||
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
|||
|
|||
namespace Squidex.Web; |
|||
|
|||
public class IgnoreHashFileProviderTests |
|||
{ |
|||
private readonly IFileProvider inner = A.Fake<IFileProvider>(); |
|||
|
|||
[Fact] |
|||
public void Should_get_file_from_inner() |
|||
{ |
|||
var fileNormal = CreateFile("styles.css"); |
|||
|
|||
var sut = CreateSut(); |
|||
|
|||
A.CallTo(() => inner.GetFileInfo(fileNormal.Name)) |
|||
.Returns(fileNormal); |
|||
|
|||
var actual = sut.GetFileInfo(fileNormal.Name); |
|||
|
|||
Assert.Equal(fileNormal, actual); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(@"\styles.css")] |
|||
[InlineData(@"/styles.css")] |
|||
public void Should_get_file_from_hashed_version_if_normal_file_does_not_exist(string path) |
|||
{ |
|||
var fileNormal = CreateFile("styles.css", exists: false); |
|||
var fileHashed = CreateFile("styles.42efefef.css"); |
|||
|
|||
var directories = new[] |
|||
{ |
|||
(string.Empty, |
|||
new[] |
|||
{ |
|||
fileHashed |
|||
} |
|||
) |
|||
}; |
|||
|
|||
var sut = CreateSut(directories); |
|||
|
|||
A.CallTo(() => inner.GetFileInfo(path)) |
|||
.Returns(fileNormal); |
|||
|
|||
A.CallTo(() => inner.GetFileInfo(fileHashed.Name)) |
|||
.Returns(fileHashed); |
|||
|
|||
var actual = sut.GetFileInfo(path); |
|||
|
|||
Assert.Equal(fileHashed, actual); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(@"build/styles.css")] |
|||
[InlineData(@"build\styles.css")] |
|||
[InlineData(@"\build\styles.css")] |
|||
[InlineData(@"/build/styles.css")] |
|||
public void Should_get_nested_file_from_hashed_version_if_normal_file_does_not_exist(string path) |
|||
{ |
|||
var directory = CreateFile("build", directory: true); |
|||
|
|||
var fileNormal = CreateFile("styles.css", exists: false); |
|||
var fileHashed = CreateFile("styles.42efefef.css"); |
|||
|
|||
var directories = new[] |
|||
{ |
|||
(string.Empty, |
|||
new[] |
|||
{ |
|||
directory |
|||
} |
|||
), |
|||
(directory.Name, |
|||
new[] |
|||
{ |
|||
fileHashed |
|||
} |
|||
) |
|||
}; |
|||
|
|||
var sut = CreateSut(directories); |
|||
|
|||
A.CallTo(() => inner.GetFileInfo(path)) |
|||
.Returns(fileNormal); |
|||
|
|||
A.CallTo(() => inner.GetFileInfo($"build/{fileHashed.Name}")) |
|||
.Returns(fileHashed); |
|||
|
|||
var actual = sut.GetFileInfo(path); |
|||
|
|||
Assert.Equal(fileHashed, actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_get_file_from_hashed_version_if_normal_file_exists() |
|||
{ |
|||
var fileNormal = CreateFile("styles.css"); |
|||
var fileHashed = CreateFile("styles.42efefef.css"); |
|||
|
|||
var directories = new[] |
|||
{ |
|||
(string.Empty, |
|||
new[] |
|||
{ |
|||
fileNormal |
|||
} |
|||
) |
|||
}; |
|||
|
|||
var sut = CreateSut(directories); |
|||
|
|||
A.CallTo(() => inner.GetFileInfo(fileNormal.Name)) |
|||
.Returns(fileNormal); |
|||
|
|||
A.CallTo(() => inner.GetFileInfo(fileHashed.Name)) |
|||
.Returns(fileHashed); |
|||
|
|||
var actual = sut.GetFileInfo(fileNormal.Name); |
|||
|
|||
Assert.Equal(fileNormal, actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_get_file_from_hashed_version_if_normal_file_is_directory() |
|||
{ |
|||
var fileNormal = CreateFile("styles.css", directory: true); |
|||
var fileHashed = CreateFile("styles.42efefef.css"); |
|||
|
|||
var directories = new[] |
|||
{ |
|||
(string.Empty, |
|||
new[] |
|||
{ |
|||
fileNormal |
|||
} |
|||
) |
|||
}; |
|||
|
|||
var sut = CreateSut(directories); |
|||
|
|||
A.CallTo(() => inner.GetFileInfo(fileNormal.Name)) |
|||
.Returns(fileNormal); |
|||
|
|||
A.CallTo(() => inner.GetFileInfo(fileHashed.Name)) |
|||
.Returns(fileHashed); |
|||
|
|||
var actual = sut.GetFileInfo(fileNormal.Name); |
|||
|
|||
Assert.Equal(fileNormal, actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_get_file_from_hashed_version_if_not_mapped() |
|||
{ |
|||
var fileNormal = CreateFile("styles.css"); |
|||
var fileHashed = CreateFile("styles.42efefef.css"); |
|||
|
|||
var sut = CreateSut(); |
|||
|
|||
A.CallTo(() => inner.GetFileInfo(fileNormal.Name)) |
|||
.Returns(fileNormal); |
|||
|
|||
A.CallTo(() => inner.GetFileInfo(fileHashed.Name)) |
|||
.Returns(fileHashed); |
|||
|
|||
var actual = sut.GetFileInfo(fileNormal.Name); |
|||
|
|||
Assert.Equal(fileNormal, actual); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_forward_watch_call_to_inner() |
|||
{ |
|||
var sut = CreateSut(); |
|||
|
|||
sut.Watch("/"); |
|||
|
|||
A.CallTo(() => inner.Watch("/")) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_forward_directory_call_to_inner() |
|||
{ |
|||
var sut = CreateSut(); |
|||
|
|||
sut.GetDirectoryContents("/"); |
|||
|
|||
A.CallTo(() => inner.GetDirectoryContents("/")) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
private IgnoreHashFileProvider CreateSut(params (string Path, IFileInfo[] Files)[] directories) |
|||
{ |
|||
foreach (var directory in directories) |
|||
{ |
|||
A.CallTo(() => inner.GetDirectoryContents(directory.Path)) |
|||
.Returns(new DirectoryContents(directory.Files)); |
|||
} |
|||
|
|||
return new IgnoreHashFileProvider(inner); |
|||
} |
|||
|
|||
private static IFileInfo CreateFile(string name, bool exists = true, bool directory = false) |
|||
{ |
|||
return new File(name, exists, directory); |
|||
} |
|||
|
|||
public record File(string Name, bool Exists = true, bool IsDirectory = false, long Length = 100) : IFileInfo |
|||
#pragma warning restore SA1313 // Parameter names should begin with lower-case letter
|
|||
{ |
|||
public DateTimeOffset LastModified => default; |
|||
|
|||
public string? PhysicalPath => default; |
|||
|
|||
public Stream CreateReadStream() |
|||
{ |
|||
throw new NotImplementedException(); |
|||
} |
|||
} |
|||
|
|||
private sealed class DirectoryContents : List<IFileInfo>, IDirectoryContents |
|||
{ |
|||
bool IDirectoryContents.Exists => true; |
|||
|
|||
public DirectoryContents(IEnumerable<IFileInfo> files) |
|||
: base(files) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
|||
*/ |
|||
|
|||
import { ErrorHandler, Injectable, NgZone } from '@angular/core'; |
|||
import { DialogService } from './../internal'; |
|||
|
|||
@Injectable() |
|||
export class GlobalErrorHandler implements ErrorHandler { |
|||
constructor( |
|||
private readonly dialogs: DialogService, |
|||
private readonly zone: NgZone, |
|||
) { |
|||
} |
|||
|
|||
public handleError(error: any): void { |
|||
const chunkFailedMessage = /Loading chunk [\d]+ failed/; |
|||
|
|||
if (chunkFailedMessage.test(error.message)) { |
|||
this.zone.run(() => { |
|||
this.dialogs.confirm('i18n:common.errors.chunkLoadingTitle', 'i18n:common.errors.chunkLoadingText') |
|||
.subscribe(() => { |
|||
location.reload(); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
console.error(error); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue