UNPKG

@nextcloud/upload

Version:
1 lines 256 kB
{"version":3,"file":"index-Ev2UnrH4.cjs","sources":["../../lib/utils/l10n.ts","../../lib/errors/UploadCancelledError.ts","../../lib/utils/logger.ts","../../lib/utils/upload.ts","../../lib/utils/config.ts","../../lib/upload.ts","../../lib/utils/filesystem.ts","../../lib/utils/fileTree.ts","../../lib/uploader/eta.ts","../../lib/uploader/uploader.ts","../../lib/getUploader.ts","../../lib/utils/conflicts.ts","../../lib/dialogs/openConflictPicker.ts","../../lib/dialogs/utils/dialog.ts","../../lib/dialogs/utils/uploadConflictHandler.ts","../../node_modules/vue-material-design-icons/Cancel.vue","../../node_modules/vue-material-design-icons/FolderUpload.vue","../../node_modules/vue-material-design-icons/Plus.vue","../../node_modules/vue-material-design-icons/Upload.vue","../../lib/vue/components/UploadPicker.vue"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\nimport { getGettextBuilder } from '@nextcloud/l10n/gettext'\n\nconst gtBuilder = getGettextBuilder()\n\t.detectLocale()\n\n// @ts-expect-error __TRANSLATIONS__ is replaced by vite\n__TRANSLATIONS__.map(data => gtBuilder.addTranslation(data.locale, data.json))\n\ninterface Gettext {\n\t/**\n\t * Get translated string (singular form), optionally with placeholders\n\t *\n\t * @param original original string to translate\n\t * @param placeholders map of placeholder key to value\n\t */\n\tgettext(original: string, placeholders?: Record<string, string | number>): string\n\n\t/**\n\t * Get translated string with plural forms\n\t *\n\t * @param singular Singular text form\n\t * @param plural Plural text form to be used if `count` requires it\n\t * @param count The number to insert into the text\n\t * @param placeholders optional map of placeholder key to value\n\t */\n\tngettext(singular: string, plural: string, count: number, placeholders?: Record<string, string | number>): string\n}\n\nconst gt = gtBuilder.build() as Gettext\n\nexport const n = gt.ngettext.bind(gt) as typeof gt.ngettext\nexport const t = gt.gettext.bind(gt) as typeof gt.gettext\n","/*!\n * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\nimport { t } from '../utils/l10n.ts'\n\nexport class UploadCancelledError extends Error {\n\n\tpublic constructor(cause?: unknown) {\n\t\tsuper(t('Upload has been cancelled'), { cause })\n\t}\n\n}\n","/**\n * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\n\nimport { getLoggerBuilder } from '@nextcloud/logger'\n\nexport default getLoggerBuilder()\n\t.setApp('@nextcloud/upload')\n\t.detectUser()\n\t.build()\n","/**\n * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\nimport type { AxiosProgressEvent, AxiosResponse, AxiosError } from 'axios'\nimport { generateRemoteUrl, getBaseUrl } from '@nextcloud/router'\nimport { getCurrentUser } from '@nextcloud/auth'\nimport axios from '@nextcloud/axios'\nimport axiosRetry, { exponentialDelay, isNetworkOrIdempotentRequestError } from 'axios-retry'\nimport { getSharingToken } from '@nextcloud/sharing/public'\n\nimport logger from './logger'\n\naxiosRetry(axios, { retries: 0 })\n\ntype UploadData = Blob | (() => Promise<Blob>)\n\ninterface UploadDataOptions {\n\t/** The abort signal */\n\tsignal: AbortSignal\n\t/** Upload progress event callback */\n\tonUploadProgress?: (event: AxiosProgressEvent) => void\n\t/** Request retry callback (e.g. network error of previous try) */\n\tonUploadRetry?: () => void\n\t/** The final destination file (for chunked uploads) */\n\tdestinationFile?: string\n\t/** Additional headers */\n\theaders?: Record<string, string|number>\n\t/** Number of retries */\n\tretries?: number,\n}\n\n/**\n * Upload some data to a given path\n * @param url the url to upload to\n * @param uploadData the data to upload\n * @param uploadOptions upload options\n */\nexport async function uploadData(\n\turl: string,\n\tuploadData: UploadData,\n\tuploadOptions: UploadDataOptions,\n): Promise<AxiosResponse> {\n\tconst options = {\n\t\theaders: {},\n\t\tonUploadProgress: () => {},\n\t\tonUploadRetry: () => {},\n\t\tretries: 5,\n\t\t...uploadOptions,\n\t}\n\n\tlet data: Blob\n\n\t// If the upload data is a blob, we can directly use it\n\t// Otherwise, we need to wait for the promise to resolve\n\tif (uploadData instanceof Blob) {\n\t\tdata = uploadData\n\t} else {\n\t\tdata = await uploadData()\n\t}\n\n\t// Helps the server to know what to do with the file afterwards (e.g. chunked upload)\n\tif (options.destinationFile) {\n\t\toptions.headers.Destination = options.destinationFile\n\t}\n\n\t// If no content type is set, we default to octet-stream\n\tif (!options.headers['Content-Type']) {\n\t\toptions.headers['Content-Type'] = 'application/octet-stream'\n\t}\n\n\treturn await axios.request({\n\t\tmethod: 'PUT',\n\t\turl,\n\t\tdata,\n\t\tsignal: options.signal,\n\t\tonUploadProgress: options.onUploadProgress,\n\t\theaders: options.headers,\n\t\t'axios-retry': {\n\t\t\tretries: options.retries,\n\t\t\tretryDelay: (retryCount: number, error: AxiosError) => exponentialDelay(retryCount, error, 1000),\n\t\t\tretryCondition(error: AxiosError): boolean {\n\t\t\t\t// Do not retry on insufficient storage - this is permanent\n\t\t\t\tif (error.status === 507) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\t// Do a retry on locked error as this is often just some preview generation\n\t\t\t\tif (error.status === 423) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\t// Otherwise fallback to default behavior\n\t\t\t\treturn isNetworkOrIdempotentRequestError(error)\n\t\t\t},\n\t\t\tonRetry: options.onUploadRetry,\n\t\t},\n\t})\n}\n\n/**\n * Get chunk of the file.\n * Doing this on the fly give us a big performance boost and proper garbage collection\n * @param file File to upload\n * @param start Offset to start upload\n * @param length Size of chunk to upload\n */\nexport const getChunk = function(file: File, start: number, length: number): Promise<Blob> {\n\tif (start === 0 && file.size <= length) {\n\t\treturn Promise.resolve(new Blob([file], { type: file.type || 'application/octet-stream' }))\n\t}\n\n\treturn Promise.resolve(new Blob([file.slice(start, start + length)], { type: 'application/octet-stream' }))\n}\n\n/**\n * Create a temporary upload workspace to upload the chunks to\n * @param destinationFile The file name after finishing the chunked upload\n * @param retries number of retries\n * @param isPublic whether this upload is in a public share or not\n * @param customHeaders Custom HTTP headers used when creating the workspace (e.g. X-NC-Nickname for file drops)\n */\nexport const initChunkWorkspace = async function(destinationFile: string | undefined = undefined, retries: number = 5, isPublic: boolean = false, customHeaders: Record<string, string> = {}): Promise<string> {\n\tlet chunksWorkspace: string\n\tif (isPublic) {\n\t\tchunksWorkspace = `${getBaseUrl()}/public.php/dav/uploads/${getSharingToken()}`\n\t} else {\n\t\tchunksWorkspace = generateRemoteUrl(`dav/uploads/${getCurrentUser()?.uid}`)\n\t}\n\n\tconst hash = [...Array(16)].map(() => Math.floor(Math.random() * 16).toString(16)).join('')\n\tconst tempWorkspace = `web-file-upload-${hash}`\n\tconst url = `${chunksWorkspace}/${tempWorkspace}`\n\tconst headers = customHeaders\n\tif (destinationFile) {\n\t\theaders.Destination = destinationFile\n\t}\n\n\tawait axios.request({\n\t\tmethod: 'MKCOL',\n\t\turl,\n\t\theaders,\n\t\t'axios-retry': {\n\t\t\tretries,\n\t\t\tretryDelay: (retryCount: number, error: AxiosError) => exponentialDelay(retryCount, error, 1000),\n\t\t},\n\t})\n\n\tlogger.debug('Created temporary upload workspace', { url })\n\n\treturn url\n}\n","/*!\n * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\nexport const getMaxChunksSize = function(fileSize: number | undefined = undefined): number {\n\tconst maxChunkSize = window.OC?.appConfig?.files?.max_chunk_size\n\tif (maxChunkSize <= 0) {\n\t\treturn 0\n\t}\n\n\t// If invalid return default\n\tif (!Number(maxChunkSize)) {\n\t\treturn 10 * 1024 * 1024\n\t}\n\n\t// v2 of chunked upload requires chunks to be 5 MB at minimum\n\tconst minimumChunkSize = Math.max(Number(maxChunkSize), 5 * 1024 * 1024)\n\n\tif (fileSize === undefined) {\n\t\treturn minimumChunkSize\n\t}\n\n\t// Adapt chunk size to fit the file in 10000 chunks for chunked upload v2\n\treturn Math.max(minimumChunkSize, Math.ceil(fileSize / 10000))\n}\n","/**\n * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\nimport type { AxiosResponse } from 'axios'\nimport { getMaxChunksSize } from './utils/config.js'\n\nexport enum Status {\n\tINITIALIZED = 0,\n\tUPLOADING = 1,\n\tASSEMBLING = 2,\n\tFINISHED = 3,\n\tCANCELLED = 4,\n\tFAILED = 5,\n}\nexport class Upload {\n\n\tprivate _source: string\n\tprivate _file: File\n\tprivate _isChunked: boolean\n\tprivate _chunks: number\n\n\tprivate _size: number\n\tprivate _uploaded = 0\n\tprivate _startTime = 0\n\n\tprivate _status: Status = Status.INITIALIZED\n\tprivate _controller: AbortController\n\tprivate _response: AxiosResponse|null = null\n\n\tconstructor(source: string, chunked = false, size: number, file: File) {\n\t\tconst chunks = Math.min(getMaxChunksSize() > 0 ? Math.ceil(size / getMaxChunksSize()) : 1, 10000)\n\t\tthis._source = source\n\t\tthis._isChunked = chunked && getMaxChunksSize() > 0 && chunks > 1\n\t\tthis._chunks = this._isChunked ? chunks : 1\n\t\tthis._size = size\n\t\tthis._file = file\n\t\tthis._controller = new AbortController()\n\t}\n\n\tget source(): string {\n\t\treturn this._source\n\t}\n\n\tget file(): File {\n\t\treturn this._file\n\t}\n\n\tget isChunked(): boolean {\n\t\treturn this._isChunked\n\t}\n\n\tget chunks(): number {\n\t\treturn this._chunks\n\t}\n\n\tget size(): number {\n\t\treturn this._size\n\t}\n\n\tget startTime(): number {\n\t\treturn this._startTime\n\t}\n\n\tset response(response: AxiosResponse|null) {\n\t\tthis._response = response\n\t}\n\n\tget response(): AxiosResponse|null {\n\t\treturn this._response\n\t}\n\n\tget uploaded(): number {\n\t\treturn this._uploaded\n\t}\n\n\t/**\n\t * Update the uploaded bytes of this upload\n\t */\n\tset uploaded(length: number) {\n\t\tif (length >= this._size) {\n\t\t\tthis._status = this._isChunked\n\t\t\t\t? Status.ASSEMBLING\n\t\t\t\t: Status.FINISHED\n\t\t\tthis._uploaded = this._size\n\t\t\treturn\n\t\t}\n\n\t\tthis._status = Status.UPLOADING\n\t\tthis._uploaded = length\n\n\t\t// If first progress, let's log the start time\n\t\tif (this._startTime === 0) {\n\t\t\tthis._startTime = new Date().getTime()\n\t\t}\n\t}\n\n\tget status(): number {\n\t\treturn this._status\n\t}\n\n\t/**\n\t * Update this upload status\n\t */\n\tset status(status: Status) {\n\t\tthis._status = status\n\t}\n\n\t/**\n\t * Returns the axios cancel token source\n\t */\n\tget signal(): AbortSignal {\n\t\treturn this._controller.signal\n\t}\n\n\t/**\n\t * Cancel any ongoing requests linked to this upload\n\t */\n\tcancel() {\n\t\tthis._controller.abort()\n\t\tthis._status = Status.CANCELLED\n\t}\n\n}\n","/*!\n * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\n// Helpers for the File and Directory API\n\n// Helper to support browser that do not support the API\nexport const isFileSystemDirectoryEntry = (o: unknown): o is FileSystemDirectoryEntry => 'FileSystemDirectoryEntry' in window && o instanceof FileSystemDirectoryEntry\n\nexport const isFileSystemFileEntry = (o: unknown): o is FileSystemFileEntry => 'FileSystemFileEntry' in window && o instanceof FileSystemFileEntry\n\nexport const isFileSystemEntry = (o: unknown): o is FileSystemEntry => 'FileSystemEntry' in window && o instanceof FileSystemEntry\n","/**\n * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\n/**\n * Helpers to generate a file tree when the File and Directory API is used (e.g. Drag and Drop or <input type=\"file\" webkitdirectory>)\n */\n\nimport { basename } from '@nextcloud/paths'\nimport { isFileSystemDirectoryEntry, isFileSystemFileEntry } from './filesystem.ts'\n\n/**\n * This is a helper class to allow building a file tree for uploading\n * It allows to create virtual directories\n */\nexport class Directory extends File {\n\n\tprivate _originalName: string\n\tprivate _path: string\n\tprivate _children: Map<string, File|this>\n\n\tconstructor(path: string) {\n\t\tsuper([], basename(path), { type: 'httpd/unix-directory', lastModified: 0 })\n\t\tthis._children = new Map()\n\t\tthis._originalName = basename(path)\n\t\tthis._path = path\n\t}\n\n\tget size(): number {\n\t\treturn this.children.reduce((sum, file) => sum + file.size, 0)\n\t}\n\n\tget lastModified(): number {\n\t\treturn this.children.reduce((latest, file) => Math.max(latest, file.lastModified), 0)\n\t}\n\n\t// We need this to keep track of renamed files\n\tget originalName(): string {\n\t\treturn this._originalName\n\t}\n\n\tget children(): Array<File|Directory> {\n\t\treturn Array.from(this._children.values())\n\t}\n\n\tget webkitRelativePath(): string {\n\t\treturn this._path\n\t}\n\n\tgetChild(name: string): File|Directory|null {\n\t\treturn this._children.get(name) ?? null\n\t}\n\n\t/**\n\t * Add multiple children at once\n\t * @param files The files to add\n\t */\n\tasync addChildren(files: Array<File|FileSystemEntry>): Promise<void> {\n\t\tfor (const file of files) {\n\t\t\tawait this.addChild(file)\n\t\t}\n\t}\n\n\t/**\n\t * Add a child to the directory.\n\t * If it is a nested child the parents will be created if not already exist.\n\t * @param file The child to add\n\t */\n\tasync addChild(file: File|FileSystemEntry) {\n\t\tconst rootPath = this._path && `${this._path}/`\n\t\tif (isFileSystemFileEntry(file)) {\n\t\t\tfile = await new Promise<File>((resolve, reject) => (file as FileSystemFileEntry).file(resolve, reject))\n\t\t} else if (isFileSystemDirectoryEntry(file)) {\n\t\t\tconst reader = file.createReader()\n\t\t\tconst entries = await new Promise<FileSystemEntry[]>((resolve, reject) => reader.readEntries(resolve, reject))\n\n\t\t\t// Create a new child directory and add the entries\n\t\t\tconst child = new Directory(`${rootPath}${file.name}`)\n\t\t\tawait child.addChildren(entries)\n\t\t\tthis._children.set(file.name, child)\n\t\t\treturn\n\t\t}\n\n\t\t// Make Typescript calm - we ensured it is not a file system entry above.\n\t\tfile = file as File\n\n\t\tconst filePath = file.webkitRelativePath ?? file.name\n\t\t// Handle plain files\n\t\tif (!filePath.includes('/')) {\n\t\t\t// Direct child of the directory\n\t\t\tthis._children.set(file.name, file)\n\t\t} else {\n\t\t\t// Check if file is a child\n\t\t\tif (!filePath.startsWith(this._path)) {\n\t\t\t\tthrow new Error(`File ${filePath} is not a child of ${this._path}`)\n\t\t\t}\n\n\t\t\t// If file is a child check if we need to nest it\n\t\t\tconst relPath = filePath.slice(rootPath.length)\n\t\t\tconst name = basename(relPath)\n\n\t\t\tif (name === relPath) {\n\t\t\t\t// It is a direct child so we can add it\n\t\t\t\tthis._children.set(name, file)\n\t\t\t} else {\n\t\t\t\t// It is not a direct child so we need to create intermediate nodes\n\t\t\t\tconst base = relPath.slice(0, relPath.indexOf('/'))\n\t\t\t\tif (this._children.has(base)) {\n\t\t\t\t\t// It is a grandchild so we can add it directly\n\t\t\t\t\tawait (this._children.get(base) as Directory).addChild(file)\n\t\t\t\t} else {\n\t\t\t\t\t// We do not know any parent of that child\n\t\t\t\t\t// so we need to add a new child on the current level\n\t\t\t\t\tconst child = new Directory(`${rootPath}${base}`)\n\t\t\t\t\tawait child.addChild(file)\n\t\t\t\t\tthis._children.set(base, child)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n}\n\n/**\n * Interface of the internal Directory class\n */\nexport type IDirectory = Pick<Directory, keyof Directory>\n","/*!\n * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\n\nimport { TypedEventTarget } from 'typescript-event-target'\nimport { n, t } from '../utils/l10n.ts'\nimport { formatFileSize } from '@nextcloud/files'\n\nexport enum EtaStatus {\n\tIdle = 0,\n\tPaused = 1,\n\tRunning = 2,\n}\n\ninterface EtaOptions {\n\t/** Low pass filter cutoff time for smoothing the speed */\n\tcutoffTime?: number\n\t/** Total number of bytes to be expected */\n\ttotal?: number\n\t/** Start the estimation directly */\n\tstart?: boolean\n}\n\nexport interface EtaEventsMap {\n\tpause: CustomEvent\n\treset: CustomEvent\n\tresume: CustomEvent\n\tupdate: CustomEvent\n}\n\nexport class Eta extends TypedEventTarget<EtaEventsMap> {\n\n\t/** Bytes done */\n\tprivate _done: number = 0\n\t/** Total bytes to do */\n\tprivate _total: number = 0\n\t/** Current progress (cached) as interval [0,1] */\n\tprivate _progress: number = 0\n\t/** Status of the ETA */\n\tprivate _status: EtaStatus = EtaStatus.Idle\n\t/** Time of the last update */\n\tprivate _startTime: number = -1\n\t/** Total elapsed time for current ETA */\n\tprivate _elapsedTime: number = 0\n\t/** Current speed in bytes per second */\n\tprivate _speed: number = -1\n\t/** Expected duration to finish in seconds */\n\tprivate _eta: number = Infinity\n\n\t/**\n\t * Cutoff time for the low pass filter of the ETA.\n\t * A higher value will consider more history information for calculation,\n\t * and thus suppress spikes of the speed,\n\t * but will make the overall resposiveness slower.\n\t */\n\tprivate _cutoffTime = 2.5\n\n\tpublic constructor(options: EtaOptions = {}) {\n\t\tsuper()\n\t\tif (options.start) {\n\t\t\tthis.resume()\n\t\t}\n\t\tif (options.total) {\n\t\t\tthis.update(0, options.total)\n\t\t}\n\t\tthis._cutoffTime = options.cutoffTime ?? 2.5\n\t}\n\n\t/**\n\t * Add more transferred bytes.\n\t * @param done Additional bytes done.\n\t */\n\tpublic add(done: number): void {\n\t\tthis.update(this._done + done)\n\t}\n\n\t/**\n\t * Update the transmission state.\n\t *\n\t * @param done The new value of transferred bytes.\n\t * @param total Optionally also update the total bytes we expect.\n\t */\n\tpublic update(done: number, total?: number): void {\n\t\tif (this.status !== EtaStatus.Running) {\n\t\t\treturn\n\t\t}\n\t\tif (total && total > 0) {\n\t\t\tthis._total = total\n\t\t}\n\n\t\tconst deltaDone = done - this._done\n\t\tconst deltaTime = (Date.now() - this._startTime) / 1000\n\n\t\tthis._startTime = Date.now()\n\t\tthis._elapsedTime += deltaTime\n\t\tthis._done = done\n\t\tthis._progress = this._done / this._total\n\n\t\t// Only update speed when the history is large enough so we can estimate it\n\t\tconst historyNeeded = this._cutoffTime + deltaTime\n\t\tif (this._elapsedTime > historyNeeded) {\n\t\t\t// Filter the done bytes using a low pass filter to suppress speed spikes\n\t\t\tconst alpha = deltaTime / (deltaTime + (1 / this._cutoffTime))\n\t\t\tconst filtered = (this._done - deltaDone) + (1 - alpha) * deltaDone\n\t\t\t// bytes per second - filtered\n\t\t\tthis._speed = Math.round(filtered / this._elapsedTime)\n\t\t} else if (this._speed === -1 && this._elapsedTime > deltaTime) {\n\t\t\t// special case when uploading with high speed\n\t\t\t// it could be that the upload is finished before we reach the curoff time\n\t\t\t// so we already give an estimation\n\t\t\tconst remaining = this._total - done\n\t\t\tconst eta = remaining / (done / this._elapsedTime)\n\t\t\t// Only set the ETA when we either already set it for a previous update\n\t\t\t// or when the special case happened that we are in fast upload and we only got a couple of seconds for the whole upload\n\t\t\t// meaning we are below 2x the cutoff time.\n\t\t\tif (this._eta !== Infinity || eta <= 2 * this._cutoffTime) {\n\t\t\t\t// We only take a couple of seconds so we set the eta to the current ETA using current speed.\n\t\t\t\t// But we do not set the speed because we do not want to trigger the real ETA calculation below\n\t\t\t\t// and especially because the speed would be very spiky (we still have no filters in place).\n\t\t\t\tthis._eta = eta\n\t\t\t}\n\t\t}\n\n\t\t// Update the eta if we have valid speed information (prevent divide by zero)\n\t\tif (this._speed > 0) {\n\t\t\t// Estimate transfer of remaining bytes with current average speed\n\t\t\tthis._eta = Math.round((this._total - this._done) / this._speed)\n\t\t}\n\n\t\tthis.dispatchTypedEvent('update', new CustomEvent('update', { cancelable: false }))\n\t}\n\n\tpublic reset(): void {\n\t\tthis._done = 0\n\t\tthis._total = 0\n\t\tthis._progress = 0\n\t\tthis._elapsedTime = 0\n\t\tthis._eta = Infinity\n\t\tthis._speed = -1\n\t\tthis._startTime = -1\n\t\tthis._status = EtaStatus.Idle\n\t\tthis.dispatchTypedEvent('reset', new CustomEvent('reset'))\n\t}\n\n\t/**\n\t * Pause the ETA calculation.\n\t */\n\tpublic pause(): void {\n\t\tif (this._status === EtaStatus.Running) {\n\t\t\tthis._status = EtaStatus.Paused\n\t\t\tthis._elapsedTime += (Date.now() - this._startTime) / 1000\n\t\t\tthis.dispatchTypedEvent('pause', new CustomEvent('pause'))\n\t\t}\n\t}\n\n\t/**\n\t * Resume the ETA calculation.\n\t */\n\tpublic resume(): void {\n\t\tif (this._status !== EtaStatus.Running) {\n\t\t\tthis._startTime = Date.now()\n\t\t\tthis._status = EtaStatus.Running\n\t\t\tthis.dispatchTypedEvent('resume', new CustomEvent('resume'))\n\t\t}\n\t}\n\n\t/**\n\t * Status of the Eta (paused, active, idle).\n\t */\n\tpublic get status(): EtaStatus {\n\t\treturn this._status\n\t}\n\n\t/**\n\t * Progress (percent done)\n\t */\n\tpublic get progress(): number {\n\t\treturn Math.round(this._progress * 10000) / 100\n\t}\n\n\t/**\n\t * Estimated time in seconds.\n\t */\n\tpublic get time(): number {\n\t\treturn this._eta\n\t}\n\n\t/**\n\t * Human readable version of the estimated time.\n\t */\n\tpublic get timeReadable(): string {\n\t\tif (this._eta === Infinity) {\n\t\t\treturn t('estimating time left')\n\t\t} else if (this._eta < 10) {\n\t\t\treturn t('a few seconds left')\n\t\t} else if (this._eta < 60) {\n\t\t\treturn n('{seconds} seconds left', '{seconds} seconds left', this._eta, { seconds: this._eta })\n\t\t}\n\n\t\tconst hours = String(Math.floor(this._eta / 3600)).padStart(2, '0')\n\t\tconst minutes = String(Math.floor((this._eta % 3600) / 60)).padStart(2, '0')\n\t\tconst seconds = String(this._eta % 60).padStart(2, '0')\n\t\treturn t('{time} left', { time: `${hours}:${minutes}:${seconds}` }) // TRANSLATORS time has the format 00:00:00\n\t}\n\n\t/**\n\t * Transfer speed in bytes per second.\n\t * Returns `-1` if not yet estimated.\n\t */\n\tpublic get speed(): number {\n\t\treturn this._speed\n\t}\n\n\t/**\n\t * Get the speed in human readable format using file sizes like 10KB/s.\n\t * Returns the empty string if not yet estimated.\n\t */\n\tpublic get speedReadable(): string {\n\t\treturn this._speed > 0\n\t\t\t? `${formatFileSize(this._speed, true)}∕s`\n\t\t\t: ''\n\t}\n\n}\n","/**\n * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\nimport type { AxiosError, AxiosResponse } from 'axios'\nimport type { WebDAVClient } from 'webdav'\nimport type { IDirectory } from '../utils/fileTree.ts'\n\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { FileType, Folder, Permission, davGetClient, davRemoteURL, davRootPath } from '@nextcloud/files'\nimport { encodePath } from '@nextcloud/paths'\nimport { normalize } from 'path'\nimport { getCapabilities } from '@nextcloud/capabilities'\n\nimport axios, { isCancel } from '@nextcloud/axios'\nimport PCancelable from 'p-cancelable'\nimport PQueue from 'p-queue'\n\nimport { UploadCancelledError } from '../errors/UploadCancelledError.ts'\nimport { getChunk, initChunkWorkspace, uploadData } from '../utils/upload.ts'\nimport { getMaxChunksSize } from '../utils/config.ts'\nimport { Status as UploadStatus, Upload } from '../upload.ts'\nimport { isFileSystemFileEntry } from '../utils/filesystem.ts'\nimport { Directory } from '../utils/fileTree.ts'\nimport { t } from '../utils/l10n.ts'\nimport logger from '../utils/logger.ts'\nimport { Eta } from './eta.ts'\n\nexport enum UploaderStatus {\n\tIDLE = 0,\n\tUPLOADING = 1,\n\tPAUSED = 2\n}\n\nexport class Uploader {\n\n\t// Initialized via setter in the constructor\n\tprivate _destinationFolder!: Folder\n\tprivate _isPublic: boolean\n\tprivate _customHeaders: Record<string, string>\n\n\t// Global upload queue\n\tprivate _uploadQueue: Array<Upload> = []\n\tprivate _jobQueue: PQueue = new PQueue({\n\t\t// Maximum number of concurrent uploads\n\t\t// @ts-expect-error TS2339 Object has no defined properties\n\t\tconcurrency: getCapabilities().files?.chunked_upload?.max_parallel_count ?? 5,\n\t})\n\n\tprivate _queueSize = 0\n\tprivate _queueProgress = 0\n\tprivate _queueStatus: UploaderStatus = UploaderStatus.IDLE\n\n\tprivate _eta = new Eta()\n\n\tprivate _notifiers: Array<(upload: Upload) => void> = []\n\n\t/**\n\t * Initialize uploader\n\t *\n\t * @param {boolean} isPublic are we in public mode ?\n\t * @param {Folder} destinationFolder the context folder to operate, relative to the root folder\n\t */\n\tconstructor(\n\t\tisPublic = false,\n\t\tdestinationFolder?: Folder,\n\t) {\n\t\tthis._isPublic = isPublic\n\t\tthis._customHeaders = {}\n\n\t\tif (!destinationFolder) {\n\t\t\tconst source = `${davRemoteURL}${davRootPath}`\n\t\t\tlet owner: string\n\n\t\t\tif (isPublic) {\n\t\t\t\towner = 'anonymous'\n\t\t\t} else {\n\t\t\t\tconst user = getCurrentUser()?.uid\n\t\t\t\tif (!user) {\n\t\t\t\t\tthrow new Error('User is not logged in')\n\t\t\t\t}\n\t\t\t\towner = user\n\t\t\t}\n\n\t\t\tdestinationFolder = new Folder({\n\t\t\t\tid: 0,\n\t\t\t\towner,\n\t\t\t\tpermissions: Permission.ALL,\n\t\t\t\troot: davRootPath,\n\t\t\t\tsource,\n\t\t\t})\n\t\t}\n\t\tthis.destination = destinationFolder\n\n\t\tlogger.debug('Upload workspace initialized', {\n\t\t\tdestination: this.destination,\n\t\t\troot: this.root,\n\t\t\tisPublic,\n\t\t\tmaxChunksSize: getMaxChunksSize(),\n\t\t})\n\t}\n\n\t/**\n\t * Get the upload destination path relative to the root folder\n\t */\n\tget destination(): Folder {\n\t\treturn this._destinationFolder\n\t}\n\n\t/**\n\t * Set the upload destination path relative to the root folder\n\t */\n\tset destination(folder: Folder) {\n\t\tif (!folder || folder.type !== FileType.Folder || !folder.source) {\n\t\t\tthrow new Error('Invalid destination folder')\n\t\t}\n\n\t\tlogger.debug('Destination set', { folder })\n\t\tthis._destinationFolder = folder\n\t}\n\n\t/**\n\t * Get the root folder\n\t */\n\tget root() {\n\t\treturn this._destinationFolder.source\n\t}\n\n\t/**\n\t * Get registered custom headers for uploads\n\t */\n\tget customHeaders(): Record<string, string> {\n\t\treturn structuredClone(this._customHeaders)\n\t}\n\n\t/**\n\t * Set a custom header\n\t * @param name The header to set\n\t * @param value The string value\n\t */\n\tsetCustomHeader(name: string, value: string = ''): void {\n\t\tthis._customHeaders[name] = value\n\t}\n\n\t/**\n\t * Unset a custom header\n\t * @param name The header to unset\n\t */\n\tdeleteCustomerHeader(name: string): void {\n\t\tdelete this._customHeaders[name]\n\t}\n\n\t/**\n\t * Get the upload queue\n\t */\n\tget queue(): Upload[] {\n\t\treturn this._uploadQueue\n\t}\n\n\tprivate reset() {\n\t\t// Reset the ETA\n\t\tthis._eta.reset()\n\t\t// If there is no upload in the queue and no job in the queue\n\t\tif (this._uploadQueue.length === 0 && this._jobQueue.size === 0) {\n\t\t\treturn\n\t\t}\n\n\t\t// Reset upload queue but keep the reference\n\t\tthis._uploadQueue.splice(0, this._uploadQueue.length)\n\t\tthis._jobQueue.clear()\n\t\tthis._queueSize = 0\n\t\tthis._queueProgress = 0\n\t\tthis._queueStatus = UploaderStatus.IDLE\n\t\tlogger.debug('Uploader state reset')\n\t}\n\n\t/**\n\t * Pause any ongoing upload(s)\n\t */\n\tpublic pause() {\n\t\tthis._eta.pause()\n\t\tthis._jobQueue.pause()\n\t\tthis._queueStatus = UploaderStatus.PAUSED\n\t\tthis.updateStats()\n\t\tlogger.debug('Uploader paused')\n\t}\n\n\t/**\n\t * Resume any pending upload(s)\n\t */\n\tpublic start() {\n\t\tthis._eta.resume()\n\t\tthis._jobQueue.start()\n\t\tthis._queueStatus = UploaderStatus.UPLOADING\n\t\tthis.updateStats()\n\t\tlogger.debug('Uploader resumed')\n\t}\n\n\t/**\n\t * Get the estimation for the uploading time.\n\t */\n\tget eta(): Eta {\n\t\treturn this._eta\n\t}\n\n\t/**\n\t * Get the upload queue stats\n\t */\n\tget info() {\n\t\treturn {\n\t\t\tsize: this._queueSize,\n\t\t\tprogress: this._queueProgress,\n\t\t\tstatus: this._queueStatus,\n\t\t}\n\t}\n\n\tprivate updateStats() {\n\t\tconst size = this._uploadQueue.map(upload => upload.size)\n\t\t\t.reduce((partialSum, a) => partialSum + a, 0)\n\t\tconst uploaded = this._uploadQueue.map(upload => upload.uploaded)\n\t\t\t.reduce((partialSum, a) => partialSum + a, 0)\n\n\t\tthis._eta.update(uploaded, size)\n\t\tthis._queueSize = size\n\t\tthis._queueProgress = uploaded\n\n\t\t// If already paused keep it that way\n\t\tif (this._queueStatus !== UploaderStatus.PAUSED) {\n\t\t\tconst pending = this._uploadQueue.find(({ status }) => [UploadStatus.INITIALIZED, UploadStatus.UPLOADING, UploadStatus.ASSEMBLING].includes(status))\n\t\t\tif (this._jobQueue.size > 0 || pending) {\n\t\t\t\tthis._queueStatus = UploaderStatus.UPLOADING\n\t\t\t} else {\n\t\t\t\tthis.eta.reset()\n\t\t\t\tthis._queueStatus = UploaderStatus.IDLE\n\t\t\t}\n\t\t}\n\t}\n\n\taddNotifier(notifier: (upload: Upload) => void) {\n\t\tthis._notifiers.push(notifier)\n\t}\n\n\t/**\n\t * Notify listeners of the upload completion\n\t * @param upload The upload that finished\n\t */\n\tprivate _notifyAll(upload: Upload): void {\n\t\tfor (const notifier of this._notifiers) {\n\t\t\ttry {\n\t\t\t\tnotifier(upload)\n\t\t\t} catch (error) {\n\t\t\t\tlogger.warn('Error in upload notifier', { error, source: upload.source })\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Uploads multiple files or folders while preserving the relative path (if available)\n\t * @param {string} destination The destination path relative to the root folder. e.g. /foo/bar (a file \"a.txt\" will be uploaded then to \"/foo/bar/a.txt\")\n\t * @param {Array<File|FileSystemEntry>} files The files and/or folders to upload\n\t * @param {Function} callback Callback that receives the nodes in the current folder and the current path to allow resolving conflicts, all nodes that are returned will be uploaded (if a folder does not exist it will be created)\n\t * @return Cancelable promise that resolves to an array of uploads\n\t *\n\t * @example\n\t * ```ts\n\t * // For example this is from handling the onchange event of an input[type=file]\n\t * async handleFiles(files: File[]) {\n\t * this.uploads = await this.uploader.batchUpload('uploads', files, this.handleConflicts)\n\t * }\n\t *\n\t * async handleConflicts(nodes: File[], currentPath: string) {\n\t * const conflicts = getConflicts(nodes, this.fetchContent(currentPath))\n\t * if (conflicts.length === 0) {\n\t * // No conflicts so upload all\n\t * return nodes\n\t * } else {\n\t * // Open the conflict picker to resolve conflicts\n\t * try {\n\t * const { selected, renamed } = await openConflictPicker(currentPath, conflicts, this.fetchContent(currentPath), { recursive: true })\n\t * return [...selected, ...renamed]\n\t * } catch (e) {\n\t * return false\n\t * }\n\t * }\n\t * }\n\t * ```\n\t */\n\tbatchUpload(\n\t\tdestination: string,\n\t\tfiles: (File|FileSystemEntry)[],\n\t\tcallback?: (nodes: Array<File|IDirectory>, currentPath: string) => Promise<Array<File|IDirectory>|false>,\n\t): PCancelable<Upload[]> {\n\t\tif (!callback) {\n\t\t\tcallback = async (files: Array<File|Directory>) => files\n\t\t}\n\n\t\treturn new PCancelable(async (resolve, reject, onCancel) => {\n\t\t\tconst rootFolder = new Directory('')\n\t\t\tawait rootFolder.addChildren(files)\n\t\t\t// create a meta upload to ensure all ongoing child requests are listed\n\t\t\tconst target = `${this.root.replace(/\\/$/, '')}/${destination.replace(/^\\//, '')}`\n\t\t\tconst upload = new Upload(target, false, 0, rootFolder)\n\t\t\tupload.status = UploadStatus.UPLOADING\n\t\t\tthis._uploadQueue.push(upload)\n\n\t\t\tlogger.debug('Starting new batch upload', { target })\n\t\t\ttry {\n\t\t\t\t// setup client with root and custom header\n\t\t\t\tconst client = davGetClient(this.root, this._customHeaders)\n\t\t\t\t// Create the promise for the virtual root directory\n\t\t\t\tconst promise = this.uploadDirectory(destination, rootFolder, callback, client)\n\t\t\t\t// Make sure to cancel it when requested\n\t\t\t\tonCancel(() => promise.cancel())\n\t\t\t\t// await the uploads and resolve with \"finished\" status\n\t\t\t\tconst uploads = await promise\n\t\t\t\tupload.status = UploadStatus.FINISHED\n\t\t\t\tresolve(uploads)\n\t\t\t} catch (error) {\n\t\t\t\tif (isCancel(error) || error instanceof UploadCancelledError) {\n\t\t\t\t\tlogger.info('Upload cancelled by user', { error })\n\t\t\t\t\tupload.status = UploadStatus.CANCELLED\n\t\t\t\t\treject(new UploadCancelledError(error))\n\t\t\t\t} else {\n\t\t\t\t\tlogger.error('Error in batch upload', { error })\n\t\t\t\t\tupload.status = UploadStatus.FAILED\n\t\t\t\t\treject(error)\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\t// Upload queue is cleared when all the uploading jobs are done\n\t\t\t\t// Meta upload unlike real uploading does not create a job\n\t\t\t\t// Removing it manually here to make sure it is remove even when no uploading happened and there was nothing to finish\n\t\t\t\tthis._uploadQueue.splice(this._uploadQueue.indexOf(upload), 1)\n\t\t\t\tthis._notifyAll(upload)\n\t\t\t\tthis.updateStats()\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Helper to create a directory wrapped inside an Upload class\n\t * @param destination Destination where to create the directory\n\t * @param directory The directory to create\n\t * @param client The cached WebDAV client\n\t */\n\tprivate createDirectory(destination: string, directory: Directory, client: WebDAVClient): PCancelable<Upload> {\n\t\tconst folderPath = normalize(`${destination}/${directory.name}`).replace(/\\/$/, '')\n\t\tconst rootPath = `${this.root.replace(/\\/$/, '')}/${folderPath.replace(/^\\//, '')}`\n\n\t\tif (!directory.name) {\n\t\t\tthrow new Error('Can not create empty directory')\n\t\t}\n\n\t\t// Add a new upload to the upload queue\n\t\tconst currentUpload: Upload = new Upload(rootPath, false, 0, directory)\n\t\tthis._uploadQueue.push(currentUpload)\n\n\t\t// Return the cancelable promise\n\t\treturn new PCancelable(async (resolve, reject, onCancel) => {\n\t\t\tconst abort = new AbortController()\n\t\t\tonCancel(() => abort.abort())\n\t\t\tcurrentUpload.signal.addEventListener('abort', () => reject(t('Upload has been cancelled')))\n\n\t\t\t// Add the request to the job queue -> wait for finish to resolve the promise\n\t\t\tawait this._jobQueue.add(async () => {\n\t\t\t\tcurrentUpload.status = UploadStatus.UPLOADING\n\t\t\t\ttry {\n\t\t\t\t\tawait client.createDirectory(folderPath, { signal: abort.signal })\n\t\t\t\t\tresolve(currentUpload)\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (isCancel(error) || error instanceof UploadCancelledError) {\n\t\t\t\t\t\tcurrentUpload.status = UploadStatus.CANCELLED\n\t\t\t\t\t\treject(new UploadCancelledError(error))\n\t\t\t\t\t} else if (error && typeof error === 'object' && 'status' in error && error.status === 405) {\n\t\t\t\t\t\t// Directory already exists, so just write into it and ignore the error\n\t\t\t\t\t\tlogger.debug('Directory already exists, writing into it', { directory: directory.name })\n\t\t\t\t\t\tcurrentUpload.status = UploadStatus.FINISHED\n\t\t\t\t\t\tresolve(currentUpload)\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Another error happened, so abort uploading the directory\n\t\t\t\t\t\tcurrentUpload.status = UploadStatus.FAILED\n\t\t\t\t\t\treject(error)\n\t\t\t\t\t}\n\t\t\t\t} finally {\n\t\t\t\t\t// Update statistics\n\t\t\t\t\tthis._notifyAll(currentUpload)\n\t\t\t\t\tthis.updateStats()\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n\n\t// Helper for uploading directories (recursively)\n\tprivate uploadDirectory(\n\t\tdestination: string,\n\t\tdirectory: Directory,\n\t\tcallback: (nodes: Array<File|Directory>, currentPath: string) => Promise<Array<File|Directory>|false>,\n\t\t// client as parameter to cache it for performance\n\t\tclient: WebDAVClient,\n\t): PCancelable<Upload[]> {\n\t\tconst folderPath = normalize(`${destination}/${directory.name}`).replace(/\\/$/, '')\n\n\t\treturn new PCancelable(async (resolve, reject, onCancel) => {\n\t\t\tconst abort = new AbortController()\n\t\t\tonCancel(() => abort.abort())\n\n\t\t\t// Let the user handle conflicts\n\t\t\tconst selectedForUpload = await callback(directory.children, folderPath)\n\t\t\tif (selectedForUpload === false) {\n\t\t\t\tlogger.debug('Upload canceled by user', { directory })\n\t\t\t\treject(new UploadCancelledError('Conflict resolution cancelled by user'))\n\t\t\t\treturn\n\t\t\t} else if (selectedForUpload.length === 0 && directory.children.length > 0) {\n\t\t\t\tlogger.debug('Skipping directory, as all files were skipped by user', { directory })\n\t\t\t\tresolve([])\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst directories: PCancelable<Upload[]>[] = []\n\t\t\tconst uploads: PCancelable<Upload>[] = []\n\t\t\t// Setup abort controller to cancel all child requests\n\t\t\tabort.signal.addEventListener('abort', () => {\n\t\t\t\tdirectories.forEach((upload) => upload.cancel())\n\t\t\t\tuploads.forEach((upload) => upload.cancel())\n\t\t\t})\n\n\t\t\tlogger.debug('Start directory upload', { directory })\n\t\t\ttry {\n\t\t\t\tif (directory.name) {\n\t\t\t\t\t// If not the virtual root we need to create the directory first before uploading\n\t\t\t\t\t// Make sure the promise is listed in the final result\n\t\t\t\t\tuploads.push(this.createDirectory(destination, directory, client) as PCancelable<Upload>)\n\t\t\t\t\t// Ensure the directory is created before uploading / creating children\n\t\t\t\t\tawait uploads.at(-1)\n\t\t\t\t}\n\n\t\t\t\tfor (const node of selectedForUpload) {\n\t\t\t\t\tif (node instanceof Directory) {\n\t\t\t\t\t\tdirectories.push(this.uploadDirectory(folderPath, node, callback, client))\n\t\t\t\t\t} else {\n\t\t\t\t\t\tuploads.push(this.upload(`${folderPath}/${node.name}`, node))\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst resolvedUploads = await Promise.all(uploads)\n\t\t\t\tconst resolvedDirectoryUploads = await Promise.all(directories)\n\t\t\t\tresolve([resolvedUploads, ...resolvedDirectoryUploads].flat())\n\t\t\t} catch (e) {\n\t\t\t\t// Ensure a failure cancels all other requests\n\t\t\t\tabort.abort(e)\n\t\t\t\treject(e)\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Upload a file to the given path\n\t * @param {string} destination the destination path relative to the root folder. e.g. /foo/bar.txt\n\t * @param {File|FileSystemFileEntry} fileHandle the file to upload\n\t * @param {string} root the root folder to upload to\n\t * @param retries number of retries\n\t */\n\tupload(destination: string, fileHandle: File|FileSystemFileEntry, root?: string, retries: number = 5): PCancelable<Upload> {\n\t\troot = root || this.root\n\t\tconst destinationPath = `${root.replace(/\\/$/, '')}/${destination.replace(/^\\//, '')}`\n\n\t\t// Get the encoded source url to this object for requests purposes\n\t\tconst { origin } = new URL(destinationPath)\n\t\tconst encodedDestinationFile = origin + encodePath(destinationPath.slice(origin.length))\n\n\t\tthis.eta.resume()\n\t\tlogger.debug(`Uploading ${fileHandle.name} to ${encodedDestinationFile}`)\n\n\t\tconst promise = new PCancelable(async (resolve, reject, onCancel): Promise<Upload> => {\n\t\t\t// Handle file system entries by retrieving the file handle\n\t\t\tif (isFileSystemFileEntry(fileHandle)) {\n\t\t\t\tfileHandle = await new Promise((resolve) => (fileHandle as FileSystemFileEntry).file(resolve, reject))\n\t\t\t}\n\t\t\t// We can cast here as we handled system entries in the if above\n\t\t\tconst file = fileHandle as File\n\n\t\t\t// @ts-expect-error TS2339 Object has no defined properties\n\t\t\tconst supportsPublicChunking = getCapabilities().dav?.public_shares_chunking ?? false\n\t\t\tconst maxChunkSize = getMaxChunksSize('size' in file ? file.size : undefined)\n\t\t\t// If manually disabled or if the file is too small\n\t\t\tconst disabledChunkUpload = (this._isPublic && !supportsPublicChunking)\n\t\t\t\t|| maxChunkSize === 0\n\t\t\t\t|| ('size' in file && file.size < maxChunkSize)\n\n\t\t\tconst upload = new Upload(destinationPath, !disabledChunkUpload, file.size, file)\n\t\t\tthis._uploadQueue.push(upload)\n\t\t\tthis.updateStats()\n\n\t\t\t// Register cancellation caller\n\t\t\tonCancel(upload.cancel)\n\n\t\t\tif (!disabledChunkUpload) {\n\t\t\t\tlogger.debug('Initializing chunked upload', { file, upload })\n\n\t\t\t\t// Let's initialize a chunk upload\n\t\t\t\tconst tempUrl = await initChunkWorkspace(encodedDestinationFile, retries, this._isPublic, this._customHeaders)\n\t\t\t\tconst chunksQueue: Array<Promise<void>> = []\n\n\t\t\t\t// Generate chunks array\n\t\t\t\tfor (let chunk = 0; chunk < upload.chunks; chunk++) {\n\t\t\t\t\tconst bufferStart = chunk * maxChunkSize\n\t\t\t\t\t// Don't go further than the file size\n\t\t\t\t\tconst bufferEnd = Math.min(bufferStart + maxChunkSize, upload.size)\n\t\t\t\t\t// Make it a Promise function for better memory management\n\t\t\t\t\tconst blob = () => getChunk(file, bufferStart, maxChunkSize)\n\n\t\t\t\t\t// Init request queue\n\t\t\t\t\tconst request = () => {\n\t\t\t\t\t\t// bytes uploaded on this chunk (as upload.uploaded tracks all chunks)\n\t\t\t\t\t\tlet chunkBytes = 0\n\t\t\t\t\t\treturn uploadData(\n\t\t\t\t\t\t\t`${tempUrl}/${chunk + 1}`,\n\t\t\t\t\t\t\tblob,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tsignal: upload.signal,\n\t\t\t\t\t\t\t\tdestinationFile: encodedDestinationFile,\n\t\t\t\t\t\t\t\tretries,\n\t\t\t\t\t\t\t\tonUploadProgress: ({ bytes }) => {\n\t\t\t\t\t\t\t\t\t// Only count 90% of bytes as the request is not yet processed by server\n\t\t\t\t\t\t\t\t\t// we set the remaining 10% when the request finished (server responded).\n\t\t\t\t\t\t\t\t\tconst progressBytes = bytes * 0.9\n\t\t\t\t\t\t\t\t\tchunkBytes += progressBytes\n\t\t\t\t\t\t\t\t\tupload.uploaded += progressBytes\n\t\t\t\t\t\t\t\t\tthis.updateStats()\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonUploadRetry: () => {\n\t\t\t\t\t\t\t\t\t// Current try failed, so reset the stats for this chunk\n\t\t\t\t\t\t\t\t\t// meaning remove the uploaded chunk bytes from stats\n\t\t\t\t\t\t\t\t\tupload.uploaded -= chunkBytes\n\t\t\t\t\t\t\t\t\tchunkBytes = 0\n\t\t\t\t\t\t\t\t\tthis.updateStats()\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\t...this._customHeaders,\n\t\t\t\t\t\t\t\t\t...this._mtimeHeader(file),\n\t\t\t\t\t\t\t\t\t'OC-Total-Length': file.size,\n\t\t\t\t\t\t\t\t\t'Content-Type': 'application/octet-stream',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\t// Update upload progress on chunk completion\n\t\t\t\t\t\t\t.then(() => {\n\t\t\t\t\t\t\t\t// request fully done so we uploaded the full chunk\n\t\t\t\t\t\t\t\t// we first remove the intermediate chunkBytes from progress events\n\t\t\t\t\t\t\t\t// and then add the real full size\n\t\t\t\t\t\t\t\tupload.uploaded += bufferEnd - bufferStart - chunkBytes\n\t\t\t\t\t\t\t\tthis.updateStats()\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t.catch((error) => {\n\t\t\t\t\t\t\t\tif (error?.response?.status === 507) {\n\t\t\t\t\t\t\t\t\tlogger.error('Upload failed, not enough space on the server or quota exceeded. Cancelling the remaining chunks', { error, upload })\n\t\t\t\t\t\t\t\t\tupload.cancel()\n\t\t\t\t\t\t\t\t\tupload.status = UploadStatus.FAILED\n\t\t\t\t\t\t\t\t\tthrow error\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif (!isCancel(error)) {\n\t\t\t\t\t\t\t\t\tlogger.error(`Chunk ${chunk + 1} ${bufferStart} - ${bufferEnd} uploading failed`, { error, upload })\n\t\t\t\t\t\t\t\t\tupload.cancel()\n\t\t\t\t\t\t\t\t\tupload.status = UploadStatus.FAILED\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tthrow error\n\t\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tchunksQueue.push(this._jobQueue.add(request))\n\t\t\t\t}\n\n\t\t\t\tconst request = async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Once all chunks are sent, assemble the final file\n\t\t\t\t\t\tawait Promise.all(chunksQueue)\n\n\t\t\t\t\t\t// Assemble the chunks\n\t\t\t\t\t\tupload.status = UploadStatus.ASSEMBLING\n\t\t\t\t\t\tthis.updateStats()\n\n\t\t\t\t\t\t// Send the assemble request\n\t\t\t\t\t\tupload.response = await axios.request({\n\t\t\t\t\t\t\tmethod: 'MOVE',\n\t\t\t\t\t\t\turl: `${tempUrl}/.file`,\n\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t...this._customHeaders,\n\t\t\t\t\t\t\t\t...this._mtimeHeader(file),\n\t\t\t\t\t\t\t\t'OC-Total-Length': file.size,\n\t\t\t\t\t\t\t\tDestination: encodedDestinationFile,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})\n\t\t\t\t\t\tupload.status = UploadStatus.FINISHED\n\t\t\t\t\t\tthis.updateStats()\n\n\t\t\t\t\t\tlogger.debug(`Successfully uploaded ${file.name}`, { file, upload })\n\t\t\t\t\t\tresolve(upload)\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tif (isCancel(error) || error instanceof UploadCancelledError) {\n\t\t\t\t\t\t\tupload.status = UploadStatus.CANCELLED\n\t\t\t\t\t\t\treject(new UploadCancelledError(error))\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tupload.status = UploadStatus.FAILED\n\t\t\t\t\t\t\treject(t('Failed to assemble the chunks together'))\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Cleaning up temp directory\n\t\t\t\t\t\taxios.request({\n\t\t\t\t\t\t\tmethod: 'DELETE',\n\t\t\t\t\t\t\turl: `${tempUrl}`,\n\t\t\t\t\t\t})\n\t\t\t\t\t} finally {\n\t\t\t\t\t\t// Notify listeners of the upload completion\n\t\t\t\t\t\tthis._notifyAll(upload)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis._jobQueue.add(request)\n\t\t\t} else {\n\t\t\t\tlogger.debug('Initializing regular upload', { file, upload })\n\n\t\t\t\t// Generating upload limit\n\t\t\t\tconst blob = await getChunk(file, 0, upload.size)\n\t\t\t\tconst request = async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tupload.response = await uploadData(\n\t\t\t\t\t\t\tencodedDestinationFile,\n\t\t\t\t\t\t\tblob,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tsignal: upload.signal,\n\t\t\t\t\t\t\t\tonUploadProgress: ({ bytes }) => {\n\t\t\t\t\t\t\t\t\t// As this is only the sent bytes not the processed ones we only count 90%.\n\t\t\t\t\t\t\t\t\t// When the upload is finished (server acknowledged the upload) the remaining 10% will be correctly set.\n\t\t\t\t\t\t\t\t\tupload.uploaded += bytes * 0.9\n\t\t\t\t\t\t\t\t\tthis.updateStats()\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonUploadRetry: () => {\n\t\t\t\t\t\t\t\t\tupload.uploaded = 0\n\t\t\t\t\t\t\t\t\tthis.updateStats()\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\t...this._customHeaders,\n\t\t\t\t\t\t\t\t\t...this._mtimeHeader(file),\n\t\t\t\t\t\t\t\t\t'Content-Type': file.type,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\n\t\t\t\t\t\t// Update progress - now we set the uploaded size to 100% of the file size\n\t\t\t\t\t\tupload.uploaded = upload.size\n\t\t\t\t\t\tthis.updateStats()\n\n\t\t\t\t\t\t// Resolve\n\t\t\t\t\t\tlogger.debug(`Successfully uploaded ${file.name}`, { file, upload })\n\t\t\t\t\t\tresolve(upload)\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tif (isCancel(error) || error instanceof UploadCancelledError) {\n\t\t\t\t\t\t\tupload.status = UploadStatus.CANCELLED\n\t\t\t\t\t\t\treject(new UploadCancelledError(error))\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Attach response to the upload object\n\t\t\t\t\t\tif ((error as AxiosError)?.response) {\n\t\t\t\t\t\t\tupload.response = (error as AxiosError).response as AxiosResponse\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tupload.status = UploadStatus.FAILED\n\t\t\t\t\t\tlogger.error(`Failed uploading ${file.name}`, { error, file, upload })\n\t\t\t\t\t\treject(t('Failed to upload the file'))\n\t\t\t\t\t}\n\n\t\t\t\t\t// Notify listeners of the upload completion\n\t\t\t\t\tthis._notifyAll(upload)\n\t\t\t\t}\n\t\t\t\tthis._jobQueue.add(request)\n\t\t\t\tthis.updateStats()\n\t\t\t}\n\n\t\t\t// Reset when upload queue is done\n\t\t\t// Only when we know we're closing on the last chunks\n\t\t\t// and/or assembling we can reset the uploader.\n\t\t\t// Otherwise he queue might be idle for a short time\n\t\t\t// and clear the Upload queue before we're done.\n\t\t\tthis._jobQueue.onIdle()\n\t\t\t\t.then(() => this.reset())\n\n\t\t\t// Finally return the Upload\n\t\t\treturn upload\n\t\t}) as PCancelable<Upload>\n\n\t\treturn promise\n\t}\n\n\t/**\n\t * Create modification time headers if valid value is available.\n\t * It can be invalid on Android devices if SD cards with NTFS / FAT are used,\n\t * as those files might use the NT epoch for time so the value will be negative.\n\t *\n\t * @param file The file to upload\n\t */\n\tprivate _mtimeHeader(file: File): { 'X-OC-Mtime'?: number } {\n\t\tconst mtime = Math.floor(file.lastModified / 1000)\n\t\tif (mtime > 0) {\n\t\t\treturn { 'X-OC-Mtime': mtime }\n\t\t}\n\t\treturn {}\n\t}\n\n}\n","/*!\n * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors\n * SPDX-License-Identifier: AGPL-3.0-or-later\n */\n\nimport { isPublicShare } from '@nextcloud/sharing/public'\n\nimport { Uploader } from './uploader/uploader.ts'\n\n/**\n * Get the global Uploader instance.\n *\n * Note: If you need a local uploader you can just create a new instance,\n * this global instance will be shared with other apps.\n *\n * @param isPublic Set to true to use public upload endpoint (by default it is auto detected)\n * @param forceRecreate Force a new uploader instance - main purpose is for testing\n */\nexport function getUploader(isPublic: boolean = isPublicShare(), forceRecreate = false): Uploader {\n\tif (fo