From 5d4f58eddc68564a1ebb1ee40f44529820824e59 Mon Sep 17 00:00:00 2001 From: jimmylee Date: Sat, 1 Aug 2020 23:00:04 -0700 Subject: [PATCH] media object: adds PDF support, validation check to restrict certain uploads --- common/validations.js | 12 ++++ components/core/Application.js | 9 +-- components/core/MediaObject.js | 69 +++++++++++++++++++ components/sidebars/SidebarAddFileToBucket.js | 38 ++++------ node_common/upload.js | 20 +++--- pages/api/slates/add-url.js | 11 +-- scenes/SceneEditAccount.js | 42 ++++------- scenes/SceneFile.js | 66 ++---------------- scenes/SceneFilesFolder.js | 18 ++--- scenes/SceneHome.js | 30 +++----- scenes/SceneSlate.js | 7 +- 11 files changed, 149 insertions(+), 173 deletions(-) create mode 100644 components/core/MediaObject.js diff --git a/common/validations.js b/common/validations.js index 43088e62..aa05ccbb 100644 --- a/common/validations.js +++ b/common/validations.js @@ -26,3 +26,15 @@ export const password = (text) => { return true; }; + +export const isFileTypeAllowed = (type = "") => { + if (type.startsWith("application/pdf")) { + return true; + } + + if (type.startsWith("image/")) { + return true; + } + + return false; +}; diff --git a/components/core/Application.js b/components/core/Application.js index a80d01ae..feed6a8a 100644 --- a/components/core/Application.js +++ b/components/core/Application.js @@ -3,6 +3,7 @@ import * as NavigationData from "~/common/navigation-data"; import * as Actions from "~/common/actions"; import * as State from "~/common/state"; import * as Credentials from "~/common/credentials"; +import * as Validations from "~/common/validations"; // NOTE(jim): // Scenes each have an ID and can be navigated to with _handleAction @@ -11,7 +12,6 @@ import SceneEditAccount from "~/scenes/SceneEditAccount"; import SceneFile from "~/scenes/SceneFile"; import SceneFilesFolder from "~/scenes/SceneFilesFolder"; import SceneHome from "~/scenes/SceneHome"; -import SceneMiners from "~/scenes/SceneMiners"; import SceneSettings from "~/scenes/SceneSettings"; import SceneWallet from "~/scenes/SceneWallet"; import SceneSlates from "~/scenes/SceneSlates"; @@ -26,7 +26,6 @@ import SidebarCreateSlate from "~/components/sidebars/SidebarCreateSlate"; import SidebarCreateWalletAddress from "~/components/sidebars/SidebarCreateWalletAddress"; import SidebarWalletSendFunds from "~/components/sidebars/SidebarWalletSendFunds"; import SidebarFileStorageDeal from "~/components/sidebars/SidebarFileStorageDeal"; -import SidebarCreatePaymentChannel from "~/components/sidebars/SidebarCreatePaymentChannel"; import SidebarAddFileToBucket from "~/components/sidebars/SidebarAddFileToBucket"; // NOTE(jim): @@ -68,7 +67,7 @@ export default class ApplicationPage extends React.Component { this.setState({ fileLoading: true }); let data = new FormData(); - data.append("image", file); + data.append("data", file); const options = { method: "POST", @@ -156,7 +155,9 @@ export default class ApplicationPage extends React.Component { if (e.dataTransfer.items[i].kind === "file") { var file = e.dataTransfer.items[i].getAsFile(); - await this._handleSetFile({ file, slate }); + if (Validations.isFileTypeAllowed(file.type)) { + await this._handleSetFile({ file, slate }); + } break; } } diff --git a/components/core/MediaObject.js b/components/core/MediaObject.js new file mode 100644 index 00000000..1bdc37bb --- /dev/null +++ b/components/core/MediaObject.js @@ -0,0 +1,69 @@ +import * as React from "react"; +import * as Constants from "~/common/constants"; + +import { css } from "@emotion/react"; + +const STYLES_FAILURE = css` + background-color: ${Constants.system.pitchBlack}; + color: ${Constants.system.white}; + display: flex; + align-items: center; + justify-content: center; + font-size: 88px; + margin: 0; + padding: 0; + width: 100%; + min-height: 10%; + height: 100%; +`; + +const STYLES_OBJECT = css` + display: block; + margin: 0; + padding: 0; + width: 100%; + min-height: 10%; + height: 100%; +`; + +const STYLES_ASSET = css` + background-color: ${Constants.system.pitchBlack}; + width: 100%; + margin: 0; + padding: 0; + min-height: 10%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + position: relative; +`; + +const STYLES_IMAGE = css` + display: block; + max-width: 100%; + max-height: 100%; +`; + +export default class MediaObject extends React.Component { + render() { + const name = `${this.props.data.name}`; + const url = this.props.data.url ? this.props.data.url : `https://hub.textile.io${this.props.data.ipfs}`; + + let mediaElement =
No Preview
; + + if (this.props.data.type.startsWith("application/pdf")) { + mediaElement = ; + } + + if (this.props.data.type.startsWith("image/")) { + mediaElement = ( +
+ +
+ ); + } + + return mediaElement; + } +} diff --git a/components/sidebars/SidebarAddFileToBucket.js b/components/sidebars/SidebarAddFileToBucket.js index 4a84791f..e12771a8 100644 --- a/components/sidebars/SidebarAddFileToBucket.js +++ b/components/sidebars/SidebarAddFileToBucket.js @@ -1,8 +1,7 @@ import * as React from "react"; -import * as Strings from "~/common/strings"; import * as Constants from "~/common/constants"; -import * as SVG from "~/components/system/svg"; import * as System from "~/components/system"; +import * as Validations from "~/common/validations"; import { css } from "@emotion/react"; @@ -49,36 +48,31 @@ export default class SidebarAddFileToBucket extends React.Component { let file = e.target.files[0]; if (!file) { - alert("Something went wrong"); + alert("TODO: Something went wrong"); + return; + } + + const isAllowed = Validations.isFileTypeAllowed(file.type); + if (!isAllowed) { + alert("TODO: File type is not allowed, yet."); return; } await this.props.onSetFile({ file, - slate: - this.props.data && this.props.data.slateId - ? { id: this.props.data.slateId } - : null, + slate: this.props.data && this.props.data.slateId ? { id: this.props.data.slateId } : null, }); }; render() { return ( - - Upload a file to Slate - - + Upload a file to Slate + {this.props.data && this.props.data.decorator === "SLATE" ? ( - This will add an image to your Slate named{" "} - {this.props.data.slatename}. + This will add an image to your Slate named {this.props.data.slatename}. ) : null} @@ -86,16 +80,12 @@ export default class SidebarAddFileToBucket extends React.Component { type="label" htmlFor="file" style={{ marginTop: 24 }} - loading={this.props.fileLoading} - > + loading={this.props.fileLoading}> Add file {!this.props.fileLoading ? ( - + Cancel ) : null} diff --git a/node_common/upload.js b/node_common/upload.js index aed8a741..a2883914 100644 --- a/node_common/upload.js +++ b/node_common/upload.js @@ -19,32 +19,30 @@ export const formMultipart = (req, res, { user }) => }); } - if (!files.image) { + console.log(files); + + if (!files.data) { return reject({ - decorator: "SERVER_UPLOAD_NOT_IMAGE_TYPE", + decorator: "SERVER_UPLOAD_ERROR", error: true, message: files, }); } - const path = files.image._writeStream.path; + const path = files.data._writeStream.path; const localPath = `./${path}`; - const data = LibraryManager.createLocalDataIncomplete(files.image); + const data = LibraryManager.createLocalDataIncomplete(files.data); - const { - buckets, - bucketKey, - bucketName, - } = await Utilities.getBucketAPIFromUserToken(user.data.tokens.api); + const { buckets, bucketKey, bucketName } = await Utilities.getBucketAPIFromUserToken(user.data.tokens.api); let readFile; let push; - // TODO(jim): Send this file to buckets. try { - // NOTE(jim): Push pathPath to your bucket. readFile = await FS.readFileSync(path).buffer; push = await buckets.pushPath(bucketKey, data.name, readFile); } catch (e) { + await FS.unlinkSync(localPath); + return reject({ decorator: "SERVER_BUCKETS_PUSH_ISSUE", error: true, diff --git a/pages/api/slates/add-url.js b/pages/api/slates/add-url.js index fc321b92..c2b8f3a8 100644 --- a/pages/api/slates/add-url.js +++ b/pages/api/slates/add-url.js @@ -1,8 +1,6 @@ import * as MW from "~/node_common/middleware"; import * as Utilities from "~/node_common/utilities"; import * as Data from "~/node_common/data"; -import * as Strings from "~/common/strings"; -import * as Powergate from "~/node_common/powergate"; const initCORS = MW.init(MW.CORS); const initAuth = MW.init(MW.RequireCookieAuthentication); @@ -13,9 +11,7 @@ export default async (req, res) => { const id = Utilities.getIdFromCookie(req); if (!id) { - return res - .status(403) - .json({ decorator: "SERVER_ADD_TO_SLATE_USER_NOT_FOUND", error: true }); + return res.status(403).json({ decorator: "SERVER_ADD_TO_SLATE_USER_NOT_FOUND", error: true }); } const user = await Data.getUserById({ @@ -62,6 +58,7 @@ export default async (req, res) => { id: req.body.data.id, ownerId: user.id, name: req.body.data.name, + type: req.body.data.type, url: `https://hub.textile.io${req.body.data.ipfs}`, }, ...slate.data.objects, @@ -83,7 +80,5 @@ export default async (req, res) => { }); } - return res - .status(200) - .json({ decorator: "SERVER_SLATE_ADD_TO_SLATE", slate }); + return res.status(200).json({ decorator: "SERVER_SLATE_ADD_TO_SLATE", slate }); }; diff --git a/scenes/SceneEditAccount.js b/scenes/SceneEditAccount.js index d097bcc8..15111d77 100644 --- a/scenes/SceneEditAccount.js +++ b/scenes/SceneEditAccount.js @@ -42,7 +42,13 @@ export default class SceneEditAccount extends React.Component { let file = e.target.files[0]; if (!file) { - alert("Something went wrong"); + alert("TODO: Something went wrong"); + return; + } + + if (!file.type.startsWith("image/")) { + alert("TODO: Error message for not an image."); + return; } let data = new FormData(); @@ -144,25 +150,15 @@ export default class SceneEditAccount extends React.Component { description="This image will appear in various lists." /> - +
- + + loading={this.state.changingAvatar}> Pick avatar
@@ -172,8 +168,7 @@ export default class SceneEditAccount extends React.Component { label="Username" description={ - This is your username on Slate. Your username is used for your - profile URL{" "} + This is your username on Slate. Your username is used for your profile URL{" "} {profileURL} @@ -186,10 +181,7 @@ export default class SceneEditAccount extends React.Component { />
- + Change username
@@ -221,10 +213,7 @@ export default class SceneEditAccount extends React.Component { />
- + Change password
@@ -236,10 +225,7 @@ export default class SceneEditAccount extends React.Component { />
- + Delete my account
diff --git a/scenes/SceneFile.js b/scenes/SceneFile.js index 04810a81..db06c74e 100644 --- a/scenes/SceneFile.js +++ b/scenes/SceneFile.js @@ -1,14 +1,10 @@ import * as React from "react"; -import * as Strings from "~/common/strings"; import * as Constants from "~/common/constants"; -import * as Fixtures from "~/common/fixtures"; -import * as System from "~/components/system"; import * as SVG from "~/components/system/svg"; import { css } from "@emotion/react"; -import Section from "~/components/core/Section"; -import ScenePage from "~/components/core/ScenePage"; +import MediaObject from "~/components/core/MediaObject"; const STYLES_FLEX = css` display: flex; @@ -47,19 +43,6 @@ const STYLES_RIGHT = css` } `; -const STYLES_ASSET = css` - display: block; - width: 100%; - margin: 0; - padding: 0; - min-height: 10%; - height: 100%; - background-color: ${Constants.system.pitchBlack}; - background-size: contain; - background-repeat: no-repeat; - background-position: 50% 50%; -`; - const STYLES_BOTTOM = css` background: ${Constants.system.pitchBlack}; color: ${Constants.system.white}; @@ -79,62 +62,21 @@ const STYLES_PATH = css` overflow-wrap: break-word; `; -const STYLES_ITEM = css` - border-radius: 4px; - outline: 0; - border: 0; - min-height: 32px; - padding: 6px 16px 6px 16px; - display: inline-flex; - align-items: center; - justify-content: center; - font-size: 12px; - letter-spacing: 0.2px; - font-family: ${Constants.font.semiBold}; - transition: 200ms ease all; - cursor: pointer; - background-color: ${Constants.system.brand}; - color: ${Constants.system.white}; - margin-left: 16px; - - :hover { - background-color: ${Constants.system.green}; - } - - :focus { - box-shadow: inset 0 0 5px 2px rgba(0, 0, 0, 0.3); - outline: 0; - border: 0; - } -`; - export default class SceneFile extends React.Component { - state = {}; - - _handleChange = (e) => { - this.setState({ [e.target.name]: e.target.value }); - }; - render() { - const file = this.props.data; - - const fileName = `${file.name}`; - const fileURL = file.url ? file.url : `https://hub.textile.io${file.ipfs}`; + const fileURL = this.props.data.url ? this.props.data.url : `https://hub.textile.io${this.props.data.ipfs}`; return (
- {fileName} + {fileURL}
this.props.onBack()}>
-
+
); } diff --git a/scenes/SceneFilesFolder.js b/scenes/SceneFilesFolder.js index c6cd93d0..58bca1a1 100644 --- a/scenes/SceneFilesFolder.js +++ b/scenes/SceneFilesFolder.js @@ -1,6 +1,4 @@ import * as React from "react"; -import * as Strings from "~/common/strings"; -import * as Constants from "~/common/constants"; import * as Actions from "~/common/actions"; import * as System from "~/components/system"; @@ -30,13 +28,14 @@ export default class SceneFilesFolder extends React.Component { }); console.log({ jobs }); + if (jobs.length) { + const response = await Actions.checkCIDStatus(jobs); - const response = await Actions.checkCIDStatus(jobs); + console.log(response); - console.log(response); - - if (response && response.update) { - await this.props.onRehydrate(); + if (response && response.update) { + await this.props.onRehydrate(); + } } if (this._interval) { @@ -63,7 +62,8 @@ export default class SceneFilesFolder extends React.Component { const data = { columns: [ - { key: "name", name: "File", type: "FILE_LINK" }, + { key: "name", name: "File", type: "FILE_LINK", width: "100%" }, + { key: "type", name: "Type" }, { key: "size", name: "Size", @@ -105,7 +105,7 @@ export default class SceneFilesFolder extends React.Component { title={this.props.current.name} buttons={[ { - name: "Upload to IPFS", + name: "Upload data", type: "SIDEBAR", value: "SIDEBAR_ADD_FILE_TO_BUCKET", }, diff --git a/scenes/SceneHome.js b/scenes/SceneHome.js index 89e734cf..989a313e 100644 --- a/scenes/SceneHome.js +++ b/scenes/SceneHome.js @@ -78,14 +78,13 @@ export default class SceneHome extends React.Component { }; // TODO(jim): Refactor later. - const slateButtons = [ - { name: "Create slate", type: "SIDEBAR", value: "SIDEBAR_CREATE_SLATE" }, - ]; + const slateButtons = [{ name: "Create slate", type: "SIDEBAR", value: "SIDEBAR_CREATE_SLATE" }]; // TODO(jim): Refactor later. const data = { columns: [ - { key: "name", name: "Data", type: "FILE_LINK" }, + { key: "name", name: "Data", type: "FILE_LINK", width: "100%" }, + { key: "type", name: "Type" }, { key: "size", name: "Size", @@ -97,14 +96,13 @@ export default class SceneHome extends React.Component { name: "Date uploaded", width: "160px", type: "FILE_DATE", - tooltip: - "This date represents when the file was first uploaded to IPFS.", }, { key: "networks", name: "Network", type: "NETWORK_TYPE", width: "188px", + tooltip: "This data is publicly available to share on the internet!", }, ], rows: this.props.viewer.library[0].children.map((each) => { @@ -123,7 +121,7 @@ export default class SceneHome extends React.Component { value: this.props.viewer.library[0].id, }, { - name: "Upload to IPFS", + name: "Upload data", type: "SIDEBAR", value: "SIDEBAR_ADD_FILE_TO_BUCKET", }, @@ -152,11 +150,7 @@ export default class SceneHome extends React.Component { Home {this.props.viewer.addresses[0] ? ( -
+
) : null} -
+
{this.props.viewer.library[0] ? ( -
+
https://slate.host/@{this.props.viewer.username}/{slatename} -
+