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;
}
Response.Headers.XFrameOptions = "SAMEORIGIN";
Response.Headers.ContentDisposition = $"inline; filename=\"{meta?.Metadata?.Name}\"";
Response.ContentType = meta?.Metadata?.MimeType ?? "application/octet-stream";
await _storage.Egress(gid, Response.Body, HttpContext.RequestAborted);
}

View File

@ -22,8 +22,14 @@ namespace VoidCat.Controllers
[DisableFormValueModelBinding]
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 ?
saveFromForm() : _storage.Ingress(Request.Body, HttpContext.RequestAborted);
saveFromForm() : _storage.Ingress(Request.Body, meta, HttpContext.RequestAborted);
}
[HttpGet]

View File

@ -6,7 +6,7 @@ namespace VoidCat.Services
{
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);

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 fPath = MapPath(id);
@ -65,6 +65,7 @@ public class LocalDiskFileIngressFactory : IFileStorage
{
Id = id,
Size = total,
Metadata = meta,
Uploaded = DateTimeOffset.UtcNow,
EditSecret = Guid.NewGuid()
};

View File

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

View File

@ -2,18 +2,57 @@ import {useEffect, useState} from "react";
import "./FileUpload.css";
import {FormatBytes} from "./Util";
import {RateCalculator} from "./RateCalculator";
export function FileUpload(props) {
let [speed, setSpeed] = useState(0);
let [progress, setProgress] = useState(0);
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", {
method: "POST",
body: props.file,
body: rs,
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) {
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"
}
async function doXHRUpload() {
let xhr = await new Promise((resolve, reject) => {
let req = new XMLHttpRequest();
req.onreadystatechange = (ev) => {
if(req.readyState === XMLHttpRequest.DONE && req.status === 200) {
let rsp = JSON.parse(req.responseText);
resolve(rsp);
}
};
req.upload.onprogress = handleProgress;
req.open("POST", "/upload");
req.setRequestHeader("Content-Type", props.file.type);
req.setRequestHeader("X-Filename", props.file.name);
req.send(props.file);
});
if (req.ok) {
// nothing
}
setResult(xhr);
}
function renderStatus() {
@ -68,15 +104,9 @@ export function FileUpload(props) {
useEffect(() => {
console.log(props.file);
doUpload();
doXHRUpload();
}, []);
useEffect(() => {
if (result) {
updateMetadata(result);
}
}, [result]);
return (
<div className="upload">
<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>
)
}