improve upload and retrieve frontend errors reporting
This commit is contained in:
parent
a2f02aa5ae
commit
8c17996da8
|
@ -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",
|
||||||
|
|
|
@ -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,20 +9,17 @@ 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";
|
||||||
|
|
||||||
export default function HomeUpload() {
|
const isValidSkylink = (skylink) => {
|
||||||
const [files, setFiles] = useState([]);
|
try {
|
||||||
const { apiUrl } = useContext(AppContext);
|
parseSkylink(skylink); // try to parse the skylink, it will throw on error
|
||||||
const [directoryMode, setDirectoryMode] = useState(false);
|
} catch (error) {
|
||||||
|
return false;
|
||||||
useEffect(() => {
|
|
||||||
if (directoryMode) {
|
|
||||||
inputRef.current.setAttribute("webkitdirectory", "true");
|
|
||||||
} else {
|
|
||||||
inputRef.current.removeAttribute("webkitdirectory");
|
|
||||||
}
|
}
|
||||||
}, [directoryMode]);
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const getFilePath = (file) => file.webkitRelativePath || file.path || file.name;
|
const getFilePath = (file) => file.webkitRelativePath || file.path || file.name;
|
||||||
|
|
||||||
|
@ -39,6 +38,49 @@ export default function HomeUpload() {
|
||||||
return path.normalize(dir).slice(root.length).split(path.sep)[0];
|
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) {
|
||||||
|
inputRef.current.setAttribute("webkitdirectory", "true");
|
||||||
|
} else {
|
||||||
|
inputRef.current.removeAttribute("webkitdirectory");
|
||||||
|
}
|
||||||
|
}, [directoryMode]);
|
||||||
|
|
||||||
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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
17
yarn.lock
17
yarn.lock
|
@ -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==
|
||||||
|
|
Reference in New Issue