mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-04-11 02:19:03 +02:00
Previews and stats
This commit is contained in:
parent
666181abcc
commit
be4bebc97f
@ -14,6 +14,7 @@ public class DownloadController : Controller
|
||||
_storage = storage;
|
||||
}
|
||||
|
||||
[ResponseCache(Location = ResponseCacheLocation.Any, Duration = 86400)]
|
||||
[HttpGet]
|
||||
[Route("{id}")]
|
||||
public async Task DownloadFile([FromRoute] string id)
|
||||
|
35
VoidCat/Controllers/StatsController.cs
Normal file
35
VoidCat/Controllers/StatsController.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Services;
|
||||
|
||||
namespace VoidCat.Controllers
|
||||
{
|
||||
[Route("stats")]
|
||||
public class StatsController : Controller
|
||||
{
|
||||
private readonly IStatsCollector _statsCollector;
|
||||
|
||||
public StatsController(IStatsCollector statsCollector)
|
||||
{
|
||||
_statsCollector = statsCollector;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<GlobalStats> GetGlobalStats()
|
||||
{
|
||||
var bw = await _statsCollector.GetBandwidth();
|
||||
return new(bw);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}")]
|
||||
public async Task<FileStats> GetFileStats([FromRoute] string id)
|
||||
{
|
||||
var bw = await _statsCollector.GetBandwidth(id.FromBase58Guid());
|
||||
return new(bw);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record GlobalStats(Bandwidth Bandwidth);
|
||||
public sealed record FileStats(Bandwidth Bandwidth);
|
||||
}
|
@ -6,6 +6,9 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
var services = builder.Services;
|
||||
services.AddRouting();
|
||||
services.AddControllers().AddNewtonsoftJson();
|
||||
|
||||
services.AddMemoryCache();
|
||||
|
||||
services.AddScoped<IFileStorage, LocalDiskFileIngressFactory>();
|
||||
services.AddScoped<IStatsCollector, InMemoryStatsCollector>();
|
||||
|
||||
|
@ -4,5 +4,10 @@
|
||||
{
|
||||
ValueTask TrackIngress(Guid id, ulong amount);
|
||||
ValueTask TrackEgress(Guid id, ulong amount);
|
||||
|
||||
ValueTask<Bandwidth> GetBandwidth();
|
||||
ValueTask<Bandwidth> GetBandwidth(Guid id);
|
||||
}
|
||||
|
||||
public sealed record Bandwidth(ulong Ingress, ulong Egress);
|
||||
}
|
||||
|
@ -1,36 +1,52 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace VoidCat.Services
|
||||
{
|
||||
public class InMemoryStatsCollector : IStatsCollector
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, ulong> _ingress = new();
|
||||
private readonly ConcurrentDictionary<Guid, ulong> _egress = new();
|
||||
|
||||
private static Guid _global = new Guid("{A98DFDCC-C4E1-4D42-B818-912086FC6157}");
|
||||
private readonly IMemoryCache _cache;
|
||||
|
||||
public InMemoryStatsCollector(IMemoryCache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public ValueTask TrackIngress(Guid id, ulong amount)
|
||||
{
|
||||
if (_ingress.ContainsKey(id) && _ingress.TryGetValue(id, out var v))
|
||||
{
|
||||
_ingress.TryUpdate(id, v + amount, v);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ingress.TryAdd(id, amount);
|
||||
}
|
||||
Incr(IngressKey(id), amount);
|
||||
Incr(IngressKey(_global), amount);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask TrackEgress(Guid id, ulong amount)
|
||||
{
|
||||
if (_egress.ContainsKey(id) && _egress.TryGetValue(id, out var v))
|
||||
{
|
||||
_egress.TryUpdate(id, v + amount, v);
|
||||
}
|
||||
else
|
||||
{
|
||||
_egress.TryAdd(id, amount);
|
||||
}
|
||||
Incr(EgressKey(id), amount);
|
||||
Incr(EgressKey(_global), amount);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public ValueTask<Bandwidth> GetBandwidth()
|
||||
=> ValueTask.FromResult(GetBandwidthInternal(_global));
|
||||
|
||||
public ValueTask<Bandwidth> GetBandwidth(Guid id)
|
||||
=> ValueTask.FromResult(GetBandwidthInternal(id));
|
||||
|
||||
private Bandwidth GetBandwidthInternal(Guid id)
|
||||
{
|
||||
var i = _cache.Get(IngressKey(id)) as ulong?;
|
||||
var o = _cache.Get(EgressKey(id)) as ulong?;
|
||||
return new(i ?? 0UL, o ?? 0UL);
|
||||
}
|
||||
|
||||
private void Incr(string k, ulong amount)
|
||||
{
|
||||
ulong v;
|
||||
_cache.TryGetValue(k, out v);
|
||||
_cache.Set(k, v + amount);
|
||||
}
|
||||
|
||||
private string IngressKey(Guid id) => $"stats:ingress:{id}";
|
||||
private string EgressKey(Guid id) => $"stats:egress:{id}";
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,23 @@
|
||||
import { Fragment } from 'react';
|
||||
import { FilePreview } from "./FilePreview";
|
||||
import { Dropzone } from "./Dropzone";
|
||||
import { GlobalStats } from "./GlobalStats";
|
||||
|
||||
import './App.css';
|
||||
import {FilePreview} from "./FilePreview";
|
||||
import {Uploader} from "./Uploader";
|
||||
|
||||
function App() {
|
||||
let hasPath = window.location.pathname !== "/";
|
||||
return hasPath ? <FilePreview id={window.location.pathname.substr(1)}/> : <Uploader/>;
|
||||
return (
|
||||
<div className="app">
|
||||
{hasPath ? <FilePreview id={window.location.pathname.substr(1)} />
|
||||
: (
|
||||
<Fragment>
|
||||
<Dropzone />
|
||||
<GlobalStats />
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Fragment, useState} from "react";
|
||||
import {FileUpload} from "./FileUpload";
|
||||
|
||||
export function Uploader(props) {
|
||||
export function Dropzone(props) {
|
||||
let [files, setFiles] = useState([]);
|
||||
|
||||
function selectFiles(e) {
|
||||
@ -34,9 +34,5 @@ export function Uploader(props) {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
{files.length === 0 ? renderDrop() : renderUploads()}
|
||||
</div>
|
||||
);
|
||||
return files.length === 0 ? renderDrop() : renderUploads();
|
||||
}
|
@ -1,3 +1,16 @@
|
||||
.preview {
|
||||
text-align: center;
|
||||
}
|
||||
margin-top: 2vh;
|
||||
}
|
||||
|
||||
.preview > a {
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
|
||||
.preview img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preview video {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -1,23 +1,50 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import { Fragment, useEffect, useState } from "react";
|
||||
|
||||
import "./FilePreview.css";
|
||||
|
||||
export function FilePreview(props) {
|
||||
let [info, setInfo] = useState();
|
||||
|
||||
|
||||
async function loadInfo() {
|
||||
let req = await fetch(`/upload/${props.id}`);
|
||||
if(req.ok) {
|
||||
if (req.ok) {
|
||||
let info = await req.json();
|
||||
setInfo(info);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function renderTypes() {
|
||||
let link = `/d/${info.id}`;
|
||||
if (info.metadata) {
|
||||
switch (info.metadata.mimeType) {
|
||||
case "image/jpg":
|
||||
case "image/jpeg":
|
||||
case "image/png": {
|
||||
return <img src={link} alt={info.metadata.name} />;
|
||||
}
|
||||
case "video/mp4":
|
||||
case "video/matroksa":
|
||||
case "video/x-matroska": {
|
||||
return <video src={link} controls />;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadInfo();
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<div className={"preview"}>
|
||||
{info ? <a href={`/d/${info.id}`}>{info.metadata?.name ?? info.id}</a> : "Not Found"}
|
||||
</div>
|
||||
<div className="preview">
|
||||
{info ? (
|
||||
<Fragment>
|
||||
this.Download(
|
||||
<a className="btn" href={`/d/${info.id}`}>{info.metadata?.name ?? info.id}</a>)
|
||||
{renderTypes()}
|
||||
</Fragment>
|
||||
) : "Not Found"}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -23,6 +23,28 @@ export function FileUpload(props) {
|
||||
setResult(rsp);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateMetadata(result) {
|
||||
let metaReq = {
|
||||
editSecret: result.editSecret,
|
||||
metadata: {
|
||||
name: props.file.name,
|
||||
mimeType: props.file.type
|
||||
}
|
||||
};
|
||||
|
||||
let req = await fetch(`/upload/${result.id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(metaReq),
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (req.ok) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
|
||||
function renderStatus() {
|
||||
if(result) {
|
||||
@ -43,11 +65,18 @@ export function FileUpload(props) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log(props.file);
|
||||
doUpload();
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (result) {
|
||||
updateMetadata(result);
|
||||
}
|
||||
}, [result]);
|
||||
|
||||
return (
|
||||
<div className="upload">
|
||||
<div className="info">
|
||||
|
5
VoidCat/spa/src/GlobalStats.css
Normal file
5
VoidCat/spa/src/GlobalStats.css
Normal file
@ -0,0 +1,5 @@
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
margin: 0 100px;
|
||||
}
|
26
VoidCat/spa/src/GlobalStats.js
Normal file
26
VoidCat/spa/src/GlobalStats.js
Normal file
@ -0,0 +1,26 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormatBytes } from "./Util";
|
||||
|
||||
import "./GlobalStats.css";
|
||||
|
||||
export function GlobalStats(props) {
|
||||
let [stats, setStats] = useState();
|
||||
|
||||
async function loadStats() {
|
||||
let req = await fetch("/stats");
|
||||
if (req.ok) {
|
||||
setStats(await req.json());
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => loadStats(), []);
|
||||
|
||||
return (
|
||||
<div className="stats">
|
||||
<div>Ingress:</div>
|
||||
<div>{FormatBytes(stats?.bandwidth?.ingress ?? 0)}</div>
|
||||
<div>Egress:</div>
|
||||
<div>{FormatBytes(stats?.bandwidth?.egress ?? 0)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -16,4 +16,18 @@ a {
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
line-height: 1.3;
|
||||
font-size: large;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
border-radius: 20px;
|
||||
background-color: white;
|
||||
color: black;
|
||||
padding: 10px 30px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user