From 8c17996da85c45ea1d1a4e31d08f5ee5fbb9641e Mon Sep 17 00:00:00 2001 From: Karol Wypchlo Date: Thu, 28 May 2020 14:30:08 +0200 Subject: [PATCH] improve upload and retrieve frontend errors reporting --- package.json | 5 +- src/components/HomeUpload/HomeUpload.js | 142 ++++++++++++++-------- src/components/HomeUpload/HomeUpload.scss | 27 ++++ src/components/UploadFile/UploadFile.js | 5 +- yarn.lock | 17 ++- 5 files changed, 143 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index 402e423f..ee453a48 100644 --- a/package.json +++ b/package.json @@ -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.4" }, "devDependencies": { "cypress": "^4.7.0", diff --git a/src/components/HomeUpload/HomeUpload.js b/src/components/HomeUpload/HomeUpload.js index 924f6c6c..7a3e7699 100644 --- a/src/components/HomeUpload/HomeUpload.js +++ b/src/components/HomeUpload/HomeUpload.js @@ -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() {

Paste the link to retrieve your file

-
- + + setSkylink(event.target.value)} + /> diff --git a/src/components/HomeUpload/HomeUpload.scss b/src/components/HomeUpload/HomeUpload.scss index 714592f6..e006ed4f 100644 --- a/src/components/HomeUpload/HomeUpload.scss +++ b/src/components/HomeUpload/HomeUpload.scss @@ -245,4 +245,31 @@ 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; + } + } + } } diff --git a/src/components/UploadFile/UploadFile.js b/src/components/UploadFile/UploadFile.js index 676fa934..6d134299 100644 --- a/src/components/UploadFile/UploadFile.js +++ b/src/components/UploadFile/UploadFile.js @@ -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 }) {

{status === "uploading" && (progress ? `Uploading ${Math.round(progress * 100)}%` : "Uploading...")} {status === "processing" && "Processing..."} - {status === "error" && Error processing file.} + {status === "error" && {error || "Upload failed."}} {status === "complete" && ( {url} @@ -80,4 +80,5 @@ UploadFile.propTypes = { status: PropTypes.string.isRequired, url: PropTypes.string, progress: PropTypes.number, + error: PropTypes.string, }; diff --git a/yarn.lock b/yarn.lock index 9285e84a..9cf879ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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.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: 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==