improve upload and retrieve frontend errors reporting

This commit is contained in:
Karol Wypchlo 2020-05-28 14:30:08 +02:00
parent a2f02aa5ae
commit 8c17996da8
5 changed files with 143 additions and 53 deletions

View File

@ -5,6 +5,7 @@
"author": "Nebulous", "author": "Nebulous",
"dependencies": { "dependencies": {
"axios": "^0.19.2", "axios": "^0.19.2",
"bytes": "^3.1.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"gatsby": "^2.22.11", "gatsby": "^2.22.11",
"gatsby-image": "^2.4.5", "gatsby-image": "^2.4.5",
@ -17,6 +18,7 @@
"gatsby-plugin-sharp": "^2.6.9", "gatsby-plugin-sharp": "^2.6.9",
"gatsby-source-filesystem": "^2.3.8", "gatsby-source-filesystem": "^2.3.8",
"gatsby-transformer-sharp": "^2.5.3", "gatsby-transformer-sharp": "^2.5.3",
"http-status-codes": "^1.4.0",
"jsonp": "^0.2.1", "jsonp": "^0.2.1",
"node-sass": "^4.14.0", "node-sass": "^4.14.0",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
@ -30,7 +32,8 @@
"react-mailchimp-subscribe": "^2.1.0", "react-mailchimp-subscribe": "^2.1.0",
"react-reveal": "^1.2.2", "react-reveal": "^1.2.2",
"react-syntax-highlighter": "^12.2.1", "react-syntax-highlighter": "^12.2.1",
"react-visibility-sensor": "^5.1.1" "react-visibility-sensor": "^5.1.1",
"skynet-js": "^0.0.4"
}, },
"devDependencies": { "devDependencies": {
"cypress": "^4.7.0", "cypress": "^4.7.0",

View File

@ -1,5 +1,7 @@
import React, { useState, useContext, useEffect } from "react"; import React, { useState, useContext, useEffect } from "react";
import bytes from "bytes";
import classNames from "classnames"; import classNames from "classnames";
import HttpStatus from "http-status-codes";
import path from "path-browserify"; import path from "path-browserify";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import Reveal from "react-reveal/Reveal"; import Reveal from "react-reveal/Reveal";
@ -7,12 +9,69 @@ import { Button, UploadFile } from "../";
import { Deco3, Deco4, Deco5, Folder, DownArrow } from "../../svg"; import { Deco3, Deco4, Deco5, Folder, DownArrow } from "../../svg";
import "./HomeUpload.scss"; import "./HomeUpload.scss";
import AppContext from "../../AppContext"; import AppContext from "../../AppContext";
import axios from "axios"; import SkynetClient, { parseSkylink } from "skynet-js";
const isValidSkylink = (skylink) => {
try {
parseSkylink(skylink); // try to parse the skylink, it will throw on error
} catch (error) {
return false;
}
return true;
};
const getFilePath = (file) => file.webkitRelativePath || file.path || file.name;
const getRelativeFilePath = (file) => {
const filePath = getFilePath(file);
const { root, dir, base } = path.parse(filePath);
const relative = path.normalize(dir).slice(root.length).split(path.sep).slice(1);
return path.join(...relative, base);
};
const getRootDirectory = (file) => {
const filePath = getFilePath(file);
const { root, dir } = path.parse(filePath);
return path.normalize(dir).slice(root.length).split(path.sep)[0];
};
const createUploadErrorMessage = (error) => {
// The request was made and the server responded with a status code that falls out of the range of 2xx
if (error.response) {
if (error.response.data.message) {
return `Upload failed with error: ${error.response.data.message}`;
}
const statusCode = error.response.status;
const statusText = HttpStatus.getStatusText(error.response.status);
return `Upload failed, our server received your request but failed with status code: ${statusCode} ${statusText}`;
}
// The request was made but no response was received. The best we can do is detect whether browser is online.
// This will be triggered mostly if the server is offline or misconfigured and doesn't respond to valid request.
if (error.request) {
if (!navigator.onLine) {
return "You are offline, please connect to the internet and try again";
}
// TODO: We should add a note "our team has been notified" and have some kind of notification with this error.
return "Server failed to respond to your request, please try again later.";
}
// TODO: We should add a note "our team has been notified" and have some kind of notification with this error.
return `Critical error, please refresh the application and try again. ${error.message}`;
};
export default function HomeUpload() { export default function HomeUpload() {
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
const [skylink, setSkylink] = useState("");
const { apiUrl } = useContext(AppContext); const { apiUrl } = useContext(AppContext);
const [directoryMode, setDirectoryMode] = useState(false); const [directoryMode, setDirectoryMode] = useState(false);
const client = new SkynetClient(apiUrl);
useEffect(() => { useEffect(() => {
if (directoryMode) { if (directoryMode) {
@ -22,23 +81,6 @@ export default function HomeUpload() {
} }
}, [directoryMode]); }, [directoryMode]);
const getFilePath = (file) => file.webkitRelativePath || file.path || file.name;
const getRelativeFilePath = (file) => {
const filePath = getFilePath(file);
const { root, dir, base } = path.parse(filePath);
const relative = path.normalize(dir).slice(root.length).split(path.sep).slice(1);
return path.join(...relative, base);
};
const getRootDirectory = (file) => {
const filePath = getFilePath(file);
const { root, dir } = path.parse(filePath);
return path.normalize(dir).slice(root.length).split(path.sep)[0];
};
const handleDrop = async (acceptedFiles) => { const handleDrop = async (acceptedFiles) => {
if (directoryMode && acceptedFiles.length) { if (directoryMode && acceptedFiles.length) {
const rootDir = getRootDirectory(acceptedFiles[0]); // get the file path from the first file const rootDir = getRootDirectory(acceptedFiles[0]); // get the file path from the first file
@ -63,39 +105,35 @@ export default function HomeUpload() {
}); });
}; };
const upload = async (formData, directory, file) => { acceptedFiles.forEach(async (file) => {
const uploadUrl = `${apiUrl}/skynet/skyfile/${directory ? `?filename=${encodeURIComponent(directory)}` : ""}`; const onUploadProgress = ({ loaded, total }) => {
const { data } = await axios.post(uploadUrl, formData, {
onUploadProgress: ({ loaded, total }) => {
const progress = loaded / total; const progress = loaded / total;
const status = progress === 1 ? "processing" : "uploading"; const status = progress === 1 ? "processing" : "uploading";
onFileStateChange(file, { status, progress }); onFileStateChange(file, { status, progress });
},
});
return data;
}; };
acceptedFiles.forEach(async (file) => { // Reject files larger than our hard limit of 1 GB with proper message
try { if (file.size > bytes("1 GB")) {
const formData = new FormData(); onFileStateChange(file, { status: "error", error: "This file size exceeds the maximum allowed size of 1 GB." });
if (file.directory) { return;
file.files.forEach((directoryFile) => {
const relativeFilePath = getRelativeFilePath(directoryFile);
formData.append("files[]", directoryFile, relativeFilePath);
});
} else {
formData.append("file", file);
} }
const { skylink } = await upload(formData, directoryMode && file.name, file); try {
let response;
onFileStateChange(file, { status: "complete", url: `${apiUrl}/${skylink}` }); if (file.directory) {
const directory = file.files.reduce((acc, file) => ({ ...acc, [getRelativeFilePath(file)]: file }), {});
response = await client.uploadDirectory(directory, encodeURIComponent(file.name), { onUploadProgress });
} else {
response = await client.upload(file, { onUploadProgress });
}
onFileStateChange(file, { status: "complete", url: client.getUrl(response.skylink) });
} catch (error) { } catch (error) {
onFileStateChange(file, { status: "error" }); onFileStateChange(file, { status: "error", error: createUploadErrorMessage(error) });
} }
}); });
}; };
@ -105,10 +143,9 @@ export default function HomeUpload() {
const handleSkylink = (event) => { const handleSkylink = (event) => {
event.preventDefault(); event.preventDefault();
const skylink = event.target.skylink.value.replace("sia://", ""); // only try to open a valid skylink
if (isValidSkylink(skylink)) {
if (skylink.match(/^[a-zA-Z0-9_-]{46}$/)) { client.open(skylink);
window.open(skylink, "_blank");
} }
}; };
@ -163,8 +200,17 @@ export default function HomeUpload() {
<h3 id="skylink-retrieve-title">Have a Skylink?</h3> <h3 id="skylink-retrieve-title">Have a Skylink?</h3>
<p>Paste the link to retrieve your file</p> <p>Paste the link to retrieve your file</p>
<form className="home-upload-retrieve-form" onSubmit={handleSkylink}> <form
<input name="skylink" type="text" placeholder="sia://" aria-labelledby="skylink-retrieve-title" /> className={classNames("home-upload-retrieve-form", { invalid: skylink && !isValidSkylink(skylink) })}
onSubmit={handleSkylink}
>
<input
name="skylink"
type="text"
placeholder="sia://"
aria-labelledby="skylink-retrieve-title"
onChange={(event) => setSkylink(event.target.value)}
/>
<button type="submit" aria-label="Retrieve file"> <button type="submit" aria-label="Retrieve file">
<DownArrow /> <DownArrow />
</button> </button>

View File

@ -245,4 +245,31 @@
border-color: $green; border-color: $green;
} }
} }
&.invalid {
input {
border-color: $red;
}
input:hover,
input:hover + button {
border-color: $red;
}
input:focus,
input:focus + button {
border-color: $red;
}
button {
border-color: $red;
&:hover {
background: $red;
// color: #cbd3cd;
border-color: $red;
// cursor: auto;
}
}
}
} }

View File

@ -4,7 +4,7 @@ import "./UploadFile.scss";
import { LoadingSpinner } from "../"; import { LoadingSpinner } from "../";
import { File, FileCheck, FileError, Copy } from "../../svg"; import { File, FileCheck, FileError, Copy } from "../../svg";
export default function UploadFile({ file, url, status, progress }) { export default function UploadFile({ file, url, status, progress, error }) {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const urlRef = useRef(null); const urlRef = useRef(null);
@ -45,7 +45,7 @@ export default function UploadFile({ file, url, status, progress }) {
<p> <p>
{status === "uploading" && (progress ? `Uploading ${Math.round(progress * 100)}%` : "Uploading...")} {status === "uploading" && (progress ? `Uploading ${Math.round(progress * 100)}%` : "Uploading...")}
{status === "processing" && "Processing..."} {status === "processing" && "Processing..."}
{status === "error" && <span className="red-text">Error processing file.</span>} {status === "error" && <span className="red-text">{error || "Upload failed."}</span>}
{status === "complete" && ( {status === "complete" && (
<a href={url} className="url green-text" target="_blank" rel="noopener noreferrer"> <a href={url} className="url green-text" target="_blank" rel="noopener noreferrer">
{url} {url}
@ -80,4 +80,5 @@ UploadFile.propTypes = {
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
url: PropTypes.string, url: PropTypes.string,
progress: PropTypes.number, progress: PropTypes.number,
error: PropTypes.string,
}; };

View File

@ -2910,7 +2910,7 @@ bytes@3.0.0:
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
bytes@3.1.0: bytes@3.1.0, bytes@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
@ -7224,6 +7224,11 @@ http-signature@~1.2.0:
jsprim "^1.2.2" jsprim "^1.2.2"
sshpk "^1.7.0" sshpk "^1.7.0"
http-status-codes@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-1.4.0.tgz#6e4c15d16ff3a9e2df03b89f3a55e1aae05fb477"
integrity sha512-JrT3ua+WgH8zBD3HEJYbeEgnuQaAnUeRRko/YojPAJjGmIfGD3KPU/asLdsLwKjfxOmQe5nXMQ0pt/7MyapVbQ==
https-browserify@^1.0.0: https-browserify@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
@ -12365,6 +12370,14 @@ sisteransi@^1.0.4:
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
skynet-js@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/skynet-js/-/skynet-js-0.0.4.tgz#25fe28c07024c56727b5900444884bfe4daf50e4"
integrity sha512-JFQlIG7x6i+iACeP44XSsYOdkHVG0a2r3qP/00MDCNlH3YXYVNzfLYJUn7tWmguSM3uRQxxKRKymw221svjdvw==
dependencies:
axios "^0.19.2"
url-parse "^1.4.7"
slash@^1.0.0: slash@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
@ -13821,7 +13834,7 @@ url-parse-lax@^3.0.0:
dependencies: dependencies:
prepend-http "^2.0.0" prepend-http "^2.0.0"
url-parse@^1.1.8, url-parse@^1.4.3: url-parse@^1.1.8, url-parse@^1.4.3, url-parse@^1.4.7:
version "1.4.7" version "1.4.7"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==