@rpcbase/client 0.195.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/client",
3
- "version": "0.195.0",
3
+ "version": "0.197.0-fileupload.0",
4
4
  "scripts": {
5
5
  "build": "../../node_modules/.bin/wireit",
6
6
  "test": "../../node_modules/.bin/wireit"
@@ -98,9 +98,9 @@
98
98
  "js-tree": "2.0.2",
99
99
  "lz-string": "1.5.0",
100
100
  "posthog-js": "1.147.0",
101
- "pouchdb-adapter-indexeddb": "8.0.1",
102
- "pouchdb-core": "8.0.1",
103
- "pouchdb-find": "8.0.1",
101
+ "pouchdb-adapter-indexeddb": "9.0.0",
102
+ "pouchdb-core": "9.0.0",
103
+ "pouchdb-find": "9.0.0",
104
104
  "react-bootstrap-typeahead": "6.3.2",
105
105
  "react-i18next": "15.0.1",
106
106
  "rrweb": "1.1.3",
@@ -1,5 +1,4 @@
1
1
  /* @flow */
2
- import assert from "assert"
3
2
  import {Platform} from "react-native"
4
3
  import {useCallback, useEffect, useState, useMemo, useId, useRef} from "react"
5
4
  import debug from "debug"
@@ -25,9 +24,10 @@ const getUseQuery = (register_query) => (
25
24
  ) => {
26
25
  const id = useId()
27
26
 
27
+ // TODO: should the uid be gone here ? it's part of the auth layer, not this here
28
28
  // TODO: retrieve this from future AuthContext in client
29
29
  const uid = useMemo(() => {
30
- // TODO: why is there a options.userId here ??
30
+ // TODO: why is there a options.userId here?? (it was for mobile we need to unify this)
31
31
  const _uid = Platform.OS === "web" ? getUid() : options.userId
32
32
  return _uid
33
33
  }, [])
@@ -75,12 +75,16 @@ const getUseQuery = (register_query) => (
75
75
  // set data in a ref so that it doesn't force re-rendering ie: unsubscribe / resubscribe
76
76
  dataRef.current = newData
77
77
 
78
+ // useStorage currently used as a fast local cache, indexedDB
78
79
  // we only save network queries
80
+ // TODO: use localstorage in react native and pouchdb everywhere
79
81
  if (useStorage && context.source === "network") {
80
82
  if (Platform.OS === "web") {
81
83
  localStorage.setItem(storageKey, LZString.compressToUTF16(JSON.stringify(newData)))
82
84
  } else {
85
+ // TODO: this is done in pouchDB nOW ?????
83
86
  // TODO: RN MMKV
87
+ console.log("mmkv NYI")
84
88
  }
85
89
  }
86
90
 
@@ -147,7 +151,7 @@ const getUseQuery = (register_query) => (
147
151
  newData = _omit(queryResult, "__txn_id")
148
152
  }
149
153
 
150
- // We return once in any case!
154
+ // we return once in any case
151
155
  if (!hasFirstReply.current) {
152
156
  hasFirstReply.current = true
153
157
 
@@ -168,7 +172,7 @@ const getUseQuery = (register_query) => (
168
172
  return
169
173
  }
170
174
 
171
- if (isEqual(data, newData) && !isEqualValues(data, newData) && __DEV__) {
175
+ if (__DEV__ && isEqual(data, newData) && !isEqualValues(data, newData)) {
172
176
  alert("EQUALITY MISMATCH THIS SHOULD NOT HAPPEN!", data, newData)
173
177
  }
174
178
 
@@ -191,11 +195,11 @@ const getUseQuery = (register_query) => (
191
195
 
192
196
 
193
197
  const loadNextPage = useCallback(() => {
194
- console.log("NYI: will load next page after DOC")
198
+ console.log("loadNextPage NYI")
195
199
  }, [])
196
200
 
197
201
 
198
- const result = useMemo(() => ({data, source, error, loading}), [data, source, error, loading])
202
+ const result = useMemo(() => ({data, source, error, loading, loadNextPage}), [data, source, error, loading, loadNextPage])
199
203
 
200
204
  // TODO:
201
205
  // if (Array.isArray(result.data) && !result.source) {
package/rts/rts.js CHANGED
@@ -10,6 +10,7 @@ import getBaseUrl from "../getBaseUrl"
10
10
 
11
11
  import store from "./store"
12
12
 
13
+
13
14
  const log = debug("rb:socket")
14
15
 
15
16
  const TENANT_ID_HEADER = "rb-tenant-id"
@@ -197,8 +198,6 @@ export const register_query = (model_name, query, _options, _callback) => {
197
198
  }
198
199
  _queries_store[model_name][query]
199
200
 
200
- console.log("QUERY OPTIONSSS", options)
201
-
202
201
  // TODO: why both run and register query here ? the run_query should come straight from register ?
203
202
  _socket.emit("run_query", {model_name, query, query_key, options})
204
203
  _socket.emit("register_query", {model_name, query, query_key, options})
@@ -6,22 +6,26 @@ import FindPlugin from "pouchdb-find"
6
6
 
7
7
  import {RB_TENANT_ID, RB_APP_NAME} from "env"
8
8
 
9
+
9
10
  const log = debug("rb:rts:store")
10
11
 
11
- let prefix = `rb/${RB_TENANT_ID}/`
12
+ let prefix = `rb/`
12
13
 
13
14
  if (RB_APP_NAME) prefix += `${RB_APP_NAME}/`
14
15
 
16
+ prefix += `${RB_TENANT_ID}/`
17
+
18
+ log("prefix:", prefix)
19
+
15
20
  PouchDB.prefix = prefix
16
21
 
17
22
  PouchDB.plugin(IndexedDBAdapter)
18
23
  PouchDB.plugin(FindPlugin)
19
24
 
20
-
21
25
  const _cols_store = Object.create(null)
22
26
 
23
-
24
27
  const get_collection = (col_name) => {
28
+
25
29
  if (!col_name) {
26
30
  console.warn("supplied invalid / empty collection name to rts")
27
31
  }
@@ -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}&nbsp;%</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
- type Props = {
6
- id?: string,
7
- className: string,
8
- disabled: boolean,
9
- isLoading: boolean,
10
- onClick: Function,
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 {formState} = useFormContext()
27
+ const formContext = useFormContext()
23
28
 
24
- const {isSubmitting} = formState
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
  }