-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathupload.ts
More file actions
146 lines (129 loc) · 4.51 KB
/
upload.ts
File metadata and controls
146 lines (129 loc) · 4.51 KB
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { AxiosProgressEvent, AxiosResponse, AxiosError } from 'axios'
import { generateRemoteUrl, getBaseUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import axios from '@nextcloud/axios'
import axiosRetry, { exponentialDelay, isNetworkOrIdempotentRequestError } from 'axios-retry'
import { getSharingToken } from '@nextcloud/sharing/public'
import logger from './logger'
axiosRetry(axios, { retries: 0 })
type UploadData = Blob | (() => Promise<Blob>)
interface UploadDataOptions {
/** The abort signal */
signal: AbortSignal
/** Upload progress event callback */
onUploadProgress?: (event: AxiosProgressEvent) => void
/** Request retry callback (e.g. network error of previous try) */
onUploadRetry?: () => void
/** The final destination file (for chunked uploads) */
destinationFile?: string
/** Additional headers */
headers?: Record<string, string|number>
/** Number of retries */
retries?: number,
}
/**
* Upload some data to a given path
* @param url the url to upload to
* @param uploadData the data to upload
* @param uploadOptions upload options
*/
export async function uploadData(
url: string,
uploadData: UploadData,
uploadOptions: UploadDataOptions,
): Promise<AxiosResponse> {
const options = {
headers: {},
onUploadProgress: () => {},
onUploadRetry: () => {},
retries: 5,
...uploadOptions,
}
let data: Blob
// If the upload data is a blob, we can directly use it
// Otherwise, we need to wait for the promise to resolve
if (uploadData instanceof Blob) {
data = uploadData
} else {
data = await uploadData()
}
// Helps the server to know what to do with the file afterwards (e.g. chunked upload)
if (options.destinationFile) {
options.headers.Destination = options.destinationFile
}
// If no content type is set, we default to octet-stream
if (!options.headers['Content-Type']) {
options.headers['Content-Type'] = 'application/octet-stream'
}
return await axios.request({
method: 'PUT',
url,
data,
signal: options.signal,
onUploadProgress: options.onUploadProgress,
headers: options.headers,
'axios-retry': {
retries: options.retries,
retryDelay: (retryCount: number, error: AxiosError) => exponentialDelay(retryCount, error, 1000),
retryCondition(error: AxiosError): boolean {
// Do not retry on insufficient storage - this is permanent
if (error.status === 507) {
return false
}
// Do a retry on locked error as this is often just some preview generation
if (error.status === 423) {
return true
}
// Otherwise fallback to default behavior
return isNetworkOrIdempotentRequestError(error)
},
onRetry: options.onUploadRetry,
},
})
}
/**
* Get chunk of the file.
* Doing this on the fly give us a big performance boost and proper garbage collection
* @param file File to upload
* @param start Offset to start upload
* @param length Size of chunk to upload
*/
export const getChunk = function(file: File, start: number, length: number): Promise<Blob> {
if (start === 0 && file.size <= length) {
return Promise.resolve(new Blob([file], { type: file.type || 'application/octet-stream' }))
}
return Promise.resolve(new Blob([file.slice(start, start + length)], { type: 'application/octet-stream' }))
}
/**
* Create a temporary upload workspace to upload the chunks to
* @param destinationFile The file name after finishing the chunked upload
* @param retries number of retries
* @param isPublic whether this upload is in a public share or not
*/
export const initChunkWorkspace = async function(destinationFile: string | undefined = undefined, retries: number = 5, isPublic: boolean = false): Promise<string> {
let chunksWorkspace: string
if (isPublic) {
chunksWorkspace = `${getBaseUrl()}/public.php/dav/uploads/${getSharingToken()}`
} else {
chunksWorkspace = generateRemoteUrl(`dav/uploads/${getCurrentUser()?.uid}`)
}
const hash = [...Array(16)].map(() => Math.floor(Math.random() * 16).toString(16)).join('')
const tempWorkspace = `web-file-upload-${hash}`
const url = `${chunksWorkspace}/${tempWorkspace}`
const headers = destinationFile ? { Destination: destinationFile } : undefined
await axios.request({
method: 'MKCOL',
url,
headers,
'axios-retry': {
retries,
retryDelay: (retryCount: number, error: AxiosError) => exponentialDelay(retryCount, error, 1000),
},
})
logger.debug('Created temporary upload workspace', { url })
return url
}