forked from Kieran/void.cat
Add progress for uploads
This commit is contained in:
parent
e060c80dfc
commit
6d37b42d11
@ -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);
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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()
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
34
VoidCat/spa/src/RateCalculator.js
Normal file
34
VoidCat/spa/src/RateCalculator.js
Normal 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;
|
||||
}
|
||||
}
|
6
VoidCat/spa/src/TextPreview.css
Normal file
6
VoidCat/spa/src/TextPreview.css
Normal file
@ -0,0 +1,6 @@
|
||||
.text-preview {
|
||||
border: 1px dashed;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
text-align: initial;
|
||||
}
|
21
VoidCat/spa/src/TextPreview.js
Normal file
21
VoidCat/spa/src/TextPreview.js
Normal 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>
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user