/
uploader.js
124 lines (112 loc) · 5.02 KB
/
uploader.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
const initUploader = (
URL_FILE_UPLOAD,
authFetch
) => {
const {
document,
qS, cE,
Dr: {
Common: {
Format,
Time: { createStepper },
Error: { catchAsync },
Function: { withRetryAsync },
Immutable: { Object: { objectSet, objectDelete, objectPickKey } }
},
Browser: { Module: { FileChunkUpload: { uploadFileByChunk } } }
}
} = window
const initialUploaderState = {
isActive: false,
uploadFileList: [ /* { key, fileBlob } */ ],
uploadProgress: { /* [key]: progress[0,1] */ },
uploadStatus: ''
}
const getUploadFileAsync = (uploaderStore, onUploadComplete) => async () => {
const { uploadFileList: fileList } = uploaderStore.getState()
uploaderStore.setState({ uploadStatus: 'uploading' })
const stepper = createStepper()
const uploadStatusList = []
for (const { key, fileBlob } of fileList) {
const onProgress = (current, total) => uploaderStore.setState({
uploadProgress: objectSet(uploaderStore.getState().uploadProgress, key, total ? (current / total) : 1)
})
const { error } = await catchAsync(uploadFileByChunk, {
fileBlob,
key,
onProgress,
uploadChunk: async (arrayBufferPacket, { key, chunkIndex, chunkTotal }) => withRetryAsync(
async () => authFetch(URL_FILE_UPLOAD, { method: 'POST', body: arrayBufferPacket }),
4, // maxRetry,
1000 // wait
)
})
error && uploadStatusList.push(`Error upload '${key}': ${error.stack || (error.target && error.target.error) || error}`)
}
uploadStatusList.push(`Done in ${Format.time(stepper())} for ${fileList.length} file`)
{
const { uploadFileList, uploadProgress } = uploaderStore.getState()
uploaderStore.setState({
uploadFileList: uploadFileList.filter((v) => !fileList.includes(v)),
uploadProgress: objectPickKey(uploadProgress, Object.keys(uploadProgress).filter((key) => !fileList.find((v) => v.key === key))),
uploadStatus: uploadStatusList.join('\n')
})
}
await onUploadComplete()
}
const getAppendUploadFileList = (uploaderStore, getExtraState) => (fileList = []) => {
const { shouldAppend, relativePath } = getExtraState()
if (!shouldAppend) return
fileList = Array.from(fileList) // NOTE: convert FileList, for Edge support, do not use `...fileList`
const dedupSet = new Set()
const uploadFileList = [
...fileList.map((fileBlob) => ({ key: `${relativePath}/${fileBlob.name}`, fileBlob })),
...uploaderStore.getState().uploadFileList
].filter(({ key }) => dedupSet.has(key) ? false : dedupSet.add(key))
const uploadProgress = uploadFileList.reduce(
(uploadProgress, { key }) => objectDelete(uploadProgress, key),
uploaderStore.getState().uploadProgress
)
uploaderStore.setState({ isActive: true, uploadFileList, uploadProgress, uploadStatus: '' })
}
const renderUploader = (uploaderStore, uploadFile, appendUploadFileList) => {
const { isActive, uploadFileList, uploadProgress, uploadStatus } = uploaderStore.getState()
let uploadBlockDiv = qS('#upload-panel')
if (!isActive) return uploadBlockDiv && uploadBlockDiv.remove()
uploadBlockDiv = uploadBlockDiv || document.body.appendChild(cE('div', {
id: 'upload-panel',
style: 'overflow: hidden; position: absolute; bottom: 0; right: 0; margin: 8px; background: var(--ct-bg-n); box-shadow: 0 0 2px 0 #888;',
innerHTML: [
'<div style="overflow-x: auto; display: flex; flex-flow: row nowrap; box-shadow: 0 0 8px 0 #888;">',
...[
'<button class="edit">Upload</button>',
'<button class="edit">Clear</button>',
'<div style="flex: 1;"></div>',
'<button class="edit" style="align-self: flex-end;">❌</button>'
],
'</div>',
'<label>Select file: <input type="file" multiple/></label>',
'<pre style="overflow: auto; padding: 8px 4px; max-width: 80vw; max-height: 60vh; min-height: 64px; color: #888;"></pre>'
].join('<br />')
}))
uploadBlockDiv.querySelector('pre').innerText = [
...uploadFileList.map(({ key, fileBlob: { size } }) =>
`[${Format.percent(uploadProgress[ key ] || 0).padStart(7, ' ')}] - ${key} (${Format.binary(size)}B)`
),
uploadStatus
].filter(Boolean).join('\n') || 'or drop file here'
const [ uploadButton, clearButton, removeBlockButton ] = uploadBlockDiv.querySelectorAll('button')
uploadButton.addEventListener('click', uploadFile)
clearButton.addEventListener('click', () => uploaderStore.setState({ ...initialUploaderState, isActive: true }))
removeBlockButton.addEventListener('click', () => uploaderStore.setState({ isActive: false }))
const uploadFileListInput = uploadBlockDiv.querySelector('input[type="file"]')
uploadFileListInput.addEventListener('change', () => appendUploadFileList(uploadFileListInput.files))
}
return {
initialUploaderState,
getUploadFileAsync,
getAppendUploadFileList,
renderUploader
}
}
export { initUploader }