Skip to content
2 changes: 1 addition & 1 deletion __tests__/upload.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { describe, expect, test, vi } from 'vitest'
import { Status, Upload } from '../lib/upload.js'
import { Status, Upload } from '../lib/core/upload.ts'

describe('Constructor checks', () => {
test('Classic upload', () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/uploader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { beforeAll, beforeEach, describe, expect, it, test, vi } from 'vitest'
import { Uploader } from '../lib/uploader'
import { Uploader } from '../lib/core/uploader'
import * as nextcloudAuth from '@nextcloud/auth'
import * as nextcloudFiles from '@nextcloud/files'

Expand Down
2 changes: 1 addition & 1 deletion __tests__/utils/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { describe, expect, test } from 'vitest'
import { getMaxChunksSize } from '../../lib/utils/config.js'
import { getMaxChunksSize } from '../../lib/core/utils/config.ts'

describe('Max chunk size tests', () => {
test('Returning valid config', () => {
Expand Down
6 changes: 3 additions & 3 deletions __tests__/utils/conflicts-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
*/

import { describe, expect, it, vi, beforeEach } from 'vitest'
import { uploadConflictHandler } from '../../lib/utils/conflicts.ts'
import { uploadConflictHandler } from '../../lib/ui/utils/uploadConflictHandler.ts'
import { InvalidFilenameError, InvalidFilenameErrorReason, File as NcFile } from '@nextcloud/files'

const validateFilename = vi.hoisted(() => vi.fn(() => true))
const openConflictPicker = vi.hoisted(() => vi.fn())
const showInvalidFilenameDialog = vi.hoisted(() => vi.fn())

vi.mock('../../lib/index.ts', () => ({ openConflictPicker }))
vi.mock('../../lib/utils/dialog.ts', () => ({ showInvalidFilenameDialog }))
vi.mock('../../lib/ui/openConflictPicker.ts', () => ({ openConflictPicker }))
vi.mock('../../lib/ui/utils/dialog.ts', () => ({ showInvalidFilenameDialog }))
vi.mock('@nextcloud/files', async (getModule) => {
const original = await getModule()
return {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/utils/fileTree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { describe, expect, it } from 'vitest'
import { Directory } from '../../lib/utils/fileTree.ts'
import { Directory } from '../../lib/core/utils/fileTree.ts'

describe('file tree utils', () => {
it('Can create a directory', () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/utils/filesystem.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { beforeAll, describe, expect, test } from 'vitest'
import { isFileSystemDirectoryEntry, isFileSystemEntry, isFileSystemFileEntry } from '../../lib/utils/filesystem'
import { isFileSystemDirectoryEntry, isFileSystemEntry, isFileSystemFileEntry } from '../../lib/shared/utils/filesystem.ts'

describe('File and Directory API helpers', () => {
describe('Without browser support', () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/utils/logger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { expect, test, vi } from 'vitest'
import logger from '../../lib/utils/logger'
import logger from '../../lib/shared/utils/logger.ts'

// Just ensure correct app is set, rest is up to that library to test
test('logger', () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/utils/upload.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { Mock } from 'vitest'
import { describe, expect, test, vi } from 'vitest'
import axios from '@nextcloud/axios'

import { getChunk, initChunkWorkspace, uploadData } from '../../lib/utils/upload.js'
import { getChunk, initChunkWorkspace, uploadData } from '../../lib/core/utils/upload.ts'

const axiosMock: Mock<typeof axios> | typeof axios = axios

Expand Down
2 changes: 1 addition & 1 deletion cypress/components/ConflictPicker.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { File as NcFile } from '@nextcloud/files'
import ConflictPicker from '../../lib/components/ConflictPicker.vue'
import ConflictPicker from '../../lib/ui/components/ConflictPicker.vue'

describe('ConflictPicker rendering', { testIsolation: true }, () => {
let image: File
Expand Down
12 changes: 5 additions & 7 deletions lib/components/UploadPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@
<script lang="ts">
import type { Entry, Node } from '@nextcloud/files'
import type { PropType } from 'vue'
import type { Upload } from '../upload.ts'
import type { Upload } from '../core/core.ts'

import { defineComponent } from 'vue'
import { Folder, NewMenuEntryCategory, getNewFileMenuEntries } from '@nextcloud/files'
Expand All @@ -179,12 +179,10 @@ import IconFolderUpload from 'vue-material-design-icons/FolderUpload.vue'
import IconPlus from 'vue-material-design-icons/Plus.vue'
import IconUpload from 'vue-material-design-icons/Upload.vue'

import { getUploader } from '../index.ts'
import { UploaderStatus } from '../uploader/uploader.ts'
import { Status as UploadStatus } from '../upload.ts'
import { t } from '../utils/l10n.ts'
import { uploadConflictHandler } from '../utils/conflicts.ts'
import logger from '../utils/logger.ts'
import { getUploader, UploadStatus, UploaderStatus } from '../core/core.ts'
import { t } from '../shared/utils/l10n.ts'
import { uploadConflictHandler } from '../ui/utils/uploadConflictHandler.ts'
import logger from '../shared/utils/logger.ts'

export default defineComponent({
name: 'UploadPicker',
Expand Down
6 changes: 6 additions & 0 deletions lib/components/components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

export { default as UploadPicker } from './UploadPicker.vue'
12 changes: 12 additions & 0 deletions lib/core/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

export type { Eta, EtaEventsMap } from './uploader/index.ts'
export type { IDirectory, Directory } from './utils/fileTree.ts'

export { getUploader, upload } from './getUploader.ts'
export { Upload, Status as UploadStatus } from './upload.ts'
export { EtaStatus, Uploader, UploaderStatus } from './uploader/index.ts'
export { getConflicts, hasConflict } from './utils/conflicts.ts'
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { t } from '../utils/l10n.ts'
import { t } from '../../shared/utils/l10n.ts'

export class UploadCancelledError extends Error {

Expand Down
42 changes: 42 additions & 0 deletions lib/core/getUploader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*!
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { isPublicShare } from '@nextcloud/sharing/public'

import { Uploader } from './uploader/index.ts'

/**
* Get the global Uploader instance.
*
* Note: If you need a local uploader you can just create a new instance,
* this global instance will be shared with other apps.
*
* @param isPublic Set to true to use public upload endpoint (by default it is auto detected)
* @param forceRecreate Force a new uploader instance - main purpose is for testing
*/
export function getUploader(isPublic: boolean = isPublicShare(), forceRecreate = false): Uploader {
if (forceRecreate || window._nc_uploader === undefined) {
window._nc_uploader = new Uploader(isPublic)
}

return window._nc_uploader
}

/**
* Upload a file
* This will init an Uploader instance if none exists.
* You will be able to retrieve it with `getUploader`
*
* @param {string} destinationPath the destination path
* @param {File} file the file to upload
* @return {Uploader} the uploader instance
*/
export function upload(destinationPath: string, file: File): Uploader {
// Init uploader and start uploading
const uploader = getUploader()
uploader.upload(destinationPath, file)

return uploader
}
2 changes: 1 addition & 1 deletion lib/upload.ts → lib/core/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { AxiosResponse } from 'axios'
import { getMaxChunksSize } from './utils/config.js'
import { getMaxChunksSize } from './utils/config.ts'

export enum Status {
INITIALIZED = 0,
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion lib/uploader/eta.ts → lib/core/uploader/eta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { TypedEventTarget } from 'typescript-event-target'
import { n, t } from '../utils/l10n.ts'
import { n, t } from '../../shared/utils/l10n.ts'
import { formatFileSize } from '@nextcloud/files'

export enum EtaStatus {
Expand Down
File renamed without changes.
6 changes: 3 additions & 3 deletions lib/uploader/uploader.ts → lib/core/uploader/uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import { UploadCancelledError } from '../errors/UploadCancelledError.ts'
import { getChunk, initChunkWorkspace, uploadData } from '../utils/upload.ts'
import { getMaxChunksSize } from '../utils/config.ts'
import { Status as UploadStatus, Upload } from '../upload.ts'
import { isFileSystemFileEntry } from '../utils/filesystem.ts'
import { isFileSystemFileEntry } from '../../shared/utils/filesystem.ts'
import { Directory } from '../utils/fileTree.ts'
import { t } from '../utils/l10n.ts'
import logger from '../utils/logger.ts'
import { t } from '../../shared/utils/l10n.ts'
import logger from '../../shared/utils/logger.ts'
import { Eta } from './eta.ts'

export enum UploaderStatus {
Expand Down
File renamed without changes.
32 changes: 32 additions & 0 deletions lib/core/utils/conflicts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { Node } from '@nextcloud/files'

/**
* Check if there is a conflict between two sets of files
* @param {Array<File|FileSystemEntry|Node>} files the incoming files
* @param {Node[]} content all the existing files in the directory
* @return {boolean} true if there is a conflict
*/
export function hasConflict(files: (File|FileSystemEntry|Node)[], content: Node[]): boolean {
return getConflicts(files, content).length > 0
}

/**
* Get the conflicts between two sets of files
* @param {Array<File|FileSystemEntry|Node>} files the incoming files
* @param {Node[]} content all the existing files in the directory
* @return {boolean} true if there is a conflict
*/
export function getConflicts<T extends File|FileSystemEntry|Node>(files: T[], content: Node[]): T[] {
const contentNames = content.map((node: Node) => node.basename)
const conflicts = files.filter((node: File|FileSystemEntry|Node) => {
const name = 'basename' in node ? node.basename : node.name
return contentNames.indexOf(name) !== -1
})

return conflicts
}
2 changes: 1 addition & 1 deletion lib/utils/fileTree.ts → lib/core/utils/fileTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { basename } from '@nextcloud/paths'
import { isFileSystemDirectoryEntry, isFileSystemFileEntry } from './filesystem.ts'
import { isFileSystemDirectoryEntry, isFileSystemFileEntry } from '../../shared/utils/filesystem.ts'

/**
* This is a helper class to allow building a file tree for uploading
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/upload.ts → lib/core/utils/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import axios from '@nextcloud/axios'
import axiosRetry, { exponentialDelay, isNetworkOrIdempotentRequestError } from 'axios-retry'
import { getSharingToken } from '@nextcloud/sharing/public'

import logger from './logger'
import logger from '../../shared/utils/logger.ts'

axiosRetry(axios, { retries: 0 })

Expand Down
117 changes: 5 additions & 112 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,118 +2,11 @@
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Node } from '@nextcloud/files'
import type { AsyncComponent } from 'vue'

import { isPublicShare } from '@nextcloud/sharing/public'
import Vue, { defineAsyncComponent } from 'vue'
export type { Directory, IDirectory, Eta, EtaEventsMap } from './core/core.ts'
export { EtaStatus, getConflicts, getUploader, hasConflict, upload, Upload, UploadStatus, Uploader, UploaderStatus } from './core/core.ts'

import { Uploader } from './uploader/uploader'
import UploadPicker from './components/UploadPicker.vue'
export type { ConflictResolutionResult, ConflictPickerOptions } from './ui/ui.ts'
export { openConflictPicker, uploadConflictHandler } from './ui/ui.ts'

export type { IDirectory, Directory } from './utils/fileTree'
export { getConflicts, hasConflict, uploadConflictHandler } from './utils/conflicts'
export { Upload, Status as UploadStatus } from './upload'
export * from './uploader/index.ts'

export type ConflictResolutionResult<T extends File|FileSystemEntry|Node> = {
selected: T[],
renamed: T[],
}

/**
* Get the global Uploader instance.
*
* Note: If you need a local uploader you can just create a new instance,
* this global instance will be shared with other apps.
*
* @param isPublic Set to true to use public upload endpoint (by default it is auto detected)
* @param forceRecreate Force a new uploader instance - main purpose is for testing
*/
export function getUploader(isPublic: boolean = isPublicShare(), forceRecreate = false): Uploader {
if (forceRecreate || window._nc_uploader === undefined) {
window._nc_uploader = new Uploader(isPublic)
}

return window._nc_uploader
}

/**
* Upload a file
* This will init an Uploader instance if none exists.
* You will be able to retrieve it with `getUploader`
*
* @param {string} destinationPath the destination path
* @param {File} file the file to upload
* @return {Uploader} the uploader instance
*/
export function upload(destinationPath: string, file: File): Uploader {
// Init uploader and start uploading
const uploader = getUploader()
uploader.upload(destinationPath, file)

return uploader
}

export interface ConflictPickerOptions {
/**
* When this is set to true a hint is shown that conflicts in directories are handles recursively
* You still need to call this function for each directory separately.
*/
recursive?: boolean
}

/**
* Open the conflict resolver
* @param {string} dirname the directory name
* @param {(File|Node)[]} conflicts the incoming files
* @param {Node[]} content all the existing files in the directory
* @param {ConflictPickerOptions} options Optional settings for the conflict picker
* @return {Promise<ConflictResolutionResult>} the selected and renamed files
*/
export async function openConflictPicker<T extends File|FileSystemEntry|Node>(
dirname: string | undefined,
conflicts: T[],
content: Node[],
options?: ConflictPickerOptions,
): Promise<ConflictResolutionResult<T>> {
const ConflictPicker = defineAsyncComponent(() => import('./components/ConflictPicker.vue')) as AsyncComponent
return new Promise((resolve, reject) => {
const picker = new Vue({
name: 'ConflictPickerRoot',
render: (h) => h(ConflictPicker, {
props: {
dirname,
conflicts,
content,
recursiveUpload: options?.recursive === true,
},
on: {
submit(results: ConflictResolutionResult<T>) {
// Return the results
resolve(results)

// Destroy the component
picker.$destroy()
picker.$el?.parentNode?.removeChild(picker.$el)
},
cancel(error?: Error) {
// Reject the promise
reject(error ?? new Error('Canceled'))

// Destroy the component
picker.$destroy()
picker.$el?.parentNode?.removeChild(picker.$el)
},
},
}),
})

// Mount the component
picker.$mount()
document.body.appendChild(picker.$el)
})
}

/** UploadPicker vue component */
export { UploadPicker }
export { UploadPicker } from './components/components.ts'
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading