Merge pull request #236 from NebulousLabs/improve-error-reporting

improve upload and retrieve frontend errors reporting
This commit is contained in:
Karol Wypchło 2020-05-28 15:17:07 +02:00 committed by GitHub
commit 7adb89a825
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 141 additions and 53 deletions

View File

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

View File

@ -1,5 +1,7 @@
import React, { useState, useContext, useEffect } from "react";
import bytes from "bytes";
import classNames from "classnames";
import HttpStatus from "http-status-codes";
import path from "path-browserify";
import { useDropzone } from "react-dropzone";
import Reveal from "react-reveal/Reveal";
@ -7,12 +9,69 @@ import { Button, UploadFile } from "../";
import { Deco3, Deco4, Deco5, Folder, DownArrow } from "../../svg";
import "./HomeUpload.scss";
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() {
const [files, setFiles] = useState([]);
const [skylink, setSkylink] = useState("");
const { apiUrl } = useContext(AppContext);
const [directoryMode, setDirectoryMode] = useState(false);
const client = new SkynetClient(apiUrl);
useEffect(() => {
if (directoryMode) {
@ -22,23 +81,6 @@ export default function HomeUpload() {
}
}, [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) => {
if (directoryMode && acceptedFiles.length) {
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) => {
const uploadUrl = `${apiUrl}/skynet/skyfile/${directory ? `?filename=${encodeURIComponent(directory)}` : ""}`;
const { data } = await axios.post(uploadUrl, formData, {
onUploadProgress: ({ loaded, total }) => {
const progress = loaded / total;
const status = progress === 1 ? "processing" : "uploading";
onFileStateChange(file, { status, progress });
},
});
return data;
};
acceptedFiles.forEach(async (file) => {
const onUploadProgress = ({ loaded, total }) => {
const progress = loaded / total;
const status = progress === 1 ? "processing" : "uploading";
onFileStateChange(file, { status, progress });
};
// Reject files larger than our hard limit of 1 GB with proper message
if (file.size > bytes("1 GB")) {
onFileStateChange(file, { status: "error", error: "This file size exceeds the maximum allowed size of 1 GB." });
return;
}
try {
const formData = new FormData();
let response;
if (file.directory) {
file.files.forEach((directoryFile) => {
const relativeFilePath = getRelativeFilePath(directoryFile);
const directory = file.files.reduce((acc, file) => ({ ...acc, [getRelativeFilePath(file)]: file }), {});
formData.append("files[]", directoryFile, relativeFilePath);
});
response = await client.uploadDirectory(directory, encodeURIComponent(file.name), { onUploadProgress });
} else {
formData.append("file", file);
response = await client.upload(file, { onUploadProgress });
}
const { skylink } = await upload(formData, directoryMode && file.name, file);
onFileStateChange(file, { status: "complete", url: `${apiUrl}/${skylink}` });
onFileStateChange(file, { status: "complete", url: client.getUrl(response.skylink) });
} catch (error) {
onFileStateChange(file, { status: "error" });
onFileStateChange(file, { status: "error", error: createUploadErrorMessage(error) });
}
});
};
@ -105,10 +143,9 @@ export default function HomeUpload() {
const handleSkylink = (event) => {
event.preventDefault();
const skylink = event.target.skylink.value.replace("sia://", "");
if (skylink.match(/^[a-zA-Z0-9_-]{46}$/)) {
window.open(skylink, "_blank");
// only try to open a valid skylink
if (isValidSkylink(skylink)) {
client.open(skylink);
}
};
@ -163,8 +200,17 @@ export default function HomeUpload() {
<h3 id="skylink-retrieve-title">Have a Skylink?</h3>
<p>Paste the link to retrieve your file</p>
<form className="home-upload-retrieve-form" onSubmit={handleSkylink}>
<input name="skylink" type="text" placeholder="sia://" aria-labelledby="skylink-retrieve-title" />
<form
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">
<DownArrow />
</button>

View File

@ -245,4 +245,29 @@
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;
border-color: $red;
}
}
}
}

View File

@ -4,7 +4,7 @@ import "./UploadFile.scss";
import { LoadingSpinner } from "../";
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 urlRef = useRef(null);
@ -45,7 +45,7 @@ export default function UploadFile({ file, url, status, progress }) {
<p>
{status === "uploading" && (progress ? `Uploading ${Math.round(progress * 100)}%` : "Uploading...")}
{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" && (
<a href={url} className="url green-text" target="_blank" rel="noopener noreferrer">
{url}
@ -80,4 +80,5 @@ UploadFile.propTypes = {
status: PropTypes.string.isRequired,
url: PropTypes.string,
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"
integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
bytes@3.1.0:
bytes@3.1.0, bytes@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
@ -7224,6 +7224,11 @@ http-signature@~1.2.0:
jsprim "^1.2.2"
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:
version "1.0.0"
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"
integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
skynet-js@^0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/skynet-js/-/skynet-js-0.0.5.tgz#f2bd8a70c50263d461486f490b3b3d0856d1e0d0"
integrity sha512-XJ2vWCe3PUsvKhebdgM3FGZxqkAtLBz6xQ3HSbwqNWJDgHtyKWwIBb6vE2GqSC684zBT1MfxlTPgxBkbpx8hpg==
dependencies:
axios "^0.19.2"
url-parse "^1.4.7"
slash@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
@ -13821,7 +13834,7 @@ url-parse-lax@^3.0.0:
dependencies:
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"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==