Add progress for uploads

This commit is contained in:
Kieran 2022-02-03 23:06:39 +00:00
parent e060c80dfc
commit 6d37b42d11
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
9 changed files with 140 additions and 33 deletions

View File

@ -27,6 +27,8 @@ public class DownloadController : Controller
return; return;
} }
Response.Headers.XFrameOptions = "SAMEORIGIN";
Response.Headers.ContentDisposition = $"inline; filename=\"{meta?.Metadata?.Name}\"";
Response.ContentType = meta?.Metadata?.MimeType ?? "application/octet-stream"; Response.ContentType = meta?.Metadata?.MimeType ?? "application/octet-stream";
await _storage.Egress(gid, Response.Body, HttpContext.RequestAborted); await _storage.Egress(gid, Response.Body, HttpContext.RequestAborted);
} }

View File

@ -22,8 +22,14 @@ namespace VoidCat.Controllers
[DisableFormValueModelBinding] [DisableFormValueModelBinding]
public Task<InternalVoidFile> UploadFile() public Task<InternalVoidFile> UploadFile()
{ {
var meta = new VoidFileMeta()
{
MimeType = Request.ContentType,
Name = Request.Headers
.FirstOrDefault(a => a.Key.Equals("X-Filename", StringComparison.InvariantCultureIgnoreCase)).Value.ToString()
};
return Request.HasFormContentType ? return Request.HasFormContentType ?
saveFromForm() : _storage.Ingress(Request.Body, HttpContext.RequestAborted); saveFromForm() : _storage.Ingress(Request.Body, meta, HttpContext.RequestAborted);
} }
[HttpGet] [HttpGet]

View File

@ -6,7 +6,7 @@ namespace VoidCat.Services
{ {
Task<VoidFile?> Get(Guid id); Task<VoidFile?> Get(Guid id);
Task<InternalVoidFile> Ingress(Stream inStream, CancellationToken cts); Task<InternalVoidFile> Ingress(Stream inStream, VoidFileMeta meta, CancellationToken cts);
Task Egress(Guid id, Stream outStream, CancellationToken cts); Task Egress(Guid id, Stream outStream, CancellationToken cts);

View File

@ -45,7 +45,7 @@ public class LocalDiskFileIngressFactory : IFileStorage
} }
} }
public async Task<InternalVoidFile> Ingress(Stream inStream, CancellationToken cts) public async Task<InternalVoidFile> Ingress(Stream inStream, VoidFileMeta meta, CancellationToken cts)
{ {
var id = Guid.NewGuid(); var id = Guid.NewGuid();
var fPath = MapPath(id); var fPath = MapPath(id);
@ -65,6 +65,7 @@ public class LocalDiskFileIngressFactory : IFileStorage
{ {
Id = id, Id = id,
Size = total, Size = total,
Metadata = meta,
Uploaded = DateTimeOffset.UtcNow, Uploaded = DateTimeOffset.UtcNow,
EditSecret = Guid.NewGuid() EditSecret = Guid.NewGuid()
}; };

View File

@ -1,6 +1,7 @@
import { Fragment, useEffect, useState } from "react"; import { Fragment, useEffect, useState } from "react";
import "./FilePreview.css"; import "./FilePreview.css";
import {TextPreview} from "./TextPreview";
export function FilePreview(props) { export function FilePreview(props) {
let [info, setInfo] = useState(); let [info, setInfo] = useState();
@ -12,7 +13,7 @@ export function FilePreview(props) {
setInfo(info); setInfo(info);
} }
} }
function renderTypes() { function renderTypes() {
let link = `/d/${info.id}`; let link = `/d/${info.id}`;
if (info.metadata) { if (info.metadata) {
@ -28,6 +29,12 @@ export function FilePreview(props) {
case "video/webm": { case "video/webm": {
return <video src={link} controls />; return <video src={link} controls />;
} }
case "text/plain":{
return <TextPreview link={link}></TextPreview>;
}
case "application/pdf": {
return <object data={link}/>;
}
} }
} }
return null; return null;

View File

@ -2,18 +2,57 @@ import {useEffect, useState} from "react";
import "./FileUpload.css"; import "./FileUpload.css";
import {FormatBytes} from "./Util"; import {FormatBytes} from "./Util";
import {RateCalculator} from "./RateCalculator";
export function FileUpload(props) { export function FileUpload(props) {
let [speed, setSpeed] = useState(0); let [speed, setSpeed] = useState(0);
let [progress, setProgress] = useState(0); let [progress, setProgress] = useState(0);
let [result, setResult] = useState(); let [result, setResult] = useState();
let calc = new RateCalculator();
async function doUpload() { function handleProgress(e) {
console.log(e);
if(e instanceof ProgressEvent) {
let newProgress = e.loaded / e.total;
calc.ReportLoaded(e.loaded);
setSpeed(calc.RateWindow(5));
setProgress(newProgress);
}
}
async function doStreamUpload() {
let offset = 0;
let rs = new ReadableStream({
start: (controller) => {
},
pull: async (controller) => {
if(offset > props.file.size) {
controller.cancel();
}
let requestedSize = props.file.size / controller.desiredSize;
console.log(`Reading ${requestedSize} Bytes`);
let end = Math.min(offset + requestedSize, props.file.size);
let blob = props.file.slice(offset, end, props.file.type);
controller.enqueue(await blob.arrayBuffer());
offset += blob.size;
},
cancel: (reason) => {
}
}, {
highWaterMark: 100
});
let req = await fetch("/upload", { let req = await fetch("/upload", {
method: "POST", method: "POST",
body: props.file, body: rs,
headers: { headers: {
"content-type": "application/octet-stream" "Content-Type": props.file.type,
"X-Filename": props.file.name
} }
}); });
@ -24,26 +63,23 @@ export function FileUpload(props) {
} }
} }
async function updateMetadata(result) { async function doXHRUpload() {
let metaReq = { let xhr = await new Promise((resolve, reject) => {
editSecret: result.editSecret, let req = new XMLHttpRequest();
metadata: { req.onreadystatechange = (ev) => {
name: props.file.name, if(req.readyState === XMLHttpRequest.DONE && req.status === 200) {
mimeType: props.file.type let rsp = JSON.parse(req.responseText);
} resolve(rsp);
}; }
};
let req = await fetch(`/upload/${result.id}`, { req.upload.onprogress = handleProgress;
method: "PATCH", req.open("POST", "/upload");
body: JSON.stringify(metaReq), req.setRequestHeader("Content-Type", props.file.type);
headers: { req.setRequestHeader("X-Filename", props.file.name);
"content-type": "application/json" req.send(props.file);
}
}); });
if (req.ok) { setResult(xhr);
// nothing
}
} }
function renderStatus() { function renderStatus() {
@ -68,15 +104,9 @@ export function FileUpload(props) {
useEffect(() => { useEffect(() => {
console.log(props.file); console.log(props.file);
doUpload(); doXHRUpload();
}, []); }, []);
useEffect(() => {
if (result) {
updateMetadata(result);
}
}, [result]);
return ( return (
<div className="upload"> <div className="upload">
<div className="info"> <div className="info">

View File

@ -0,0 +1,34 @@
export class RateCalculator {
constructor() {
this.reports = [];
this.lastLoaded = 0;
}
ReportProgress(amount) {
this.reports.push({
time: new Date().getTime(),
amount
});
}
ReportLoaded(loaded) {
this.reports.push({
time: new Date().getTime(),
amount: loaded - this.lastLoaded
});
this.lastLoaded = loaded;
}
RateWindow(s) {
let total = 0.0;
let windowStart = new Date().getTime() - (s * 1000);
for(let r of this.reports) {
if(r.time >= windowStart) {
total += r.amount;
}
}
return total / s;
}
}

View File

@ -0,0 +1,6 @@
.text-preview {
border: 1px dashed;
padding: 10px;
border-radius: 10px;
text-align: initial;
}

View File

@ -0,0 +1,21 @@
import {useEffect, useState} from "react";
import "./TextPreview.css";
export function TextPreview(props) {
let [content, setContent] = useState("Loading..");
async function getContent(link) {
let req = await fetch(link);
if(req.ok) {
setContent(await req.text());
}
}
useEffect(() => {
getContent(props.link);
}, []);
return (
<pre className="text-preview">{content}</pre>
)
}