Skip to content

Commit

Permalink
Folder uploads (#1515)
Browse files Browse the repository at this point in the history
* add basic folder upload

* wire up folder uploads for modal

* Rename Modal

* fix types

* remove types

* lingui extract

* fix lint

* add source attribution

* lingui extract

* incorporate feedback

* lingui extract

* fix lint

* Fix casing

* Rename component for consistency

* lingui extract

* clean up types

Co-authored-by: GitHub Actions <actions@github.com>
Co-authored-by: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com>
Co-authored-by: Ryan Noble <ryanjnoble@gmail.com>
  • Loading branch information
4 people authored Sep 16, 2021
1 parent 669e3a2 commit 0e454aa
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 38 deletions.
21 changes: 13 additions & 8 deletions packages/common-components/src/FileInput/FileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ const useStyles = makeStyles(({ constants, palette, overrides }: ITheme) =>
})
)

interface FileWithPath extends File {
path?: string
}

interface IFileInputProps extends DropzoneOptions {
className?: string
variant?: "dropzone" | "filepicker"
Expand Down Expand Up @@ -137,21 +141,22 @@ const FileInput = ({
const classes = useStyles()
const [previews, setPreviews] = useState<any[]>([])
const [errors, setErrors] = useState<any[]>([])
const [{ value }, meta, helpers] = useField(name)
const [{ value }, meta, helpers] = useField<Array<FileWithPath>>(name)

useEffect(() => {
onFileNumberChange && onFileNumberChange(value.length)
}, [onFileNumberChange, value.length])

const onDrop = useCallback(
async (acceptedFiles: File[], fileRejections: FileRejection[]) => {
async (acceptedFiles: Array<FileWithPath>, fileRejections: FileRejection[]) => {
const filtered = acceptedFiles.filter((file) =>
maxFileSize ? file.size <= maxFileSize : true
)

setErrors([])
if (showPreviews) {
setPreviews(
filtered.map((file: any) =>
filtered.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file)
})
Expand Down Expand Up @@ -188,7 +193,7 @@ const FileInput = ({
})

const removeItem = (i: number) => {
const items = value as any[]
const items = value
items.splice(i, 1)
helpers.setValue(items)
}
Expand Down Expand Up @@ -220,12 +225,12 @@ const FileInput = ({
>
<ScrollbarWrapper className={clsx("scrollbar")}>
<ul>
{value.map((file: any, i: any) => (
{value.map((file: any, i: number) => (
<li
className={clsx(classes.item, classNames?.item)}
key={i}
>
<span className={classes.itemText}>{file.name}</span>
<span className={classes.itemText}>{file.path}</span>
<Button
testId="remove-from-file-list"
className={clsx(classes.crossIcon, classNames?.closeIcon)}
Expand All @@ -246,9 +251,9 @@ const FileInput = ({
)
) : (
<>
{value.value?.length === 0
{value?.length === 0
? "No files selected"
: `${value.value?.length} file(s) selected`}
: `${value?.length} file(s) selected`}
<Button
onClick={open}
size="small"
Expand Down
2 changes: 2 additions & 0 deletions packages/files-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@tkey/security-questions": "3.12.0",
"@tkey/web-storage": "3.12.0",
"@toruslabs/torus-direct-web-sdk": "4.10.0",
"@types/filesystem": "^0.0.32",
"@types/uuid": "^8.3.0",
"axios": "0.21.4",
"babel-loader": "8.1.0",
Expand All @@ -24,6 +25,7 @@
"babel-preset-react": "^6.24.1",
"bnc-onboard": "1.32.0",
"clsx": "^1.1.1",
"datatransfer-files-promise": "^1.3.1",
"dayjs": "^1.9.7",
"eth-crypto": "^1.8.0",
"ethers": "^5.4.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { useLocalStorage } from "@chainsafe/browser-storage-hooks"
import { DISMISSED_SURVEY_KEY } from "../../SurveyBanner"
import { FileBrowserContext } from "../../../Contexts/FileBrowserContext"
import { parseFileContentResponse } from "../../../Utils/Helpers"
import getFilesFromDataTransferItems from "../../../Utils/getFilesFromDataTransferItems"

const CSFFileBrowser: React.FC<IFileBrowserModuleProps> = () => {
const { downloadFile, uploadFiles, buckets } = useFiles()
Expand Down Expand Up @@ -174,23 +175,12 @@ const CSFFileBrowser: React.FC<IFileBrowserModuleProps> = () => {

const handleUploadOnDrop = useCallback(async (files: File[], fileItems: DataTransferItemList, path: string) => {
if (!bucket) return
let hasFolder = false
for (let i = 0; i < files.length; i++) {
if (fileItems[i].webkitGetAsEntry()?.isDirectory) {
hasFolder = true
}
}
if (hasFolder) {
addToast({
title: t`Folder uploads are not supported currently`,
type: "error"
})
} else {
uploadFiles(bucket, files, path)
.then(() => refreshContents())
.catch(console.error)
}
}, [addToast, uploadFiles, bucket, refreshContents])
const flattenedFiles = await getFilesFromDataTransferItems(fileItems)
const paths = [...new Set(flattenedFiles.map(f => f.filepath))]
paths.forEach(p => {
uploadFiles(bucket, flattenedFiles.filter(f => f.filepath === p), getPathWithFile(path, p))
})
}, [uploadFiles, bucket])

const viewFolder = useCallback((cid: string) => {
const fileSystemItem = pathContents.find(f => f.cid === cid)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Button, FileInput } from "@chainsafe/common-components"
import { useFiles } from "../../Contexts/FilesContext"
import { useFiles } from "../../../Contexts/FilesContext"
import { createStyles, makeStyles } from "@chainsafe/common-theme"
import React, { useCallback, useState } from "react"
import { Formik, Form } from "formik"
import { array, object } from "yup"
import CustomModal from "../Elements/CustomModal"
import CustomModal from "../../Elements/CustomModal"
import { Trans, t } from "@lingui/macro"
import clsx from "clsx"
import { CSFTheme } from "../../Themes/types"
import { useFileBrowser } from "../../Contexts/FileBrowserContext"
import { CSFTheme } from "../../../Themes/types"
import { useFileBrowser } from "../../../Contexts/FileBrowserContext"
import { getPathWithFile } from "../../../Utils/pathUtils"

const useStyles = makeStyles(({ constants, breakpoints }: CSFTheme) =>
createStyles({
Expand Down Expand Up @@ -89,15 +90,19 @@ const UploadFileModule = ({ modalOpen, close }: IUploadFileModuleProps) => {
setIsDoneDisabled(filesNumber === 0)
}, [])

const onSubmit = useCallback(async (values, helpers) => {
const onSubmit = useCallback(async (values: {files: Array<File & {path: string}>}, helpers) => {
if (!bucket) return
helpers.setSubmitting(true)
try {
close()
await uploadFiles(bucket, values.files, currentPath)
const paths = [...new Set(values.files.map(f => f.path.substring(0, f.path.lastIndexOf("/"))))]
paths.forEach(async p => {
const filesToUpload = values.files.filter((f => f.path.substring(0, f.path.lastIndexOf("/")) === p))
await uploadFiles(bucket, filesToUpload, getPathWithFile(currentPath, p))
})
refreshContents && refreshContents()
helpers.resetForm()
} catch (errors) {
} catch (errors: any) {
if (errors[0].message.includes("conflict with existing")) {
helpers.setFieldError("files", "File/Folder exists")
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import FileSystemItem from "./FileSystemItem/FileSystemItem"
import FilePreviewModal from "../../FilePreviewModal"

import CreateFolderModal from "../CreateFolderModal"
import UploadFileModule from "../../UploadFileModule"
import UploadFileModule from "../UploadFileModal"
import MoveFileModal from "../MoveFileModal"
import FileInfoModal from "../FileInfoModal"
import { CONTENT_TYPES } from "../../../../Utils/Constants"
Expand Down
1 change: 0 additions & 1 deletion packages/files-ui/src/Contexts/FilesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,6 @@ const FilesProvider = ({ children }: FilesContextProps) => {
}
})
)

await filesApiClient.uploadBucketObjects(
bucket.id,
filesParam,
Expand Down
78 changes: 78 additions & 0 deletions packages/files-ui/src/Utils/getFilesFromDataTransferItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//Shamelessly borrowed from https://github.com/anatol-grabowski/datatransfer-files-promise with added Types

type FileWithPath = File & {filepath: string}

const getFilesFromDataTransferItems = async (dataTransferItems: DataTransferItemList): Promise<Array<FileWithPath>> => {
const readFile = (entry: FileEntry, path = ""): Promise<FileWithPath> => {
return new Promise((resolve, reject) => {
entry.file((file: File) => {
Object.defineProperty(file, "filepath", {
value: path
})
resolve(file as FileWithPath)
}, (err: Error) => {
reject(err)
})
})
}

const dirReadEntries = (dirReader: DirectoryReader, path: string): Promise<FileWithPath[]> => {
return new Promise((resolve, reject) => {
dirReader.readEntries(async (entries: FileSystemEntry[]) => {
let files = [] as Array<FileWithPath>
for (const entry of entries) {
const itemFiles = await getFilesFromEntry(entry, path) as Array<FileWithPath>
files = files.concat(itemFiles)
}
resolve(files)
}, (err: Error) => {
reject(err)
})
})
}

const readDir = async (entry: DirectoryEntry, path: string) => {
const dirReader = entry.createReader()
const newPath = path + entry.name + "/"
let files = [] as Array<FileWithPath>
let newFiles
do {
newFiles = await dirReadEntries(dirReader, newPath)
files = files.concat(newFiles)
} while (newFiles.length > 0)
return files
}

const getFilesFromEntry = async (entry: FileSystemEntry, path = "") => {
if (entry.isFile) {
const file = await readFile(entry as FileEntry, path)
return [file]
}
if (entry.isDirectory) {
const files = await readDir(entry as DirectoryEntry, path)
return files
}
}

let files = [] as Array<FileWithPath>
const entries = []

// Pull out all entries before reading them
for (let i = 0, ii = dataTransferItems.length; i < ii; i++) {
entries.push(dataTransferItems[i].webkitGetAsEntry())
}

// Recursively read through all entries
for (const entry of entries) {
if (entry) {
const newFiles = await getFilesFromEntry(entry)
if (newFiles) {
files = files.concat(newFiles)
}
}
}

return files
}

export default getFilesFromDataTransferItems
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import FileSystemItem from "../FileSystemItem/FileSystemItem"
import UploadProgressModals from "../UploadProgressModals"
import DownloadProgressModals from "../DownloadProgressModals"
import CreateFolderModal from "../CreateFolderModal/CreateFolderModal"
import UploadFileModule from "../UploadFileModal/UploadFileModal"
import UploadFileModal from "../UploadFileModal/UploadFileModal"
import MoveFileModal from "../MoveFileModal/MoveFileModal"
import { CONTENT_TYPES } from "../../../Utils/Constants"
import { CSSTheme } from "../../../Themes/types"
Expand Down Expand Up @@ -990,7 +990,7 @@ const FilesList = () => {
modalOpen={createFolderModalOpen}
close={() => setCreateFolderModalOpen(false)}
/>
<UploadFileModule
<UploadFileModal
modalOpen={isUploadModalOpen}
close={() => setIsUploadModalOpen(false)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ interface IUploadFileModuleProps {
close: () => void
}

const UploadFileModule = ({ modalOpen, close }: IUploadFileModuleProps) => {
const UploadFileModal = ({ modalOpen, close }: IUploadFileModuleProps) => {
const classes = useStyles()
const [isDoneDisabled, setIsDoneDisabled] = useState(true)
const { uploadFiles } = useStorage()
Expand Down Expand Up @@ -166,4 +166,4 @@ const UploadFileModule = ({ modalOpen, close }: IUploadFileModuleProps) => {
)
}

export default UploadFileModule
export default UploadFileModal
17 changes: 17 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5829,6 +5829,18 @@
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==

"@types/filesystem@^0.0.32":
version "0.0.32"
resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.32.tgz#307df7cc084a2293c3c1a31151b178063e0a8edf"
integrity sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ==
dependencies:
"@types/filewriter" "*"

"@types/filewriter@*":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.29.tgz#a48795ecadf957f6c0d10e0c34af86c098fa5bee"
integrity sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==

"@types/fs-extra@^8.0.1":
version "8.1.1"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.1.tgz#1e49f22d09aa46e19b51c0b013cb63d0d923a068"
Expand Down Expand Up @@ -10731,6 +10743,11 @@ data-urls@^1.0.0, data-urls@^1.1.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"

datatransfer-files-promise@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/datatransfer-files-promise/-/datatransfer-files-promise-1.3.1.tgz#8996214c76925b57fe4c03b25a214ceaca76f063"
integrity sha512-xwh/RYaCTzUVyZr+rDKO28ew3Nbe4aP1HmEa1rNta9Uq1VNI2+ME0s1JKwsnUKXEpCWL+puGXkpJ6j+3vm/T8g==

date-fns@^2.16.1:
version "2.16.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.1.tgz#05775792c3f3331da812af253e1a935851d3834b"
Expand Down

0 comments on commit 0e454aa

Please sign in to comment.