Basic
This commit is contained in:
parent
afee82fed9
commit
60229a0c06
32
VoidCat/Controllers/DownloadController.cs
Normal file
32
VoidCat/Controllers/DownloadController.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Services;
|
||||||
|
|
||||||
|
namespace VoidCat.Controllers;
|
||||||
|
|
||||||
|
[Route("d")]
|
||||||
|
public class DownloadController : Controller
|
||||||
|
{
|
||||||
|
private readonly IFileStorage _storage;
|
||||||
|
|
||||||
|
public DownloadController(IFileStorage storage)
|
||||||
|
{
|
||||||
|
_storage = storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{id}")]
|
||||||
|
public async Task DownloadFile([FromRoute] string id)
|
||||||
|
{
|
||||||
|
var gid = id.FromBase58Guid();
|
||||||
|
var meta = await _storage.Get(gid);
|
||||||
|
if (meta == null)
|
||||||
|
{
|
||||||
|
Response.StatusCode = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response.ContentType = meta?.Metadata?.MimeType ?? "application/octet-stream";
|
||||||
|
await _storage.Egress(gid, Response.Body, HttpContext.RequestAborted);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using VoidCat.Model;
|
using VoidCat.Model;
|
||||||
using VoidCat.Services;
|
using VoidCat.Services;
|
||||||
|
|
||||||
@ -7,25 +8,44 @@ namespace VoidCat.Controllers
|
|||||||
[Route("upload")]
|
[Route("upload")]
|
||||||
public class UploadController : Controller
|
public class UploadController : Controller
|
||||||
{
|
{
|
||||||
private readonly IFileIngressFactory _fileIngress;
|
private readonly IFileStorage _storage;
|
||||||
private readonly IStatsCollector _stats;
|
|
||||||
|
|
||||||
public UploadController(IStatsCollector stats, IFileIngressFactory fileIngress)
|
public UploadController(IFileStorage storage)
|
||||||
{
|
{
|
||||||
_stats = stats;
|
_storage = storage;
|
||||||
_fileIngress = fileIngress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public Task<VoidFile> UploadFile()
|
[DisableRequestSizeLimit]
|
||||||
|
public Task<InternalVoidFile> UploadFile()
|
||||||
{
|
{
|
||||||
return Request.HasFormContentType ?
|
return Request.HasFormContentType ?
|
||||||
saveFromForm() : _fileIngress.Ingress(Request.Body);
|
saveFromForm() : _storage.Ingress(Request.Body, HttpContext.RequestAborted);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<VoidFile> saveFromForm()
|
[HttpGet]
|
||||||
|
[Route("{id}")]
|
||||||
|
public Task<VoidFile?> GetInfo([FromRoute] string id)
|
||||||
{
|
{
|
||||||
return Task.FromResult<VoidFile>(null);
|
return _storage.Get(id.FromBase58Guid());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPatch]
|
||||||
|
[Route("{id}")]
|
||||||
|
public Task UpdateFileInfo([FromRoute]string id, [FromBody]UpdateFileInfoRequest request)
|
||||||
|
{
|
||||||
|
return _storage.UpdateInfo(new VoidFile()
|
||||||
|
{
|
||||||
|
Id = id.FromBase58Guid(),
|
||||||
|
Metadata = request.Metadata
|
||||||
|
}, request.EditSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<InternalVoidFile> saveFromForm()
|
||||||
|
{
|
||||||
|
return Task.FromResult<InternalVoidFile>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record UpdateFileInfoRequest([JsonConverter(typeof(Base58GuidConverter))] Guid EditSecret, VoidFileMeta Metadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
VoidCat/Model/Base58GuidConverter.cs
Normal file
21
VoidCat/Model/Base58GuidConverter.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace VoidCat.Model;
|
||||||
|
|
||||||
|
public class Base58GuidConverter : JsonConverter<Guid>
|
||||||
|
{
|
||||||
|
public override void WriteJson(JsonWriter writer, Guid value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
writer.WriteValue(value.ToBase58());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Guid ReadJson(JsonReader reader, Type objectType, Guid existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonToken.String && existingValue == Guid.Empty)
|
||||||
|
{
|
||||||
|
return (reader.Value as string)?.FromBase58Guid() ?? existingValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingValue;
|
||||||
|
}
|
||||||
|
}
|
10
VoidCat/Model/Exceptions/VoidFileNotFoundException.cs
Normal file
10
VoidCat/Model/Exceptions/VoidFileNotFoundException.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace VoidCat.Model.Exceptions;
|
||||||
|
|
||||||
|
public class VoidFileNotFoundException : Exception
|
||||||
|
{
|
||||||
|
public VoidFileNotFoundException(Guid id)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
public Guid Id { get; }
|
||||||
|
}
|
8
VoidCat/Model/Exceptions/VoidNotAllowedException.cs
Normal file
8
VoidCat/Model/Exceptions/VoidNotAllowedException.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace VoidCat.Model.Exceptions;
|
||||||
|
|
||||||
|
public class VoidNotAllowedException : Exception
|
||||||
|
{
|
||||||
|
public VoidNotAllowedException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
16
VoidCat/Model/Extensions.cs
Normal file
16
VoidCat/Model/Extensions.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
namespace VoidCat.Model;
|
||||||
|
|
||||||
|
public static class Extensions
|
||||||
|
{
|
||||||
|
public static Guid FromBase58Guid(this string base58)
|
||||||
|
{
|
||||||
|
var enc = new NBitcoin.DataEncoders.Base58Encoder();
|
||||||
|
return new Guid(enc.DecodeData(base58));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToBase58(this Guid id)
|
||||||
|
{
|
||||||
|
var enc = new NBitcoin.DataEncoders.Base58Encoder();
|
||||||
|
return enc.EncodeData(id.ToByteArray());
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,32 @@
|
|||||||
namespace VoidCat.Model
|
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace VoidCat.Model
|
||||||
{
|
{
|
||||||
public class VoidFile
|
public class VoidFile
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; } = Guid.NewGuid();
|
[JsonConverter(typeof(Base58GuidConverter))]
|
||||||
|
public Guid Id { get; init; }
|
||||||
public string? Name { get; init; }
|
|
||||||
|
|
||||||
public string? Description { get; init; }
|
|
||||||
|
|
||||||
|
public VoidFileMeta Metadata { get; set; }
|
||||||
|
|
||||||
public ulong Size { get; init; }
|
public ulong Size { get; init; }
|
||||||
|
|
||||||
public DateTimeOffset Uploaded { get; init; }
|
public DateTimeOffset Uploaded { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class InternalVoidFile : VoidFile
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(Base58GuidConverter))]
|
||||||
|
public Guid EditSecret { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VoidFileMeta
|
||||||
|
{
|
||||||
|
public string? Name { get; init; }
|
||||||
|
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
public string? MimeType { get; init; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,6 @@
|
|||||||
{
|
{
|
||||||
public class VoidSettings
|
public class VoidSettings
|
||||||
{
|
{
|
||||||
public string FilePath { get; init; } = "./data";
|
public string DataDirectory { get; init; } = "./data";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,8 @@ var builder = WebApplication.CreateBuilder(args);
|
|||||||
|
|
||||||
var services = builder.Services;
|
var services = builder.Services;
|
||||||
services.AddRouting();
|
services.AddRouting();
|
||||||
services.AddControllers();
|
services.AddControllers().AddNewtonsoftJson();
|
||||||
services.AddScoped<IFileIngressFactory, LocalDiskFileIngressFactory>();
|
services.AddScoped<IFileStorage, LocalDiskFileIngressFactory>();
|
||||||
services.AddScoped<IStatsCollector, InMemoryStatsCollector>();
|
services.AddScoped<IStatsCollector, InMemoryStatsCollector>();
|
||||||
|
|
||||||
var configuration = builder.Configuration;
|
var configuration = builder.Configuration;
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
using VoidCat.Model;
|
|
||||||
|
|
||||||
namespace VoidCat.Services
|
|
||||||
{
|
|
||||||
public interface IFileIngressFactory
|
|
||||||
{
|
|
||||||
Task<VoidFile> Ingress(Stream inStream);
|
|
||||||
}
|
|
||||||
}
|
|
15
VoidCat/Services/IFileStorage.cs
Normal file
15
VoidCat/Services/IFileStorage.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using VoidCat.Model;
|
||||||
|
|
||||||
|
namespace VoidCat.Services
|
||||||
|
{
|
||||||
|
public interface IFileStorage
|
||||||
|
{
|
||||||
|
Task<VoidFile?> Get(Guid id);
|
||||||
|
|
||||||
|
Task<InternalVoidFile> Ingress(Stream inStream, CancellationToken cts);
|
||||||
|
|
||||||
|
Task Egress(Guid id, Stream outStream, CancellationToken cts);
|
||||||
|
|
||||||
|
Task UpdateInfo(VoidFile patch, Guid editSecret);
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,36 @@
|
|||||||
namespace VoidCat.Services
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace VoidCat.Services
|
||||||
{
|
{
|
||||||
public class InMemoryStatsCollector : IStatsCollector
|
public class InMemoryStatsCollector : IStatsCollector
|
||||||
{
|
{
|
||||||
|
private readonly ConcurrentDictionary<Guid, ulong> _ingress = new();
|
||||||
|
private readonly ConcurrentDictionary<Guid, ulong> _egress = new();
|
||||||
|
|
||||||
public ValueTask TrackIngress(Guid id, ulong amount)
|
public ValueTask TrackIngress(Guid id, ulong amount)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
if (_ingress.ContainsKey(id) && _ingress.TryGetValue(id, out var v))
|
||||||
|
{
|
||||||
|
_ingress.TryUpdate(id, v + amount, v);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_ingress.TryAdd(id, amount);
|
||||||
|
}
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask TrackEgress(Guid id, ulong amount)
|
public ValueTask TrackEgress(Guid id, ulong amount)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
if (_egress.ContainsKey(id) && _egress.TryGetValue(id, out var v))
|
||||||
|
{
|
||||||
|
_egress.TryUpdate(id, v + amount, v);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_egress.TryAdd(id, amount);
|
||||||
|
}
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
using System.Buffers;
|
|
||||||
using VoidCat.Model;
|
|
||||||
|
|
||||||
namespace VoidCat.Services;
|
|
||||||
|
|
||||||
public class LocalDiskFileIngressFactory : IFileIngressFactory
|
|
||||||
{
|
|
||||||
private readonly VoidSettings _settings;
|
|
||||||
private readonly IStatsCollector _stats;
|
|
||||||
|
|
||||||
public LocalDiskFileIngressFactory(VoidSettings settings, IStatsCollector stats)
|
|
||||||
{
|
|
||||||
_settings = settings;
|
|
||||||
_stats = stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<VoidFile> Ingress(Stream inStream)
|
|
||||||
{
|
|
||||||
var id = Guid.NewGuid();
|
|
||||||
var fPath = mapPath(id);
|
|
||||||
using var fsTemp = new FileStream(fPath, FileMode.Create, FileAccess.ReadWrite);
|
|
||||||
|
|
||||||
var buffer = MemoryPool<byte>.Shared.Rent();
|
|
||||||
var total = 0UL;
|
|
||||||
var readLength = 0;
|
|
||||||
while ((readLength = await inStream.ReadAsync(buffer.Memory)) > 0)
|
|
||||||
{
|
|
||||||
await fsTemp.WriteAsync(buffer.Memory[..readLength]);
|
|
||||||
await _stats.TrackIngress(id, (ulong)readLength);
|
|
||||||
total += (ulong)readLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
Id = id,
|
|
||||||
Size = total
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private string mapPath(Guid id) =>
|
|
||||||
Path.Join(_settings.FilePath, id.ToString());
|
|
||||||
}
|
|
103
VoidCat/Services/LocalDiskFileStorage.cs
Normal file
103
VoidCat/Services/LocalDiskFileStorage.cs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
using System.Buffers;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using VoidCat.Model;
|
||||||
|
using VoidCat.Model.Exceptions;
|
||||||
|
|
||||||
|
namespace VoidCat.Services;
|
||||||
|
|
||||||
|
public class LocalDiskFileIngressFactory : IFileStorage
|
||||||
|
{
|
||||||
|
private readonly VoidSettings _settings;
|
||||||
|
private readonly IStatsCollector _stats;
|
||||||
|
|
||||||
|
public LocalDiskFileIngressFactory(VoidSettings settings, IStatsCollector stats)
|
||||||
|
{
|
||||||
|
_settings = settings;
|
||||||
|
_stats = stats;
|
||||||
|
|
||||||
|
if (!Directory.Exists(_settings.DataDirectory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_settings.DataDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<VoidFile?> Get(Guid id)
|
||||||
|
{
|
||||||
|
var path = MapMeta(id);
|
||||||
|
if (!File.Exists(path)) throw new VoidFileNotFoundException(id);
|
||||||
|
|
||||||
|
var json = await File.ReadAllTextAsync(path);
|
||||||
|
return JsonConvert.DeserializeObject<VoidFile>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Egress(Guid id, Stream outStream, CancellationToken cts)
|
||||||
|
{
|
||||||
|
var path = MapPath(id);
|
||||||
|
if (!File.Exists(path)) throw new VoidFileNotFoundException(id);
|
||||||
|
|
||||||
|
await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||||
|
using var buffer = MemoryPool<byte>.Shared.Rent();
|
||||||
|
var readLength = 0;
|
||||||
|
while ((readLength = await fs.ReadAsync(buffer.Memory, cts)) > 0)
|
||||||
|
{
|
||||||
|
await outStream.WriteAsync(buffer.Memory[..readLength], cts);
|
||||||
|
await _stats.TrackEgress(id, (ulong)readLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<InternalVoidFile> Ingress(Stream inStream, CancellationToken cts)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
var fPath = MapPath(id);
|
||||||
|
await using var fsTemp = new FileStream(fPath, FileMode.Create, FileAccess.ReadWrite);
|
||||||
|
|
||||||
|
using var buffer = MemoryPool<byte>.Shared.Rent();
|
||||||
|
var total = 0UL;
|
||||||
|
var readLength = 0;
|
||||||
|
while ((readLength = await inStream.ReadAsync(buffer.Memory, cts)) > 0)
|
||||||
|
{
|
||||||
|
await fsTemp.WriteAsync(buffer.Memory[..readLength], cts);
|
||||||
|
await _stats.TrackIngress(id, (ulong)readLength);
|
||||||
|
total += (ulong)readLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fm = new InternalVoidFile()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Size = total,
|
||||||
|
Uploaded = DateTimeOffset.UtcNow,
|
||||||
|
EditSecret = Guid.NewGuid()
|
||||||
|
};
|
||||||
|
|
||||||
|
var mPath = MapMeta(id);
|
||||||
|
var json = JsonConvert.SerializeObject(fm);
|
||||||
|
await File.WriteAllTextAsync(mPath, json, cts);
|
||||||
|
return fm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateInfo(VoidFile patch, Guid editSecret)
|
||||||
|
{
|
||||||
|
var path = MapMeta(patch.Id);
|
||||||
|
if (!File.Exists(path)) throw new VoidFileNotFoundException(patch.Id);
|
||||||
|
|
||||||
|
var oldJson = await File.ReadAllTextAsync(path);
|
||||||
|
var oldObj = JsonConvert.DeserializeObject<InternalVoidFile>(oldJson);
|
||||||
|
|
||||||
|
if (oldObj?.EditSecret != editSecret)
|
||||||
|
{
|
||||||
|
throw new VoidNotAllowedException("Edit secret incorrect");
|
||||||
|
}
|
||||||
|
|
||||||
|
// only patch metadata
|
||||||
|
oldObj.Metadata = patch.Metadata;
|
||||||
|
|
||||||
|
var json = JsonConvert.SerializeObject(oldObj);
|
||||||
|
await File.WriteAllTextAsync(path, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string MapPath(Guid id) =>
|
||||||
|
Path.Join(_settings.DataDirectory, id.ToString());
|
||||||
|
|
||||||
|
private string MapMeta(Guid id) =>
|
||||||
|
Path.ChangeExtension(MapPath(id), ".json");
|
||||||
|
}
|
@ -11,7 +11,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.14.0" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.14.0" />
|
||||||
|
<PackageReference Include="NBitcoin" Version="6.0.19" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Don't publish the SPA source files, but do show them in the project files list -->
|
<!-- Don't publish the SPA source files, but do show them in the project files list -->
|
||||||
|
@ -7,6 +7,6 @@
|
|||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"Settings": {
|
"Settings": {
|
||||||
"VoidSettings": "./data"
|
"DataDirectory": "./data"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,10 @@
|
|||||||
import './App.css';
|
import './App.css';
|
||||||
|
import {FilePreview} from "./FilePreview";
|
||||||
|
import {Uploader} from "./Uploader";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
let hasPath = window.location.pathname !== "/";
|
||||||
function selectFiles(e) {
|
return hasPath ? <FilePreview id={window.location.pathname.substr(1)}/> : <Uploader/>;
|
||||||
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="app">
|
|
||||||
<div className="drop" onClick={selectFiles}>
|
|
||||||
<h3>Drop files here!</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
32
VoidCat/spa/src/Const.js
Normal file
32
VoidCat/spa/src/Const.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* @constant {number} - Size of 1 kiB
|
||||||
|
*/
|
||||||
|
export const kiB = Math.pow(1024, 1);
|
||||||
|
/**
|
||||||
|
* @constant {number} - Size of 1 MiB
|
||||||
|
*/
|
||||||
|
export const MiB = Math.pow(1024, 2);
|
||||||
|
/**
|
||||||
|
* @constant {number} - Size of 1 GiB
|
||||||
|
*/
|
||||||
|
export const GiB = Math.pow(1024, 3);
|
||||||
|
/**
|
||||||
|
* @constant {number} - Size of 1 TiB
|
||||||
|
*/
|
||||||
|
export const TiB = Math.pow(1024, 4);
|
||||||
|
/**
|
||||||
|
* @constant {number} - Size of 1 PiB
|
||||||
|
*/
|
||||||
|
export const PiB = Math.pow(1024, 5);
|
||||||
|
/**
|
||||||
|
* @constant {number} - Size of 1 EiB
|
||||||
|
*/
|
||||||
|
export const EiB = Math.pow(1024, 6);
|
||||||
|
/**
|
||||||
|
* @constant {number} - Size of 1 ZiB
|
||||||
|
*/
|
||||||
|
export const ZiB = Math.pow(1024, 7);
|
||||||
|
/**
|
||||||
|
* @constant {number} - Size of 1 YiB
|
||||||
|
*/
|
||||||
|
export const YiB = Math.pow(1024, 8);
|
23
VoidCat/spa/src/FilePreview.js
Normal file
23
VoidCat/spa/src/FilePreview.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import {useEffect, useState} from "react";
|
||||||
|
|
||||||
|
export function FilePreview(props) {
|
||||||
|
let [info, setInfo] = useState();
|
||||||
|
|
||||||
|
async function loadInfo() {
|
||||||
|
let req = await fetch(`/upload/${props.id}`);
|
||||||
|
if(req.ok) {
|
||||||
|
let info = await req.json();
|
||||||
|
setInfo(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadInfo();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{info ? <a href={`/d/${info.id}`}>{info.metadata?.name ?? info.id}</a> : "Not Found"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
20
VoidCat/spa/src/FileUpload.css
Normal file
20
VoidCat/spa/src/FileUpload.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.upload {
|
||||||
|
display: flex;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid grey;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload .info {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload .status {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload dt {
|
||||||
|
font-size: 12px;
|
||||||
|
color: grey;
|
||||||
|
}
|
66
VoidCat/spa/src/FileUpload.js
Normal file
66
VoidCat/spa/src/FileUpload.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import {useEffect, useState} from "react";
|
||||||
|
|
||||||
|
import "./FileUpload.css";
|
||||||
|
import {FormatBytes} from "./Util";
|
||||||
|
|
||||||
|
export function FileUpload(props) {
|
||||||
|
let [speed, setSpeed] = useState(0);
|
||||||
|
let [progress, setProgress] = useState(0);
|
||||||
|
let [result, setResult] = useState();
|
||||||
|
|
||||||
|
async function doUpload() {
|
||||||
|
let req = await fetch("/upload", {
|
||||||
|
method: "POST",
|
||||||
|
body: props.file,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/octet-stream"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(req.ok) {
|
||||||
|
let rsp = await req.json();
|
||||||
|
console.log(rsp);
|
||||||
|
setResult(rsp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStatus() {
|
||||||
|
if(result) {
|
||||||
|
return (
|
||||||
|
<dl>
|
||||||
|
<dt>Link:</dt>
|
||||||
|
<dd><a target="_blank" href={`/${result.id}`}>{result.id}</a></dd>
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<dl>
|
||||||
|
<dt>Speed:</dt>
|
||||||
|
<dd>{FormatBytes(speed)}/s</dd>
|
||||||
|
<dt>Progress:</dt>
|
||||||
|
<dd>{(progress * 100).toFixed(0)}%</dd>
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(props.file);
|
||||||
|
doUpload();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="upload">
|
||||||
|
<div className="info">
|
||||||
|
<dl>
|
||||||
|
<dt>Name:</dt>
|
||||||
|
<dd>{props.file.name}</dd>
|
||||||
|
<dt>Size:</dt>
|
||||||
|
<dd>{FormatBytes(props.file.size)}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div className="status">
|
||||||
|
{renderStatus()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
42
VoidCat/spa/src/Uploader.js
Normal file
42
VoidCat/spa/src/Uploader.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import {Fragment, useState} from "react";
|
||||||
|
import {FileUpload} from "./FileUpload";
|
||||||
|
|
||||||
|
export function Uploader(props) {
|
||||||
|
let [files, setFiles] = useState([]);
|
||||||
|
|
||||||
|
function selectFiles(e) {
|
||||||
|
let i = document.createElement('input');
|
||||||
|
i.setAttribute('type', 'file');
|
||||||
|
i.setAttribute('multiple', '');
|
||||||
|
i.addEventListener('change', function (evt) {
|
||||||
|
setFiles(evt.target.files);
|
||||||
|
});
|
||||||
|
i.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUploads() {
|
||||||
|
let fElm = [];
|
||||||
|
for(let f of files) {
|
||||||
|
fElm.push(<FileUpload file={f}/>);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{fElm}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDrop() {
|
||||||
|
return (
|
||||||
|
<div className="drop" onClick={selectFiles}>
|
||||||
|
<h3>Drop files here!</h3>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
{files.length === 0 ? renderDrop() : renderUploads()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
27
VoidCat/spa/src/Util.js
Normal file
27
VoidCat/spa/src/Util.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import * as Const from "./Const";
|
||||||
|
/**
|
||||||
|
* Formats bytes into binary notation
|
||||||
|
* @param {number} b - The value in bytes
|
||||||
|
* @param {number} [f=2] - The number of decimal places to use
|
||||||
|
* @returns {string} Bytes formatted in binary notation
|
||||||
|
*/
|
||||||
|
export function FormatBytes(b, f) {
|
||||||
|
f = typeof f === 'number' ? 2 : f;
|
||||||
|
if (b >= Const.YiB)
|
||||||
|
return (b / Const.YiB).toFixed(f) + ' YiB';
|
||||||
|
if (b >= Const.ZiB)
|
||||||
|
return (b / Const.ZiB).toFixed(f) + ' ZiB';
|
||||||
|
if (b >= Const.EiB)
|
||||||
|
return (b / Const.EiB).toFixed(f) + ' EiB';
|
||||||
|
if (b >= Const.PiB)
|
||||||
|
return (b / Const.PiB).toFixed(f) + ' PiB';
|
||||||
|
if (b >= Const.TiB)
|
||||||
|
return (b / Const.TiB).toFixed(f) + ' TiB';
|
||||||
|
if (b >= Const.GiB)
|
||||||
|
return (b / Const.GiB).toFixed(f) + ' GiB';
|
||||||
|
if (b >= Const.MiB)
|
||||||
|
return (b / Const.MiB).toFixed(f) + ' MiB';
|
||||||
|
if (b >= Const.kiB)
|
||||||
|
return (b / Const.kiB).toFixed(f) + ' KiB';
|
||||||
|
return b.toFixed(f) + ' B'
|
||||||
|
}
|
@ -1,15 +1,19 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;700&display=swap');
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
font-family: 'Source Code Pro', monospace;
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
a {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
color: white;
|
||||||
monospace;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user