Browse Source

Cache headers fixed?

pull/340/head
Sebastian 7 years ago
parent
commit
925597c289
  1. 3
      src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
  2. 3
      src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  3. 3
      src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  4. 3
      src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  5. 5
      src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs
  6. 3
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  7. 2
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  8. 3
      src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  9. 5
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  10. 2
      src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  11. 3
      src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
  12. 7
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  13. 2
      src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs
  14. 3
      src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs
  15. 3
      src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs
  16. 2
      src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs
  17. 7
      src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  18. 2
      src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs
  19. 5
      src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  20. 3
      src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs
  21. 3
      src/Squidex/Config/Web/WebExtensions.cs
  22. 5
      src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs
  23. 10
      src/Squidex/Pipeline/ETagExtensions.cs
  24. 5
      src/Squidex/Pipeline/ETagFilter.cs
  25. 2
      src/Squidex/Pipeline/IGenerateEtag.cs
  26. 4
      src/Squidex/app/framework/angular/http/caching.interceptor.ts
  27. 4781
      src/Squidex/package-lock.json
  28. 3
      tests/Squidex.Tests/Pipeline/CommandMiddlewares/ETagCommandMiddlewareTests.cs

3
src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs

@ -8,6 +8,7 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
@ -48,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var response = App.Clients.Select(ClientDto.FromKvp).ToArray();
Response.Headers["ETag"] = App.Version.ToString();
Response.Headers[HeaderNames.ETag] = App.Version.ToString();
return Ok(response);
}

3
src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs

@ -7,6 +7,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
@ -48,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var response = ContributorsDto.FromApp(App, appPlansProvider);
Response.Headers["ETag"] = App.Version.ToString();
Response.Headers[HeaderNames.ETag] = App.Version.ToString();
return Ok(response);
}

3
src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs

@ -8,6 +8,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
@ -45,7 +46,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var response = AppLanguageDto.FromApp(App);
Response.Headers["ETag"] = App.Version.ToString();
Response.Headers[HeaderNames.ETag] = App.Version.ToString();
return Ok(response);
}

3
src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs

@ -9,6 +9,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands;
@ -48,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var response = App.Patterns.Select(AppPatternDto.FromKvp).OrderBy(x => x.Name).ToArray();
Response.Headers["ETag"] = App.Version.ToString();
Response.Headers[HeaderNames.ETag] = App.Version.ToString();
return Ok(response);
}

5
src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs

@ -7,6 +7,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
@ -48,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var response = RolesDto.FromApp(App);
Response.Headers["ETag"] = App.Version.ToString();
Response.Headers[HeaderNames.ETag] = App.Version.ToString();
return Ok(response);
}
@ -70,7 +71,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
{
var response = await permissionsProvider.GetPermissionsAsync(App);
Response.Headers["ETag"] = string.Join(";", response).Sha256Base64();
Response.Headers[HeaderNames.ETag] = string.Join(";", response).Sha256Base64();
return Ok(response);
}

3
src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -9,6 +9,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Commands;
@ -65,7 +66,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
var response = entities.ToArray(a => AppDto.FromApp(a, userId, userPermissions, appPlansProvider));
Response.Headers["ETag"] = response.ToManyEtag();
Response.Headers[HeaderNames.ETag] = response.ToManyEtag();
return Ok(response);
}

2
src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs

@ -20,7 +20,7 @@ using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
public sealed class AppDto : IGenerateEtag
public sealed class AppDto : IGenerateETag
{
/// <summary>
/// The name of the app.

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

@ -9,6 +9,7 @@ using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
@ -67,7 +68,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
return NotFound();
}
Response.Headers["ETag"] = entity.FileVersion.ToString();
Response.Headers[HeaderNames.ETag] = entity.FileVersion.ToString();
return new FileCallbackResult(entity.MimeType, entity.FileName, async bodyStream =>
{

5
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Areas.Api.Controllers.Contents;
@ -111,7 +112,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
Response.Headers["Surrogate-Key"] = response.Items.ToSurrogateKeys();
}
Response.Headers["ETag"] = response.Items.ToManyEtag(response.Total);
Response.Headers[HeaderNames.ETag] = response.Items.ToManyEtag(response.Total);
return Ok(response);
}
@ -148,7 +149,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
Response.Headers["Surrogate-Key"] = entity.Id.ToString();
}
Response.Headers["ETag"] = entity.Version.ToString();
Response.Headers[HeaderNames.ETag] = entity.Version.ToString();
return Ok(response);
}

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

@ -16,7 +16,7 @@ using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
public sealed class AssetDto : IGenerateEtag
public sealed class AssetDto : IGenerateETag
{
/// <summary>
/// The id of the asset.

3
src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs

@ -8,6 +8,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Orleans;
using Squidex.Areas.Api.Controllers.Comments.Models;
using Squidex.Domain.Apps.Entities.Comments;
@ -56,7 +57,7 @@ namespace Squidex.Areas.Api.Controllers.Comments
var result = await grainFactory.GetGrain<ICommentGrain>(commentsId).GetCommentsAsync(version);
var response = CommentsDto.FromResult(result);
Response.Headers["ETag"] = response.Version.ToString();
Response.Headers[HeaderNames.ETag] = response.Version.ToString();
return Ok(response);
}

7
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -10,6 +10,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using NodaTime;
using NodaTime.Text;
using Squidex.Areas.Api.Controllers.Contents.Models;
@ -141,7 +142,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
Response.Headers["Surrogate-Key"] = response.Items.ToSurrogateKeys();
}
Response.Headers["ETag"] = response.Items.ToManyEtag(response.Total);
Response.Headers[HeaderNames.ETag] = response.Items.ToManyEtag(response.Total);
return Ok(response);
}
@ -175,7 +176,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
Response.Headers["Surrogate-Key"] = content.Id.ToString();
}
Response.Headers["ETag"] = content.Version.ToString();
Response.Headers[HeaderNames.ETag] = content.Version.ToString();
return Ok(response);
}
@ -211,7 +212,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
Response.Headers["Surrogate-Key"] = content.Id.ToString();
}
Response.Headers["ETag"] = content.Version.ToString();
Response.Headers[HeaderNames.ETag] = content.Version.ToString();
return Ok(response.Data);
}

2
src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs

@ -20,7 +20,7 @@ using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Contents.Models
{
public sealed class ContentDto : IGenerateEtag
public sealed class ContentDto : IGenerateETag
{
/// <summary>
/// The if of the content item.

3
src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs

@ -7,6 +7,7 @@
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
@ -41,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.Languages
{
var response = Language.AllLanguages.Select(LanguageDto.FromLanguage).ToArray();
Response.Headers["Etag"] = "1";
Response.Headers[HeaderNames.ETag] = "1";
return Ok(response);
}

3
src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs

@ -7,6 +7,7 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Plans.Models;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
@ -52,7 +53,7 @@ namespace Squidex.Areas.Api.Controllers.Plans
var response = AppPlansDto.FromApp(App, appPlansProvider, hasPortal);
Response.Headers["ETag"] = App.Version.ToString();
Response.Headers[HeaderNames.ETag] = App.Version.ToString();
return Ok(response);
}

2
src/Squidex/Areas/Api/Controllers/Rules/Models/RuleDto.cs

@ -18,7 +18,7 @@ using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Rules.Models
{
public sealed class RuleDto : IGenerateEtag
public sealed class RuleDto : IGenerateETag
{
/// <summary>
/// The id of the rule.

7
src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using NodaTime;
using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Domain.Apps.Entities;
@ -59,7 +60,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
{
var response = RuleElementRegistry.Actions.ToDictionary(x => x.Key, x => SimpleMapper.Map(x.Value, new RuleElementDto()));
Response.Headers["Etag"] = RuleActionsEtag;
Response.Headers[HeaderNames.ETag] = RuleActionsEtag;
return Ok(response);
}
@ -79,7 +80,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
{
var response = RuleElementRegistry.Triggers.ToDictionary(x => x.Key, x => SimpleMapper.Map(x.Value, new RuleElementDto()));
Response.Headers["Etag"] = RuleTriggersEtag;
Response.Headers[HeaderNames.ETag] = RuleTriggersEtag;
return Ok(response);
}
@ -103,7 +104,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
var response = entities.Select(RuleDto.FromRule).ToArray();
Response.Headers["ETag"] = response.ToManyEtag(0);
Response.Headers[HeaderNames.ETag] = response.ToManyEtag(0);
return Ok(response);
}

2
src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs

@ -15,7 +15,7 @@ using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.Schemas.Models
{
public sealed class SchemaDto : IGenerateEtag
public sealed class SchemaDto : IGenerateETag
{
/// <summary>
/// The id of the schema.

5
src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Schemas;
@ -54,7 +55,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
var response = schemas.ToArray(SchemaDto.FromSchema);
Response.Headers["ETag"] = response.ToManyEtag();
Response.Headers[HeaderNames.ETag] = response.ToManyEtag();
return Ok(response);
}
@ -93,7 +94,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas
var response = SchemaDetailsDto.FromSchema(entity);
Response.Headers["ETag"] = entity.Version.ToString();
Response.Headers[HeaderNames.ETag] = entity.Version.ToString();
return Ok(response);
}

3
src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs

@ -10,6 +10,7 @@ using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace Squidex.Areas.Frontend.Middlewares
{
@ -55,7 +56,7 @@ namespace Squidex.Areas.Frontend.Middlewares
memoryStream.Seek(0, SeekOrigin.Begin);
context.Response.Headers["Content-Length"] = memoryStream.Length.ToString();
context.Response.Headers[HeaderNames.ContentLength] = memoryStream.Length.ToString();
await memoryStream.CopyToAsync(responseBody);
}

3
src/Squidex/Config/Web/WebExtensions.cs

@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Net.Http.Headers;
using Squidex.Infrastructure.Json;
using Squidex.Pipeline;
using Squidex.Pipeline.Robots;
@ -63,7 +64,7 @@ namespace Squidex.Config.Web
var json = serializer.Serialize(response);
httpContext.Response.Headers["Content-Types"] = "text/json";
httpContext.Response.Headers[HeaderNames.ContentType] = "text/json";
return httpContext.Response.WriteAsync(json);
});

5
src/Squidex/Pipeline/CommandMiddlewares/ETagCommandMiddleware.cs

@ -9,6 +9,7 @@ using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
@ -33,7 +34,7 @@ namespace Squidex.Pipeline.CommandMiddlewares
}
var headers = httpContextAccessor.HttpContext.Request.Headers;
var headerMatch = headers["If-Match"].ToString();
var headerMatch = headers[HeaderNames.IfMatch].ToString();
if (!string.IsNullOrWhiteSpace(headerMatch) && long.TryParse(headerMatch, NumberStyles.Any, CultureInfo.InvariantCulture, out var expectedVersion))
{
@ -48,7 +49,7 @@ namespace Squidex.Pipeline.CommandMiddlewares
if (context.Result<object>() is EntitySavedResult result)
{
httpContextAccessor.HttpContext.Response.Headers["ETag"] = result.Version.ToString();
httpContextAccessor.HttpContext.Response.Headers[HeaderNames.ETag] = result.Version.ToString();
}
}
}

10
src/Squidex/Pipeline/ETagExtensions.cs

@ -17,7 +17,7 @@ namespace Squidex.Pipeline
{
private static readonly int GuidLength = Guid.Empty.ToString().Length;
public static string ToManyEtag<T>(this IReadOnlyList<T> items, long total = 0) where T : IGenerateEtag
public static string ToManyEtag<T>(this IReadOnlyList<T> items, long total = 0) where T : IGenerateETag
{
using (Profiler.Trace("CalculateEtag"))
{
@ -27,7 +27,7 @@ namespace Squidex.Pipeline
}
}
private static string Unhashed<T>(IReadOnlyList<T> items, long total) where T : IGenerateEtag
private static string Unhashed<T>(IReadOnlyList<T> items, long total) where T : IGenerateETag
{
var sb = new StringBuilder((items.Count * (GuidLength + 4)) + 10);
@ -47,10 +47,10 @@ namespace Squidex.Pipeline
}
}
return sb.ToString();
return sb.ToString().Sha256Base64();
}
public static string ToSurrogateKeys<T>(this IReadOnlyList<T> items) where T : IGenerateEtag
public static string ToSurrogateKeys<T>(this IReadOnlyList<T> items) where T : IGenerateETag
{
if (items.Count == 0)
{
@ -70,7 +70,7 @@ namespace Squidex.Pipeline
return sb.ToString();
}
public static string ToEtag<T>(this T item) where T : IGenerateEtag
public static string ToEtag<T>(this T item) where T : IGenerateETag
{
return item.Version.ToString();
}

5
src/Squidex/Pipeline/ETagFilter.cs

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Net.Http.Headers;
namespace Squidex.Pipeline
{
@ -21,9 +22,9 @@ namespace Squidex.Pipeline
var httpContext = context.HttpContext;
if (HttpMethods.IsGet(httpContext.Request.Method) &&
httpContext.Request.Headers.TryGetValue("If-None-Match", out var noneMatch) &&
httpContext.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var noneMatch) &&
httpContext.Response.StatusCode == 200 &&
httpContext.Response.Headers.TryGetValue("ETag", out var etag) &&
httpContext.Response.Headers.TryGetValue(HeaderNames.ETag, out var etag) &&
!string.IsNullOrWhiteSpace(noneMatch) &&
!string.IsNullOrWhiteSpace(etag) &&
string.Equals(etag, noneMatch, System.StringComparison.Ordinal))

2
src/Squidex/Pipeline/IGenerateEtag.cs

@ -9,7 +9,7 @@ using System;
namespace Squidex.Pipeline
{
public interface IGenerateEtag
public interface IGenerateETag
{
Guid Id { get; }

4
src/Squidex/app/framework/angular/http/caching.interceptor.ts

@ -21,13 +21,13 @@ export class CachingInterceptor implements HttpInterceptor {
const cacheEntry = this.cache[req.url];
if (cacheEntry) {
req = req.clone({ headers: req.headers.set('If-None-Match', cacheEntry.headers.get('Etag')!) });
req = req.clone({ headers: req.headers.set('If-None-Match', cacheEntry.headers.get('ETag')!) });
}
return next.handle(req).pipe(
tap(response => {
if (Types.is(response, HttpResponse)) {
if (response.headers.get('Etag')) {
if (response.headers.get('ETag')) {
this.cache[req.url] = response;
}
}

4781
src/Squidex/package-lock.json

File diff suppressed because it is too large

3
tests/Squidex.Tests/Pipeline/CommandMiddlewares/ETagCommandMiddlewareTests.cs

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure.Commands;
using Xunit;
@ -67,7 +68,7 @@ namespace Squidex.Pipeline.CommandMiddlewares
await sut.HandleAsync(context);
Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext.Response.Headers["ETag"]);
Assert.Equal(new StringValues("17"), httpContextAccessor.HttpContext.Response.Headers[HeaderNames.ETag]);
}
}
}

Loading…
Cancel
Save