Skip to content

Commit 6ba4844

Browse files
authored
Merge pull request #925 from nextcloud-libraries/feat/chunking-v2
2 parents 7f72e2d + 9da6748 commit 6ba4844

File tree

6 files changed

+82
-9
lines changed

6 files changed

+82
-9
lines changed

__tests__/utils/config.spec.ts

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ describe('Max chunk size tests', () => {
77
expect(getMaxChunksSize()).toBe(15 * 1024 * 1024)
88
})
99

10+
test('Returning valid config for chunking v2 minimum size', () => {
11+
Object.assign(window, {OC: {appConfig: {files: { max_chunk_size: 4 * 1024 * 1024 }}}})
12+
expect(getMaxChunksSize()).toBe(5 * 1024 * 1024)
13+
})
14+
15+
test('Returning valid config for chunking v2 maximum chunk count', () => {
16+
Object.assign(window, {OC: {appConfig: {files: { max_chunk_size: 5 * 1024 * 1024 }}}})
17+
expect(getMaxChunksSize(50 * 1024 * 1024 * 10000)).toBe(5 * 1024 * 1024 * 10)
18+
})
19+
1020
test('Returning disabled chunking config', () => {
1121
Object.assign(window, {OC: {appConfig: {files: { max_chunk_size: 0 }}}})
1222
expect(getMaxChunksSize()).toBe(0)

__tests__/utils/upload.spec.ts

+50
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,33 @@ describe('Initialize chunks upload temporary workspace', () => {
6767
url,
6868
})
6969
})
70+
71+
test('Init random workspace for file destination', async () => {
72+
axiosMock.request = vi.fn((config: any) => Promise.resolve(config?.onUploadProgress?.()))
73+
74+
// mock the current location for our assert on the URL
75+
Object.defineProperty(window, 'location', {
76+
value: new URL('https://cloud.domain.com'),
77+
configurable: true,
78+
})
79+
80+
// mock the current user
81+
document.head.setAttribute('data-user', 'test')
82+
83+
const url = await initChunkWorkspace('https://cloud.domain.com/remote.php/dav/files/test/image.jpg')
84+
85+
expect(url.startsWith('https://cloud.domain.com/remote.php/dav/uploads/test/web-file-upload-')).toBe(true)
86+
expect(url.length).toEqual('https://cloud.domain.com/remote.php/dav/uploads/test/web-file-upload-123456789abcdefg'.length)
87+
88+
expect(axiosMock.request).toHaveBeenCalledTimes(1)
89+
expect(axiosMock.request).toHaveBeenCalledWith({
90+
method: 'MKCOL',
91+
url,
92+
headers: {
93+
Destination: 'https://cloud.domain.com/remote.php/dav/files/test/image.jpg',
94+
},
95+
})
96+
})
7097
})
7198

7299
describe('Upload data', () => {
@@ -112,6 +139,29 @@ describe('Upload data', () => {
112139
})
113140
})
114141

142+
test('Upload data stream with destination', async () => {
143+
axiosMock.request = vi.fn((config: any) => Promise.resolve(config?.onUploadProgress()))
144+
145+
const url = 'https://cloud.domain.com/remote.php/dav/files/test/image.jpg'
146+
const blob = new Blob([new ArrayBuffer(50 * 1024 * 1024)])
147+
const signal = new AbortController().signal
148+
const onUploadProgress = vi.fn()
149+
await uploadData(url, blob, signal, onUploadProgress, url)
150+
151+
expect(onUploadProgress).toHaveBeenCalledTimes(1)
152+
expect(axiosMock.request).toHaveBeenCalledTimes(1)
153+
expect(axiosMock.request).toHaveBeenCalledWith({
154+
method: 'PUT',
155+
url,
156+
data: blob,
157+
signal,
158+
onUploadProgress,
159+
headers: {
160+
Destination: url,
161+
},
162+
})
163+
})
164+
115165
test('Upload cancellation', async () => {
116166
axiosMock.request = vi.fn((config: any) => Promise.resolve(config?.onUploadProgress()))
117167

lib/upload.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class Upload {
2525
private _response: AxiosResponse|null = null
2626

2727
constructor(source: string, chunked = false, size: number, file: File) {
28-
const chunks = getMaxChunksSize() > 0 ? Math.ceil(size / getMaxChunksSize()) : 1
28+
const chunks = Math.min(getMaxChunksSize() > 0 ? Math.ceil(size / getMaxChunksSize()) : 1, 10000)
2929
this._source = source
3030
this._isChunked = chunked && getMaxChunksSize() > 0 && chunks > 1
3131
this._chunks = this._isChunked ? chunks : 1

lib/uploader.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export class Uploader {
170170

171171
// If manually disabled or if the file is too small
172172
// TODO: support chunk uploading in public pages
173-
const maxChunkSize = getMaxChunksSize()
173+
const maxChunkSize = getMaxChunksSize(file.size)
174174
const disabledChunkUpload = maxChunkSize === 0
175175
|| file.size < maxChunkSize
176176
|| this._isPublic
@@ -188,7 +188,7 @@ export class Uploader {
188188
logger.debug('Initializing chunked upload', { file, upload })
189189

190190
// Let's initialize a chunk upload
191-
const tempUrl = await initChunkWorkspace()
191+
const tempUrl = await initChunkWorkspace(destinationFile)
192192
const chunksQueue: Array<Promise<any>> = []
193193

194194
// Generate chunks array
@@ -201,12 +201,12 @@ export class Uploader {
201201

202202
// Init request queue
203203
const request = () => {
204-
return uploadData(`${tempUrl}/${bufferEnd}`, blob, upload.signal, () => this.updateStats())
204+
return uploadData(`${tempUrl}/${chunk+1}`, blob, upload.signal, () => this.updateStats(), destinationFile)
205205
// Update upload progress on chunk completion
206206
.then(() => { upload.uploaded = upload.uploaded + maxChunkSize })
207207
.catch((error) => {
208208
if (!(error instanceof CanceledError)) {
209-
logger.error(`Chunk ${bufferStart} - ${bufferEnd} uploading failed`)
209+
logger.error(`Chunk ${chunk+1} ${bufferStart} - ${bufferEnd} uploading failed`)
210210
upload.status = UploadStatus.FAILED
211211
}
212212
throw error

lib/utils/config.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const getMaxChunksSize = function(): number {
1+
export const getMaxChunksSize = function(fileSize: number | undefined = undefined): number {
22
const maxChunkSize = window.OC?.appConfig?.files?.max_chunk_size
33
if (maxChunkSize <= 0) {
44
return 0
@@ -9,5 +9,13 @@ export const getMaxChunksSize = function(): number {
99
return 10 * 1024 * 1024
1010
}
1111

12-
return Number(maxChunkSize)
12+
// v2 of chunked upload requires chunks to be 5 MB at minimum
13+
const minimumChunkSize = Math.max(Number(maxChunkSize), 5 * 1024 * 1024)
14+
15+
if (fileSize === undefined) {
16+
return minimumChunkSize
17+
}
18+
19+
// Adapt chunk size to fit the file in 10000 chunks for chunked upload v2
20+
return Math.max(minimumChunkSize, Math.ceil(fileSize / 10000))
1321
}

lib/utils/upload.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ type UploadData = Blob | (() => Promise<Blob>)
1212
/**
1313
* Upload some data to a given path
1414
*/
15-
export const uploadData = async function(url: string, uploadData: UploadData, signal: AbortSignal, onUploadProgress = () => {}): Promise<AxiosResponse> {
15+
export const uploadData = async function(url: string, uploadData: UploadData, signal: AbortSignal, onUploadProgress = () => {}, destinationFile: string | undefined = undefined): Promise<AxiosResponse> {
1616
let data: Blob
1717

1818
if (uploadData instanceof Blob) {
@@ -21,12 +21,15 @@ export const uploadData = async function(url: string, uploadData: UploadData, si
2121
data = await uploadData()
2222
}
2323

24+
const headers = destinationFile ? { Destination: destinationFile } : undefined
25+
2426
return await axios.request({
2527
method: 'PUT',
2628
url,
2729
data,
2830
signal,
2931
onUploadProgress,
32+
headers,
3033
})
3134
}
3235

@@ -57,15 +60,17 @@ export const getChunk = function(file: File, start: number, length: number): Pro
5760
/**
5861
* Create a temporary upload workspace to upload the chunks to
5962
*/
60-
export const initChunkWorkspace = async function(): Promise<string> {
63+
export const initChunkWorkspace = async function(destinationFile: string | undefined = undefined): Promise<string> {
6164
const chunksWorkspace = generateRemoteUrl(`dav/uploads/${getCurrentUser()?.uid}`)
6265
const hash = [...Array(16)].map(() => Math.floor(Math.random() * 16).toString(16)).join('')
6366
const tempWorkspace = `web-file-upload-${hash}`
6467
const url = `${chunksWorkspace}/${tempWorkspace}`
68+
const headers = destinationFile ? { Destination: destinationFile } : undefined
6569

6670
await axios.request({
6771
method: 'MKCOL',
6872
url,
73+
headers,
6974
})
7075

7176
return url

0 commit comments

Comments
 (0)