@rpcbase/client 0.196.0 → 0.197.0-fileupload.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/ui/FileUpload/FileUploadContext.tsx +160 -0
- package/ui/FileUpload/FileUploadForm/index.tsx +137 -0
- package/ui/FileUpload/FileUploadForm/usePreventUnload.js +21 -0
- package/ui/FileUpload/FileUploadModal.tsx +53 -0
- package/ui/FileUpload/UploadButton.tsx +23 -0
- package/ui/FileUpload/constants.ts +16 -0
- package/ui/FileUpload/file-upload.scss +1 -0
- package/ui/FileUpload/index.tsx +21 -0
- package/ui/FileUpload/upload-worker/get_file_hash.js +63 -0
- package/ui/FileUpload/upload-worker/index.js +15 -0
- package/ui/FileUpload/upload-worker/no_compress_exts.ts +33 -0
- package/ui/FileUpload/upload-worker/upload_file.js +127 -0
- package/ui/SubmitButton/{index.js → index.tsx} +18 -9
package/package.json
CHANGED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import Promise from "bluebird"
|
|
2
|
+
import { ReactNode } from "react"
|
|
3
|
+
import debug from "debug"
|
|
4
|
+
import { createContext, useContext, useState, useRef, useEffect } from "react"
|
|
5
|
+
|
|
6
|
+
// import complete_upload from "rpc!server/items/drives/files/complete_upload";
|
|
7
|
+
import {EVENTS, UPLOAD_MAX_CONCURRENCY} from "./constants"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const log = debug("drive:upload_context")
|
|
11
|
+
|
|
12
|
+
export const FileUploadContext = createContext()
|
|
13
|
+
|
|
14
|
+
export const FileUploadContextProvider = ({
|
|
15
|
+
onUploadComplete,
|
|
16
|
+
children,
|
|
17
|
+
}: {
|
|
18
|
+
onUploadComplete?: (files: any) => Promise<void>,
|
|
19
|
+
children: ReactNode,
|
|
20
|
+
}) => {
|
|
21
|
+
const workerRef = useRef(null)
|
|
22
|
+
|
|
23
|
+
const [canUpload, setCanUpload] = useState(false)
|
|
24
|
+
const [isUploading, setIsUploading] = useState(false)
|
|
25
|
+
const [selectedFiles, setSelectedFiles] = useState([])
|
|
26
|
+
const [processedFiles, setProcessedFiles] = useState(Object.create(null))
|
|
27
|
+
|
|
28
|
+
const pendingUploadCallbacksRef = useRef({})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
const updateProcessedFile = (filename, payload) => {
|
|
32
|
+
setProcessedFiles((previousProcessedFiles) => {
|
|
33
|
+
const nextProcessedFiles = Object.create(null)
|
|
34
|
+
Object.assign(nextProcessedFiles, previousProcessedFiles)
|
|
35
|
+
|
|
36
|
+
if (!nextProcessedFiles[filename]) nextProcessedFiles[filename] = {}
|
|
37
|
+
|
|
38
|
+
Object.assign(nextProcessedFiles[filename], payload)
|
|
39
|
+
return nextProcessedFiles
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// when all files are ready to upload update parent context
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!(Object.keys(processedFiles).length > 0)) return
|
|
46
|
+
|
|
47
|
+
let isComplete = true
|
|
48
|
+
Object.keys(processedFiles).forEach((k) => {
|
|
49
|
+
if (processedFiles[k]?.status !== EVENTS.HASH_COMPLETE) {
|
|
50
|
+
isComplete = false
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
if (isComplete && !canUpload) {
|
|
55
|
+
// wait a bit for transitions to finish
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
setCanUpload(true)
|
|
58
|
+
}, 300)
|
|
59
|
+
}
|
|
60
|
+
}, [processedFiles, canUpload, setCanUpload])
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
workerRef.current = new Worker(
|
|
64
|
+
new URL("./upload-worker/index.js", import.meta.url),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const onMessage = async({ data }) => {
|
|
68
|
+
log("worker message:", data)
|
|
69
|
+
const { type, payload } = data
|
|
70
|
+
|
|
71
|
+
if (type === EVENTS.UPLOAD_COMPLETE) {
|
|
72
|
+
const {filename, hash} = payload
|
|
73
|
+
updateProcessedFile(filename, {
|
|
74
|
+
status: EVENTS.UPLOAD_COMPLETE,
|
|
75
|
+
})
|
|
76
|
+
const cb = pendingUploadCallbacksRef.current[hash]
|
|
77
|
+
cb.resolve()
|
|
78
|
+
} else if (type === EVENTS.UPLOAD_ERROR) {
|
|
79
|
+
const cb = pendingUploadCallbacksRef.current[hash]
|
|
80
|
+
cb.reject()
|
|
81
|
+
} else if ([EVENTS.HASH_PROGRESS, EVENTS.HASH_COMPLETE].includes(type)) {
|
|
82
|
+
//
|
|
83
|
+
console.log("got hash event", type, payload)
|
|
84
|
+
} else {
|
|
85
|
+
throw new Error("unknown message event type")
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
workerRef.current.addEventListener("message", onMessage)
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
workerRef.current.removeEventListener("message", onMessage)
|
|
93
|
+
}
|
|
94
|
+
}, [])
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
const startUploadAndWait = ({file, hash}) => new Promise((resolve, reject) => {
|
|
98
|
+
console.log("startUploadAndWait", {file, hash})
|
|
99
|
+
|
|
100
|
+
workerRef.current.postMessage({type: EVENTS.UPLOAD_FILE, file, hash})
|
|
101
|
+
|
|
102
|
+
pendingUploadCallbacksRef.current[hash] = {resolve, reject}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
const uploadAllFiles = async() => {
|
|
107
|
+
setCanUpload(false)
|
|
108
|
+
setIsUploading(true)
|
|
109
|
+
|
|
110
|
+
const getUploadFiles = () => selectedFiles.map((f) => {
|
|
111
|
+
const processedFile = processedFiles[f.name]
|
|
112
|
+
if (!processedFile) {
|
|
113
|
+
throw new Error(`unable to retrieve processed file: ${f.name}`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
file: f,
|
|
118
|
+
...processedFile,
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
await Promise.map(getUploadFiles(), startUploadAndWait, {concurrency: UPLOAD_MAX_CONCURRENCY})
|
|
123
|
+
|
|
124
|
+
if (typeof onUploadComplete === "function") {
|
|
125
|
+
await onUploadComplete(getUploadFiles())
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
setCanUpload(true)
|
|
129
|
+
setIsUploading(false)
|
|
130
|
+
setSelectedFiles([])
|
|
131
|
+
setProcessedFiles(Object.create(null))
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<FileUploadContext.Provider
|
|
137
|
+
value={{
|
|
138
|
+
workerRef,
|
|
139
|
+
//
|
|
140
|
+
canUpload,
|
|
141
|
+
setCanUpload,
|
|
142
|
+
//
|
|
143
|
+
isUploading,
|
|
144
|
+
setIsUploading,
|
|
145
|
+
//
|
|
146
|
+
selectedFiles,
|
|
147
|
+
setSelectedFiles,
|
|
148
|
+
//
|
|
149
|
+
processedFiles,
|
|
150
|
+
updateProcessedFile,
|
|
151
|
+
//
|
|
152
|
+
uploadAllFiles,
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
{children}
|
|
156
|
+
</FileUploadContext.Provider>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const useFileUploadContext = () => useContext(FileUploadContext)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import assert from "assert"
|
|
2
|
+
import {useEffect} from "react"
|
|
3
|
+
import _set from "lodash/set"
|
|
4
|
+
|
|
5
|
+
import {formatFileSize} from "@rpcbase/std"
|
|
6
|
+
|
|
7
|
+
import {useFileUploadContext} from "../FileUploadContext"
|
|
8
|
+
|
|
9
|
+
import usePreventUnload from "./usePreventUnload"
|
|
10
|
+
|
|
11
|
+
import {EVENTS} from "../constants"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const STATUS_MESSAGES = {
|
|
15
|
+
HASH_COMPLETE: "Ready to upload!",
|
|
16
|
+
HASHING: "Checking file...",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const FileUploadForm = () => {
|
|
20
|
+
const {workerRef, selectedFiles, setSelectedFiles, processedFiles, updateProcessedFile} = useFileUploadContext()
|
|
21
|
+
|
|
22
|
+
usePreventUnload()
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!workerRef.current) return
|
|
26
|
+
|
|
27
|
+
const onMessage = ({data: {type, payload}}) => {
|
|
28
|
+
// done hashing
|
|
29
|
+
if (type === EVENTS.HASH_COMPLETE) {
|
|
30
|
+
const {filename, hash} = payload
|
|
31
|
+
updateProcessedFile(filename, {
|
|
32
|
+
status: EVENTS.HASH_COMPLETE,
|
|
33
|
+
hash,
|
|
34
|
+
progress: 100,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
// hash_progress
|
|
38
|
+
else if (type === EVENTS.HASH_PROGRESS) {
|
|
39
|
+
updateProcessedFile(payload.filename, {
|
|
40
|
+
status: EVENTS.HASHING,
|
|
41
|
+
progress: payload.progress,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
workerRef.current.addEventListener("message", onMessage)
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
workerRef.current.removeEventListener("message", onMessage)
|
|
50
|
+
}
|
|
51
|
+
}, [workerRef.current])
|
|
52
|
+
|
|
53
|
+
const onClickPicker = async() => {
|
|
54
|
+
const dirHandle = await window.showDirectoryPicker({
|
|
55
|
+
startIn: "desktop",
|
|
56
|
+
})
|
|
57
|
+
console.log("got dir hanlde", dirHandle)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
useEffect(() => {}, [selectedFiles])
|
|
61
|
+
|
|
62
|
+
const onFilesChange = (e) => {
|
|
63
|
+
const {files} = e.target
|
|
64
|
+
|
|
65
|
+
// TODO: we should have a way to limit to a maximum number of hashes at the same time
|
|
66
|
+
Array.from(files).forEach((file) => {
|
|
67
|
+
assert(file.webkitRelativePath === "", "expected webkitRelativePath to be ''")
|
|
68
|
+
|
|
69
|
+
workerRef.current.postMessage({type: EVENTS.BEGIN_HASH, file})
|
|
70
|
+
})
|
|
71
|
+
setSelectedFiles(Array.from(files))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="px-3">
|
|
76
|
+
<div className="mb-3">
|
|
77
|
+
<label htmlFor="file-upload-input-files" className="form-label fw-bold">
|
|
78
|
+
Add Files
|
|
79
|
+
</label>
|
|
80
|
+
<input
|
|
81
|
+
className="form-control"
|
|
82
|
+
type="file"
|
|
83
|
+
id="file-upload-input-files"
|
|
84
|
+
accept="*"
|
|
85
|
+
multiple
|
|
86
|
+
onChange={onFilesChange}
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div className="text-secondary">OR</div>
|
|
91
|
+
|
|
92
|
+
<button
|
|
93
|
+
className="btn btn-light mt-2 d-flex flex-row align-items-center"
|
|
94
|
+
onClick={onClickPicker}
|
|
95
|
+
>
|
|
96
|
+
<img src="/static/icons/drive/folder-xs.svg" />
|
|
97
|
+
<span className="ms-2">Upload a Folder</span>
|
|
98
|
+
</button>
|
|
99
|
+
|
|
100
|
+
{selectedFiles.length > 0 && <hr className="mt-4" />}
|
|
101
|
+
|
|
102
|
+
<div className="files-list">
|
|
103
|
+
{selectedFiles.map((f, i) => {
|
|
104
|
+
const progress = processedFiles[f.name]?.progress || 0
|
|
105
|
+
const status = STATUS_MESSAGES[processedFiles[f.name]?.status] || "Preparing..."
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div key={`file-${i}`}>
|
|
109
|
+
<div className="d-flex flex-row align-items-center mx-2 mt-3 mb-1">
|
|
110
|
+
<img src="/static/icons/drive/file-xs.svg" />
|
|
111
|
+
<div className="ms-2 text-truncate">{f.name}</div>
|
|
112
|
+
<div className="ms-2 text-secondary text-monospace">{formatFileSize(f.size)}</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div className="d-flex flex-row align-items-center">
|
|
116
|
+
<div className="progress mt-0" style={{width: 120}}>
|
|
117
|
+
<div
|
|
118
|
+
className="progress-bar"
|
|
119
|
+
role="progressbar"
|
|
120
|
+
style={{width: `${progress}%`}}
|
|
121
|
+
aria-valuenow={progress}
|
|
122
|
+
aria-valuemin="0"
|
|
123
|
+
aria-valuemax="100"
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
<div className="ms-2 text-secondary">{progress} %</div>
|
|
127
|
+
<div className="ms-2 text-secondary">Status: {status}</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
})}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default FileUploadForm
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
|
+
/* @flow */
|
|
3
|
+
import {useEffect} from "react"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const usePreventUnload = () => {
|
|
7
|
+
if (__DEV__) return
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const onBeforeUnload = (e) => {
|
|
11
|
+
e.preventDefault()
|
|
12
|
+
e.returnValue = ""
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
window.addEventListener("beforeunload", onBeforeUnload)
|
|
16
|
+
|
|
17
|
+
return () => window.removeEventListener("beforeunload", onBeforeUnload)
|
|
18
|
+
}, [])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default usePreventUnload
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import ActivityIndicator from "../ActivityIndicator"
|
|
2
|
+
import Modal from "../Modal"
|
|
3
|
+
|
|
4
|
+
import {FileUploadContextProvider} from "./FileUploadContext"
|
|
5
|
+
import FileUploadForm from "./FileUploadForm"
|
|
6
|
+
import UploadButton from "./UploadButton"
|
|
7
|
+
|
|
8
|
+
import "./file-upload.scss"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
// TODO: option to disable compression
|
|
12
|
+
|
|
13
|
+
export const FileUploadModal = ({onHide}) => {
|
|
14
|
+
const isLoading = false
|
|
15
|
+
|
|
16
|
+
const onHideFn = () => null
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<FileUploadContextProvider>
|
|
20
|
+
<Modal className="file-upload-modal" show scrollable={true} onHide={onHideFn}>
|
|
21
|
+
<Modal.Header className="close-top" closeButton>
|
|
22
|
+
<img
|
|
23
|
+
width={22}
|
|
24
|
+
height={22}
|
|
25
|
+
className="me-2 align-self-start mt-1"
|
|
26
|
+
src={`/static/icons/drive/file-upload-s.svg`}
|
|
27
|
+
/>
|
|
28
|
+
<div>
|
|
29
|
+
<div>File Upload</div>
|
|
30
|
+
<small className="text-secondary fw-normal">Upload any type of file, any size.</small>
|
|
31
|
+
</div>
|
|
32
|
+
</Modal.Header>
|
|
33
|
+
<Modal.Body className="py-2">
|
|
34
|
+
{isLoading && (
|
|
35
|
+
<div className="d-flex flex-row align-items-center">
|
|
36
|
+
<ActivityIndicator size={24} />
|
|
37
|
+
<div className="ms-2">Loading text...</div>
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
<FileUploadForm />
|
|
42
|
+
</Modal.Body>
|
|
43
|
+
<Modal.Footer className="d-flex justify-content-between">
|
|
44
|
+
<div>
|
|
45
|
+
<a href="/docs/drives">/docs/drives</a>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<UploadButton />
|
|
49
|
+
</Modal.Footer>
|
|
50
|
+
</Modal>
|
|
51
|
+
</FileUploadContextProvider>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {SubmitButton} from "../SubmitButton"
|
|
2
|
+
import {useFileUploadContext} from "./FileUploadContext"
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export const UploadButton = () => {
|
|
6
|
+
const {canUpload, isUploading, uploadAllFiles} =
|
|
7
|
+
useFileUploadContext()
|
|
8
|
+
|
|
9
|
+
const onStartUpload = async() => {
|
|
10
|
+
await uploadAllFiles()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<SubmitButton
|
|
15
|
+
id="file-start-upload-button"
|
|
16
|
+
disabled={!canUpload}
|
|
17
|
+
isLoading={isUploading}
|
|
18
|
+
onClick={onStartUpload}
|
|
19
|
+
title="Start Upload"
|
|
20
|
+
submittingTitle="Uploading Files..."
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const UPLOAD_MAX_CONCURRENCY = 1
|
|
2
|
+
|
|
3
|
+
export const HASH_CHUNK_SIZE = 2 * 1024 * 1024 // 1 MB
|
|
4
|
+
|
|
5
|
+
// must match server chunk size to avoid mongodb chunking again
|
|
6
|
+
export const UPLOAD_CHUNK_SIZE = 1 * 1024 * 1024 // 1MB
|
|
7
|
+
|
|
8
|
+
export const EVENTS = {
|
|
9
|
+
BEGIN_HASH: "BEGIN_HASH",
|
|
10
|
+
HASH_PROGRESS: "HASH_PROGRESS",
|
|
11
|
+
HASHING: "HASHING",
|
|
12
|
+
HASH_COMPLETE: "HASH_COMPLETE",
|
|
13
|
+
UPLOAD_FILE: "UPLOAD_FILE",
|
|
14
|
+
UPLOAD_COMPLETE: "UPLOAD_COMPLETE",
|
|
15
|
+
UPLOAD_ERROR: "UPLOAD_ERROR",
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "helpers";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { FileUploadContextProvider } from "./FileUploadContext"
|
|
2
|
+
import FileUploadForm from "./FileUploadForm"
|
|
3
|
+
import {UploadButton} from "./UploadButton"
|
|
4
|
+
|
|
5
|
+
import "./file-upload.scss"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export const FileUpload = ({
|
|
9
|
+
onUploadComplete,
|
|
10
|
+
}: {
|
|
11
|
+
onUploadComplete?: (files: any) => void | Promise<void>
|
|
12
|
+
}) => {
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<FileUploadContextProvider onUploadComplete={onUploadComplete}>
|
|
16
|
+
<FileUploadForm />
|
|
17
|
+
|
|
18
|
+
<UploadButton />
|
|
19
|
+
</FileUploadContextProvider>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
import {createXXHash128} from "hash-wasm"
|
|
3
|
+
|
|
4
|
+
import {EVENTS, HASH_CHUNK_SIZE} from "../constants"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// read and hash the file
|
|
8
|
+
const get_file_hash = async(file) => {
|
|
9
|
+
if (!file) return
|
|
10
|
+
|
|
11
|
+
const hasher = await createXXHash128()
|
|
12
|
+
|
|
13
|
+
const reader = new FileReader()
|
|
14
|
+
const size = file.size
|
|
15
|
+
let chunks_count = 0
|
|
16
|
+
|
|
17
|
+
let offset = 0
|
|
18
|
+
let bytes_read = 0
|
|
19
|
+
|
|
20
|
+
reader.onloadend = async(e) => {
|
|
21
|
+
if (e.target.readyState === FileReader.DONE) {
|
|
22
|
+
const chunk = new Uint8Array(e.target.result)
|
|
23
|
+
bytes_read += chunk.length
|
|
24
|
+
|
|
25
|
+
chunks_count++
|
|
26
|
+
|
|
27
|
+
console.log(`${chunks_count} hash chunks // ${bytes_read} bytes_read...`)
|
|
28
|
+
|
|
29
|
+
await hasher.update(chunk)
|
|
30
|
+
|
|
31
|
+
self.postMessage({
|
|
32
|
+
type: EVENTS.HASH_PROGRESS,
|
|
33
|
+
payload: {
|
|
34
|
+
// TODO: use relative path here as well in case we have two same filenames from two dirs
|
|
35
|
+
filename: file.name,
|
|
36
|
+
progress: ((bytes_read / size) * 100).toFixed(1),
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (offset < size) {
|
|
41
|
+
offset += HASH_CHUNK_SIZE
|
|
42
|
+
const blob = file.slice(offset, offset + HASH_CHUNK_SIZE)
|
|
43
|
+
reader.readAsArrayBuffer(blob)
|
|
44
|
+
} else {
|
|
45
|
+
const hash = hasher.digest()
|
|
46
|
+
|
|
47
|
+
self.postMessage({
|
|
48
|
+
type: EVENTS.HASH_COMPLETE,
|
|
49
|
+
payload: {
|
|
50
|
+
filename: file.name,
|
|
51
|
+
hash,
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const blob = file.slice(offset, offset + HASH_CHUNK_SIZE)
|
|
59
|
+
|
|
60
|
+
reader.readAsArrayBuffer(blob)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default get_file_hash
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
import upload_file from "./upload_file"
|
|
3
|
+
import get_file_hash from "./get_file_hash"
|
|
4
|
+
|
|
5
|
+
import {EVENTS} from "../constants"
|
|
6
|
+
|
|
7
|
+
self.onmessage = ({data: {file, type, hash}}) => {
|
|
8
|
+
if (type === EVENTS.BEGIN_HASH) {
|
|
9
|
+
get_file_hash(file)
|
|
10
|
+
} else if (type === EVENTS.UPLOAD_FILE) {
|
|
11
|
+
upload_file(file, hash)
|
|
12
|
+
} else {
|
|
13
|
+
console.log("upload worker: unknown message", type)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// we consider these file types to be already compressed
|
|
2
|
+
export const NO_COMPRESS_EXTS = [
|
|
3
|
+
".jpg",
|
|
4
|
+
".jpeg",
|
|
5
|
+
".png",
|
|
6
|
+
".gif",
|
|
7
|
+
".tif",
|
|
8
|
+
".tiff",
|
|
9
|
+
".webp",
|
|
10
|
+
".heif",
|
|
11
|
+
".heic",
|
|
12
|
+
".mp3",
|
|
13
|
+
".aac",
|
|
14
|
+
".ogg",
|
|
15
|
+
".flac",
|
|
16
|
+
".m4a",
|
|
17
|
+
".mp4",
|
|
18
|
+
".m4v",
|
|
19
|
+
".avi",
|
|
20
|
+
".mov",
|
|
21
|
+
".mkv",
|
|
22
|
+
".webm",
|
|
23
|
+
".zip",
|
|
24
|
+
".gzip",
|
|
25
|
+
".gz",
|
|
26
|
+
".rar",
|
|
27
|
+
".7z",
|
|
28
|
+
".bz2",
|
|
29
|
+
".tar.gz",
|
|
30
|
+
".tgz",
|
|
31
|
+
".jar",
|
|
32
|
+
".apk",
|
|
33
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
import assert from "assert"
|
|
3
|
+
// import debug from "debug"
|
|
4
|
+
import brotliPromise from "brotli-wasm"
|
|
5
|
+
|
|
6
|
+
import getBaseUrl from "../../../getBaseUrl"
|
|
7
|
+
|
|
8
|
+
import {NO_COMPRESS_EXTS} from "./no_compress_exts"
|
|
9
|
+
import {UPLOAD_CHUNK_SIZE, EVENTS} from "../constants"
|
|
10
|
+
|
|
11
|
+
// const log = debug("rb:upload_worker")
|
|
12
|
+
const log = (...args) => console.log(...args)
|
|
13
|
+
|
|
14
|
+
let _brotli
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const check_should_compress = (file) => {
|
|
18
|
+
for (const ext of NO_COMPRESS_EXTS) {
|
|
19
|
+
if (file.name.endsWith(ext)) return false
|
|
20
|
+
}
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const upload_file = async(file, hash) => {
|
|
25
|
+
assert(file, "upload_file has no file")
|
|
26
|
+
|
|
27
|
+
const should_compress = check_should_compress(file)
|
|
28
|
+
log("filename:", file.name, "should_compress:", should_compress)
|
|
29
|
+
|
|
30
|
+
if (should_compress && !_brotli) {
|
|
31
|
+
_brotli = await brotliPromise
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const reader = new FileReader()
|
|
35
|
+
const size = file.size
|
|
36
|
+
|
|
37
|
+
log("total file size", file.size)
|
|
38
|
+
|
|
39
|
+
const total_chunks = Math.ceil(size / UPLOAD_CHUNK_SIZE)
|
|
40
|
+
|
|
41
|
+
let current_chunks_count = 0
|
|
42
|
+
|
|
43
|
+
let offset = 0
|
|
44
|
+
let total_bytes_read = 0
|
|
45
|
+
|
|
46
|
+
reader.onloadend = async(e) => {
|
|
47
|
+
if (e.target.readyState === FileReader.DONE) {
|
|
48
|
+
const chunk_index = current_chunks_count
|
|
49
|
+
|
|
50
|
+
// how many bytes did we just read
|
|
51
|
+
const read_chunk_length = e.target.result.byteLength
|
|
52
|
+
|
|
53
|
+
const chunk = should_compress
|
|
54
|
+
? _brotli.compress(new Uint8Array(e.target.result))
|
|
55
|
+
: e.target.result
|
|
56
|
+
|
|
57
|
+
log("will upload chunk of size:", chunk.size)
|
|
58
|
+
|
|
59
|
+
const data = new FormData()
|
|
60
|
+
|
|
61
|
+
data.append("file_chunk", new Blob([chunk]))
|
|
62
|
+
data.append("is_compressed", should_compress ? "yes" : "no")
|
|
63
|
+
data.append("chunk_index", chunk_index)
|
|
64
|
+
data.append("total_chunks", total_chunks)
|
|
65
|
+
data.append("chunk_size", UPLOAD_CHUNK_SIZE)
|
|
66
|
+
data.append("original_filename", file.name)
|
|
67
|
+
data.append("mime_type", file.type)
|
|
68
|
+
data.append("hash", hash)
|
|
69
|
+
let res_json
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch(`${getBaseUrl()}/rb-api/v1/files/upload_chunk`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
credentials: "include",
|
|
74
|
+
body: data,
|
|
75
|
+
})
|
|
76
|
+
res_json = await res.json()
|
|
77
|
+
assert(res_json.status === "ok", "failed to fetch")
|
|
78
|
+
} catch (error) {
|
|
79
|
+
log("Error uploading chunk:", error)
|
|
80
|
+
console.log("error json", res_json)
|
|
81
|
+
// self.postMessage({
|
|
82
|
+
// type: EVENTS.UPLOAD_ERROR,
|
|
83
|
+
// payload: {
|
|
84
|
+
// filename: file.name,
|
|
85
|
+
// hash,
|
|
86
|
+
// error: error.message,
|
|
87
|
+
// },
|
|
88
|
+
// })
|
|
89
|
+
//
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
total_bytes_read += read_chunk_length
|
|
94
|
+
|
|
95
|
+
current_chunks_count++
|
|
96
|
+
|
|
97
|
+
log(`upload chunks ${current_chunks_count} chunk // ${total_bytes_read} total_bytes_read...`)
|
|
98
|
+
|
|
99
|
+
// there are more bytes to be read
|
|
100
|
+
if (total_bytes_read < size) {
|
|
101
|
+
offset += UPLOAD_CHUNK_SIZE
|
|
102
|
+
const blob = file.slice(offset, offset + UPLOAD_CHUNK_SIZE)
|
|
103
|
+
reader.readAsArrayBuffer(blob)
|
|
104
|
+
}
|
|
105
|
+
// reading complete, finalize upload
|
|
106
|
+
else {
|
|
107
|
+
console.log("upload complete!")
|
|
108
|
+
console.log("FINALLL RESS", res_json)
|
|
109
|
+
self.postMessage({
|
|
110
|
+
type: EVENTS.UPLOAD_COMPLETE,
|
|
111
|
+
payload: {
|
|
112
|
+
filename: file.name,
|
|
113
|
+
hash,
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
log("unknown filereader state", e.target.readyState)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// start reading the first chunk
|
|
123
|
+
const blob = file.slice(offset, offset + UPLOAD_CHUNK_SIZE)
|
|
124
|
+
reader.readAsArrayBuffer(blob)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default upload_file
|
|
@@ -1,31 +1,40 @@
|
|
|
1
|
+
import {ReactNode} from "react"
|
|
1
2
|
import {useFormContext} from "react-hook-form"
|
|
3
|
+
|
|
2
4
|
import ActivityIndicator from "../ActivityIndicator"
|
|
3
5
|
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
id?: string
|
|
7
|
-
className
|
|
8
|
-
disabled: boolean
|
|
9
|
-
isLoading: boolean
|
|
10
|
-
|
|
7
|
+
interface Props {
|
|
8
|
+
id?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
title: string;
|
|
13
|
+
submittingTitle?: string;
|
|
14
|
+
onClick: () => void | Promise<void>;
|
|
15
|
+
children?: ReactNode;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
export const SubmitButton = ({
|
|
14
|
-
className = "",
|
|
15
19
|
id = "btn-submit",
|
|
20
|
+
className = "",
|
|
16
21
|
title,
|
|
17
22
|
submittingTitle,
|
|
18
23
|
children,
|
|
19
24
|
...props
|
|
20
25
|
}: Props) => {
|
|
21
26
|
|
|
22
|
-
const
|
|
27
|
+
const formContext = useFormContext()
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
let isSubmitting = formContext?.formState?.isSubmitting
|
|
25
30
|
|
|
26
31
|
const isDisabled = props.disabled || props.isLoading || isSubmitting
|
|
27
32
|
const isLoading = props.isLoading || isSubmitting
|
|
28
33
|
|
|
34
|
+
if (!formContext && isLoading) {
|
|
35
|
+
isSubmitting = true
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
if (title && children) {
|
|
30
39
|
throw new Error("button cannot have both title and children props")
|
|
31
40
|
}
|