Refactor metadata

This commit is contained in:
Kieran 2022-02-17 15:52:49 +00:00
parent ad7b40df57
commit 6a5e1e24bd
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
21 changed files with 355 additions and 282 deletions

View File

@ -1,7 +1,6 @@
using System.Net;
using Microsoft.AspNetCore.Mvc;
using VoidCat.Model;
using VoidCat.Services;
using VoidCat.Services.Abstractions;
namespace VoidCat.Controllers;
@ -32,9 +31,9 @@ public class DownloadController : Controller
public async Task DownloadFile([FromRoute] string id)
{
var gid = id.FromBase58Guid();
var meta = await SetupDownload(gid);
var voidFile = await SetupDownload(gid);
var egressReq = new EgressRequest(gid, GetRanges(Request, (long)meta!.Size));
var egressReq = new EgressRequest(gid, GetRanges(Request, (long) voidFile!.Metadata!.Size));
if (egressReq.Ranges.Count() > 1)
{
_logger.LogWarning("Multi-range request not supported!");
@ -46,10 +45,10 @@ public class DownloadController : Controller
}
else if (egressReq.Ranges.Count() == 1)
{
Response.StatusCode = (int)HttpStatusCode.PartialContent;
Response.StatusCode = (int) HttpStatusCode.PartialContent;
if (egressReq.Ranges.Sum(a => a.Size) == 0)
{
Response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
Response.StatusCode = (int) HttpStatusCode.RequestedRangeNotSatisfiable;
return;
}
}
@ -70,7 +69,7 @@ public class DownloadController : Controller
await Response.CompleteAsync();
}
private async Task<VoidFile?> SetupDownload(Guid id)
private async Task<PublicVoidFile?> SetupDownload(Guid id)
{
var meta = await _storage.Get(id);
if (meta == null)
@ -111,4 +110,4 @@ public class DownloadController : Controller
}
}
}
}
}

View File

@ -23,11 +23,13 @@ namespace VoidCat.Controllers
{
var bw = await _statsReporter.GetBandwidth();
var bytes = 0UL;
var count = 0;
await foreach (var vf in _fileStore.ListFiles())
{
bytes += vf.Size;
bytes += vf.Metadata?.Size ?? 0;
count++;
}
return new(bw, bytes);
return new(bw, bytes, count);
}
[HttpGet]
@ -39,6 +41,6 @@ namespace VoidCat.Controllers
}
}
public sealed record GlobalStats(Bandwidth Bandwidth, ulong TotalBytes);
public sealed record GlobalStats(Bandwidth Bandwidth, ulong TotalBytes, int Count);
public sealed record FileStats(Bandwidth Bandwidth);
}

View File

@ -28,7 +28,9 @@ namespace VoidCat.Controllers
var meta = new VoidFileMeta()
{
MimeType = Request.Headers.GetHeader("V-Content-Type"),
Name = Request.Headers.GetHeader("V-Filename")
Name = Request.Headers.GetHeader("V-Filename"),
Description = Request.Headers.GetHeader("V-Description"),
Digest = Request.Headers.GetHeader("V-Full-Digest")
};
var digest = Request.Headers.GetHeader("V-Digest");
@ -72,24 +74,10 @@ namespace VoidCat.Controllers
[HttpGet]
[Route("{id}")]
public ValueTask<VoidFile?> GetInfo([FromRoute] string id)
public ValueTask<PublicVoidFile?> GetInfo([FromRoute] string id)
{
return _storage.Get(id.FromBase58Guid());
}
[HttpPatch]
[Route("{id}")]
public ValueTask UpdateFileInfo([FromRoute] string id, [FromBody] UpdateFileInfoRequest request)
{
return _storage.UpdateInfo(new VoidFile()
{
Id = id.FromBase58Guid(),
Metadata = request.Metadata
}, request.EditSecret);
}
public record UpdateFileInfoRequest([JsonConverter(typeof(Base58GuidConverter))] Guid EditSecret,
VoidFileMeta Metadata);
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
@ -108,9 +96,9 @@ namespace VoidCat.Controllers
}
}
public record UploadResult(bool Ok, InternalVoidFile? File, string? ErrorMessage)
public record UploadResult(bool Ok, PrivateVoidFile? File, string? ErrorMessage)
{
public static UploadResult Success(InternalVoidFile vf)
public static UploadResult Success(PrivateVoidFile vf)
=> new(true, vf, null);
public static UploadResult Error(string message)

30
VoidCat/Model/Paywall.cs Normal file
View File

@ -0,0 +1,30 @@
namespace VoidCat.Model;
public record Paywall
{
public PaywallServices Service { get; init; }
public PaywallConfig? Config { get; init; }
}
public enum PaywallServices
{
None,
Strike
}
public enum PaywallCurrencies
{
BTC,
USD,
EUR,
GBP
}
public abstract record PaywallConfig
{
public PaywallCurrencies Currency { get; init; }
public decimal Cost { get; init; }
}
public record StrikePaywallConfig(string Handle) : PaywallConfig;

View File

@ -1,32 +1,31 @@

using Newtonsoft.Json;
using Newtonsoft.Json;
namespace VoidCat.Model
{
public record VoidFile
public abstract record VoidFile<TMeta> where TMeta : VoidFileMeta
{
/// <summary>
/// Id of the file
/// </summary>
[JsonConverter(typeof(Base58GuidConverter))]
public Guid Id { get; init; }
public VoidFileMeta? Metadata { get; set; }
public ulong Size { get; init; }
public DateTimeOffset Uploaded { get; init; }
}
public record InternalVoidFile : VoidFile
{
[JsonConverter(typeof(Base58GuidConverter))]
public Guid EditSecret { get; init; }
}
public record VoidFileMeta
{
public string? Name { get; init; }
public string? Description { get; init; }
/// <summary>
/// Metadta related to the file
/// </summary>
public TMeta? Metadata { get; init; }
public string? MimeType { get; init; }
/// <summary>
/// Optional paywall config
/// </summary>
public Paywall? Paywall { get; init; }
}
}
public sealed record PublicVoidFile : VoidFile<VoidFileMeta>
{
}
public sealed record PrivateVoidFile : VoidFile<SecretVoidFileMeta>
{
}
}

View File

@ -0,0 +1,57 @@
using Newtonsoft.Json;
using VoidCat.Services.Abstractions;
namespace VoidCat.Model;
/// <summary>
/// File metadata which is managed by <see cref="IFileMetadataStore"/>
/// </summary>
public record VoidFileMeta
{
/// <summary>
/// Metadata version
/// </summary>
public int Version { get; init; } = 2;
/// <summary>
/// Filename
/// </summary>
public string? Name { get; init; }
/// <summary>
/// Size of the file in storage
/// </summary>
public ulong Size { get; init; }
/// <summary>
/// Date file was uploaded
/// </summary>
public DateTimeOffset Uploaded { get; init; } = DateTimeOffset.UtcNow;
/// <summary>
/// Description about the file
/// </summary>
public string? Description { get; init; }
/// <summary>
/// The content type of the file
/// </summary>
public string? MimeType { get; init; }
/// <summary>
/// SHA-256 hash of the file
/// </summary>
public string? Digest { get; init; }
}
/// <summary>
/// <see cref="VoidFile"/> with attached <see cref="EditSecret"/>
/// </summary>
public record SecretVoidFileMeta : VoidFileMeta
{
/// <summary>
/// A secret key used to make edits to the file after its uploaded
/// </summary>
[JsonConverter(typeof(Base58GuidConverter))]
public Guid EditSecret { get; init; }
}

View File

@ -6,6 +6,7 @@ using StackExchange.Redis;
using VoidCat.Model;
using VoidCat.Services;
using VoidCat.Services.Abstractions;
using VoidCat.Services.Migrations;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
@ -41,6 +42,8 @@ services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
};
});
// void.cat services
services.AddVoidMigrations();
services.AddScoped<IFileMetadataStore, LocalDiskFileMetadataStore>();
services.AddScoped<IFileStore, LocalDiskFileStore>();
services.AddScoped<IAggregateStatsCollector, AggregateStatsCollector>();
@ -61,6 +64,13 @@ else
var app = builder.Build();
// run migrations
var migrations = app.Services.GetServices<IMigration>();
foreach (var migration in migrations)
{
await migration.Migrate();
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseRouting();

View File

@ -4,9 +4,9 @@ namespace VoidCat.Services.Abstractions;
public interface IFileMetadataStore
{
ValueTask<InternalVoidFile?> Get(Guid id);
ValueTask<SecretVoidFileMeta?> Get(Guid id);
ValueTask Set(InternalVoidFile meta);
ValueTask Set(Guid id, SecretVoidFileMeta meta);
ValueTask Update(VoidFile patch, Guid editSecret);
ValueTask Update(Guid id, SecretVoidFileMeta patch);
}

View File

@ -4,15 +4,13 @@ namespace VoidCat.Services.Abstractions;
public interface IFileStore
{
ValueTask<VoidFile?> Get(Guid id);
ValueTask<PublicVoidFile?> Get(Guid id);
ValueTask<InternalVoidFile> Ingress(IngressPayload payload, CancellationToken cts);
ValueTask<PrivateVoidFile> Ingress(IngressPayload payload, CancellationToken cts);
ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts);
ValueTask UpdateInfo(VoidFile patch, Guid editSecret);
IAsyncEnumerable<VoidFile> ListFiles();
IAsyncEnumerable<PublicVoidFile> ListFiles();
}
public sealed record IngressPayload(Stream InStream, VoidFileMeta Meta, string Hash)

View File

@ -7,7 +7,7 @@ namespace VoidCat.Services;
public class LocalDiskFileMetadataStore : IFileMetadataStore
{
private const string MetadataDir = "metadata";
private const string MetadataDir = "metadata-v2";
private readonly VoidSettings _settings;
public LocalDiskFileMetadataStore(VoidSettings settings)
@ -21,34 +21,31 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
}
}
public async ValueTask<InternalVoidFile?> Get(Guid id)
public async ValueTask<SecretVoidFileMeta?> Get(Guid id)
{
var path = MapMeta(id);
if (!File.Exists(path)) return default;
var json = await File.ReadAllTextAsync(path);
return JsonConvert.DeserializeObject<InternalVoidFile>(json);
return JsonConvert.DeserializeObject<SecretVoidFileMeta>(json);
}
public async ValueTask Set(InternalVoidFile meta)
public async ValueTask Set(Guid id, SecretVoidFileMeta meta)
{
var path = MapMeta(meta.Id);
var path = MapMeta(id);
var json = JsonConvert.SerializeObject(meta);
await File.WriteAllTextAsync(path, json);
}
public async ValueTask Update(VoidFile patch, Guid editSecret)
public async ValueTask Update(Guid id, SecretVoidFileMeta patch)
{
var oldMeta = await Get(patch.Id);
if (oldMeta?.EditSecret != editSecret)
var oldMeta = await Get(id);
if (oldMeta?.EditSecret != patch.EditSecret)
{
throw new VoidNotAllowedException("Edit secret incorrect");
}
// only patch metadata
oldMeta.Metadata = patch.Metadata;
await Set(oldMeta);
await Set(id, patch);
}
private string MapMeta(Guid id) =>

View File

@ -26,9 +26,13 @@ public class LocalDiskFileStore : IFileStore
}
}
public async ValueTask<VoidFile?> Get(Guid id)
public async ValueTask<PublicVoidFile?> Get(Guid id)
{
return await _metadataStore.Get(id);
return new()
{
Id = id,
Metadata = await _metadataStore.Get(id)
};
}
public async ValueTask Egress(EgressRequest request, Stream outStream, CancellationToken cts)
@ -47,11 +51,11 @@ public class LocalDiskFileStore : IFileStore
}
}
public async ValueTask<InternalVoidFile> Ingress(IngressPayload payload, CancellationToken cts)
public async ValueTask<PrivateVoidFile> Ingress(IngressPayload payload, CancellationToken cts)
{
var id = payload.Id ?? Guid.NewGuid();
var fPath = MapPath(id);
InternalVoidFile? vf = null;
SecretVoidFileMeta? vf = null;
if (payload.IsAppend)
{
vf = await _metadataStore.Get(payload.Id!.Value);
@ -81,10 +85,12 @@ public class LocalDiskFileStore : IFileStore
}
else
{
vf = new InternalVoidFile()
vf = new SecretVoidFileMeta()
{
Id = id,
Metadata = payload.Meta,
Name = payload.Meta.Name,
Description = payload.Meta.Description,
Digest = payload.Meta.Digest,
MimeType = payload.Meta.MimeType,
Uploaded = DateTimeOffset.UtcNow,
EditSecret = Guid.NewGuid(),
Size = total
@ -92,16 +98,15 @@ public class LocalDiskFileStore : IFileStore
}
await _metadataStore.Set(vf);
return vf;
await _metadataStore.Set(id, vf);
return new()
{
Id = id,
Metadata = vf
};
}
public ValueTask UpdateInfo(VoidFile patch, Guid editSecret)
{
return _metadataStore.Update(patch, editSecret);
}
public async IAsyncEnumerable<VoidFile> ListFiles()
public async IAsyncEnumerable<PublicVoidFile> ListFiles()
{
foreach (var fe in Directory.EnumerateFiles(_settings.DataDirectory))
{
@ -111,7 +116,11 @@ public class LocalDiskFileStore : IFileStore
var meta = await _metadataStore.Get(id);
if (meta != default)
{
yield return meta;
yield return new()
{
Id = id,
Metadata = meta
};
}
}
}
@ -134,9 +143,9 @@ public class LocalDiskFileStore : IFileStore
var totalRead = readLength + offset;
var buf = buffer.Memory[..totalRead];
await fs.WriteAsync(buf, cts);
await _stats.TrackIngress(id, (ulong)buf.Length);
await _stats.TrackIngress(id, (ulong) buf.Length);
sha.TransformBlock(buf.ToArray(), 0, buf.Length, null, 0);
total += (ulong)buf.Length;
total += (ulong) buf.Length;
offset = 0;
}
@ -160,7 +169,7 @@ public class LocalDiskFileStore : IFileStore
var fullSize = readLength + offset;
await outStream.WriteAsync(buffer.Memory[..fullSize], cts);
await _stats.TrackEgress(id, (ulong)fullSize);
await _stats.TrackEgress(id, (ulong) fullSize);
await outStream.FlushAsync(cts);
offset = 0;
}
@ -188,8 +197,8 @@ public class LocalDiskFileStore : IFileStore
var fullSize = readLength + offset;
var toWrite = Math.Min(fullSize, dataRemaining);
await outStream.WriteAsync(buffer.Memory[..(int)toWrite], cts);
await _stats.TrackEgress(id, (ulong)toWrite);
await outStream.WriteAsync(buffer.Memory[..(int) toWrite], cts);
await _stats.TrackEgress(id, (ulong) toWrite);
await outStream.FlushAsync(cts);
dataRemaining -= toWrite;
offset = 0;
@ -204,4 +213,4 @@ public class LocalDiskFileStore : IFileStore
private string MapPath(Guid id) =>
Path.Join(_settings.DataDirectory, id.ToString());
}
}

View File

@ -0,0 +1,116 @@
using Newtonsoft.Json;
using VoidCat.Model;
namespace VoidCat.Services.Migrations;
public class MigrateMetadata_20220217 : IMigration
{
private const string MetadataDir = "metadata";
private const string MetadataV2Dir = "metadata-v2";
private readonly ILogger<MigrateMetadata_20220217> _logger;
private readonly VoidSettings _settings;
public MigrateMetadata_20220217(VoidSettings settings, ILogger<MigrateMetadata_20220217> log)
{
_settings = settings;
_logger = log;
}
public async ValueTask Migrate()
{
var newMeta = Path.Combine(_settings.DataDirectory, MetadataV2Dir);
if (!Directory.Exists(newMeta))
{
Directory.CreateDirectory(newMeta);
}
foreach (var fe in Directory.EnumerateFiles(_settings.DataDirectory))
{
var filename = Path.GetFileNameWithoutExtension(fe);
if (!Guid.TryParse(filename, out var id)) continue;
var fp = MapMeta(id);
if (File.Exists(fp))
{
_logger.LogInformation("Migrating metadata for {file}", fp);
try
{
var oldJson = await File.ReadAllTextAsync(fp);
if(!oldJson.Contains("\"Metadata\":")) continue; // old format should contain "Metadata":
var old = JsonConvert.DeserializeObject<InternalVoidFile>(oldJson);
var newObj = new PrivateVoidFile()
{
Id = old!.Id,
Metadata = new()
{
Name = old.Metadata!.Name,
Description = old.Metadata.Description,
Uploaded = old.Uploaded,
MimeType = old.Metadata.MimeType,
EditSecret = old.EditSecret,
Size = old.Size
}
};
await File.WriteAllTextAsync(MapV2Meta(id), JsonConvert.SerializeObject(newObj));
// delete old metadata
File.Delete(fp);
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
}
}
}
private string MapMeta(Guid id) =>
Path.ChangeExtension(Path.Join(_settings.DataDirectory, MetadataDir, id.ToString()), ".json");
private string MapV2Meta(Guid id) =>
Path.ChangeExtension(Path.Join(_settings.DataDirectory, MetadataV2Dir, id.ToString()), ".json");
private record VoidFile
{
[JsonConverter(typeof(Base58GuidConverter))]
public Guid Id { get; init; }
public VoidFileMeta? Metadata { get; set; }
public ulong Size { get; init; }
public DateTimeOffset Uploaded { get; init; }
}
private record InternalVoidFile : VoidFile
{
[JsonConverter(typeof(Base58GuidConverter))]
public Guid EditSecret { get; init; }
}
private record VoidFileMeta
{
public string? Name { get; init; }
public string? Description { get; init; }
public string? MimeType { get; init; }
}
private record NewVoidFileMeta
{
public string? Name { get; init; }
public ulong Size { get; init; }
public DateTimeOffset Uploaded { get; init; } = DateTimeOffset.UtcNow;
public string? Description { get; init; }
public string? MimeType { get; init; }
public string? Digest { get; init; }
}
private record NewSecretVoidFileMeta : NewVoidFileMeta
{
[JsonConverter(typeof(Base58GuidConverter))]
public Guid EditSecret { get; init; }
}
}

View File

@ -0,0 +1,15 @@
namespace VoidCat.Services.Migrations;
public interface IMigration
{
ValueTask Migrate();
}
public static class Migrations
{
public static IServiceCollection AddVoidMigrations(this IServiceCollection svc)
{
svc.AddTransient<IMigration, MigrateMetadata_20220217>();
return svc;
}
}

View File

@ -4,14 +4,11 @@
"private": true,
"proxy": "https://localhost:7195",
"dependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.2.1",
"feather-icons-react": "^0.5.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^6.2.1",
"react-scripts": "5.0.0",
"web-vitals": "^2.1.0"
"react-scripts": "5.0.0"
},
"scripts": {
"start": "react-scripts start",

View File

@ -3,7 +3,6 @@ import {useEffect, useState} from "react";
import "./FileUpload.css";
import {buf2hex, ConstName, FormatBytes} from "./Util";
import {RateCalculator} from "./RateCalculator";
import {upload} from "@testing-library/user-event/dist/upload";
const UploadState = {
NotStarted: 0,

View File

@ -2,4 +2,13 @@
display: grid;
grid-auto-flow: column;
margin: 0 30px;
line-height: 32px;
}
.stats svg {
vertical-align: middle;
margin-right: 10px;
}
.stats > div {
}

View File

@ -1,4 +1,5 @@
import {useEffect, useState} from "react";
import FeatherIcon from "feather-icons-react";
import {FormatBytes} from "./Util";
import "./GlobalStats.css";
@ -16,13 +17,23 @@ export function GlobalStats(props) {
useEffect(() => loadStats(), []);
return (
<div className="stats">
<div>Ingress:</div>
<div>{FormatBytes(stats?.bandwidth?.ingress ?? 0, 2)}</div>
<div>Egress:</div>
<div>{FormatBytes(stats?.bandwidth?.egress ?? 0, 2)}</div>
<div>Storage:</div>
<div>{FormatBytes(stats?.totalBytes ?? 0, 2)}</div>
</div>
<dl className="stats">
<div>
<FeatherIcon icon="upload-cloud" />
{FormatBytes(stats?.bandwidth?.ingress ?? 0, 2)}
</div>
<div>
<FeatherIcon icon="download-cloud" />
{FormatBytes(stats?.bandwidth?.egress ?? 0, 2)}
</div>
<div>
<FeatherIcon icon="database" />
{FormatBytes(stats?.totalBytes ?? 0, 2)}
</div>
<div>
<FeatherIcon icon="hash" />
{stats?.count ?? 0}
</div>
</dl>
);
}

View File

@ -1,17 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './index.css';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
);

View File

@ -1,13 +0,0 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@ -1015,7 +1015,7 @@
core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa"
integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==
@ -1491,50 +1491,6 @@
"@svgr/plugin-svgo" "^5.5.0"
loader-utils "^2.0.0"
"@testing-library/dom@^8.0.0":
version "8.11.2"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.11.2.tgz#fc110c665a066c2287be765e4a35ba8dad737015"
integrity sha512-idsS/cqbYudXcVWngc1PuWNmXs416oBy2g/7Q8QAUREt5Z3MUkAL2XJD7xazLJ6esDfqRDi/ZBxk+OPPXitHRw==
dependencies:
"@babel/code-frame" "^7.10.4"
"@babel/runtime" "^7.12.5"
"@types/aria-query" "^4.2.0"
aria-query "^5.0.0"
chalk "^4.1.0"
dom-accessibility-api "^0.5.9"
lz-string "^1.4.4"
pretty-format "^27.0.2"
"@testing-library/jest-dom@^5.14.1":
version "5.16.1"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.1.tgz#3db7df5ae97596264a7da9696fe14695ba02e51f"
integrity sha512-ajUJdfDIuTCadB79ukO+0l8O+QwN0LiSxDaYUTI4LndbbUsGi6rWU1SCexXzBA2NSjlVB9/vbkasQIL3tmPBjw==
dependencies:
"@babel/runtime" "^7.9.2"
"@types/testing-library__jest-dom" "^5.9.1"
aria-query "^5.0.0"
chalk "^3.0.0"
css "^3.0.0"
css.escape "^1.5.1"
dom-accessibility-api "^0.5.6"
lodash "^4.17.15"
redent "^3.0.0"
"@testing-library/react@^12.0.0":
version "12.1.2"
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.2.tgz#f1bc9a45943461fa2a598bb4597df1ae044cfc76"
integrity sha512-ihQiEOklNyHIpo2Y8FREkyD1QAea054U0MVbwH1m8N9TxeFz+KoJ9LkqoKqJlzx2JDm56DVwaJ1r36JYxZM05g==
dependencies:
"@babel/runtime" "^7.12.5"
"@testing-library/dom" "^8.0.0"
"@testing-library/user-event@^13.2.1":
version "13.5.0"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295"
integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==
dependencies:
"@babel/runtime" "^7.12.5"
"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@ -1545,11 +1501,6 @@
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
"@types/aria-query@^4.2.0":
version "4.2.2"
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc"
integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14":
version "7.1.18"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.18.tgz#1a29abcc411a9c05e2094c98f9a1b7da6cdf49f8"
@ -1704,14 +1655,6 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/jest@*":
version "27.4.0"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.0.tgz#037ab8b872067cae842a320841693080f9cb84ed"
integrity sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==
dependencies:
jest-diff "^27.0.0"
pretty-format "^27.0.0"
"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.9"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
@ -1796,13 +1739,6 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
"@types/testing-library__jest-dom@^5.9.1":
version "5.14.2"
resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz#564fb2b2dc827147e937a75b639a05d17ce18b44"
integrity sha512-vehbtyHUShPxIa9SioxDwCvgxukDMH//icJG90sXQBUm5lJOHLT5kNeU9tnivhnA/TkOFMzGIXN2cTc4hY8/kg==
dependencies:
"@types/jest" "*"
"@types/trusted-types@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
@ -2241,11 +2177,6 @@ aria-query@^4.2.2:
"@babel/runtime" "^7.10.2"
"@babel/runtime-corejs3" "^7.10.2"
aria-query@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c"
integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
@ -2322,11 +2253,6 @@ at-least-node@^1.0.0:
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
atob@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
autoprefixer@^10.4.2:
version "10.4.2"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.2.tgz#25e1df09a31a9fba5c40b578936b90d35c9d4d3b"
@ -2688,14 +2614,6 @@ chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@ -3087,20 +3005,6 @@ css-what@^5.1.0:
resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe"
integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==
css.escape@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=
css@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d"
integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==
dependencies:
inherits "^2.0.4"
source-map "^0.6.1"
source-map-resolve "^0.6.0"
cssdb@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-5.1.0.tgz#ec728d5f5c0811debd0820cbebda505d43003400"
@ -3224,11 +3128,6 @@ decimal.js@^10.2.1:
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
dedent@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
@ -3392,11 +3291,6 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9:
version "0.5.10"
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.10.tgz#caa6d08f60388d0bb4539dd75fe458a9a1d0014c"
integrity sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g==
dom-converter@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
@ -4012,6 +3906,11 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
feather-icons-react@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/feather-icons-react/-/feather-icons-react-0.5.0.tgz#74f8b398f4031491901aa47ff470899e408df159"
integrity sha512-k7y6JnghcwLi3uo5SaSnnngfHOE+IPpAFzlsmNhlXwbP8jev2rOYbEq1g5lhMglbV934KhcaSbo3DYV32I3/Ug==
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@ -4603,7 +4502,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@ -4973,7 +4872,7 @@ jest-config@^27.4.7:
pretty-format "^27.4.6"
slash "^3.0.0"
jest-diff@^27.0.0, jest-diff@^27.4.6:
jest-diff@^27.4.6:
version "27.4.6"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.4.6.tgz#93815774d2012a2cbb6cf23f84d48c7a2618f98d"
integrity sha512-zjaB0sh0Lb13VyPsd92V7HkqF6yKRH9vm33rwBt7rPYrpQvS1nCvlIy2pICbKta+ZjWngYLNn4cCK4nyZkjS/w==
@ -5589,7 +5488,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
lodash@^4.17.14, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -5615,11 +5514,6 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
lz-string@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=
magic-string@^0.25.0, magic-string@^0.25.7:
version "0.25.7"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
@ -5713,11 +5607,6 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
min-indent@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
mini-css-extract-plugin@^2.4.5:
version "2.5.3"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.5.3.tgz#c5c79f9b22ce9b4f164e9492267358dbe35376d9"
@ -6737,7 +6626,7 @@ pretty-error@^4.0.0:
lodash "^4.17.20"
renderkid "^3.0.0"
pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.4.6:
pretty-format@^27.4.6:
version "27.4.6"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.4.6.tgz#1b784d2f53c68db31797b2348fa39b49e31846b7"
integrity sha512-NblstegA1y/RJW2VyML+3LlpFjzx62cUrtBIKIWDXEDkjNeleA7Od7nrzcs/VLQvAeV4CgSYhrN39DRN88Qi/g==
@ -7027,14 +6916,6 @@ recursive-readdir@^2.2.2:
dependencies:
minimatch "3.0.4"
redent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
dependencies:
indent-string "^4.0.0"
strip-indent "^3.0.0"
regenerate-unicode-properties@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326"
@ -7477,14 +7358,6 @@ source-map-loader@^3.0.0:
iconv-lite "^0.6.3"
source-map-js "^1.0.1"
source-map-resolve@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2"
integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==
dependencies:
atob "^2.1.2"
decode-uri-component "^0.2.0"
source-map-support@^0.5.6, source-map-support@~0.5.20:
version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
@ -7692,13 +7565,6 @@ strip-final-newline@^2.0.0:
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
strip-indent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
dependencies:
min-indent "^1.0.0"
strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
@ -8184,11 +8050,6 @@ wbuf@^1.1.0, wbuf@^1.7.3:
dependencies:
minimalistic-assert "^1.0.0"
web-vitals@^2.1.0:
version "2.1.4"
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.4.tgz#76563175a475a5e835264d373704f9dde718290c"
integrity sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"