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;
|
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);
|
||||||
}
|
}
|
||||||
|
@ -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]
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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()
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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">
|
||||||
|
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