Browse Source

chore: add drag and drop recursion and FilesystemAPI testing

Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
pull/44409/head
skjnldsv 4 weeks ago
parent
commit
38c7ca0d4d

+ 124
- 0
__tests__/FileSystemAPIUtils.ts View File

@@ -0,0 +1,124 @@
import { basename } from 'node:path'
import mime from 'mime'

class FileSystemEntry {

private _isFile: boolean
private _fullPath: string

constructor(isFile: boolean, fullPath: string) {
this._isFile = isFile
this._fullPath = fullPath
}

get isFile() {
return !!this._isFile
}

get isDirectory() {
return !this.isFile
}

get name() {
return basename(this._fullPath)
}

}

export class FileSystemFileEntry extends FileSystemEntry {

private _contents: string
private _lastModified: number

constructor(fullPath: string, contents: string, lastModified = Date.now()) {
super(true, fullPath)
this._contents = contents
this._lastModified = lastModified
}

file(success: (file: File) => void) {
const lastModified = this._lastModified
// Faking the mime by using the file extension
const type = mime.getType(this.name) || ''
success(new File([this._contents], this.name, { lastModified, type }))
}

}

export class FileSystemDirectoryEntry extends FileSystemEntry {

private _entries: FileSystemEntry[]

constructor(fullPath: string, entries: FileSystemEntry[]) {
super(false, fullPath)
this._entries = entries || []
}

createReader() {
let read = false
return {
readEntries: (success: (entries: FileSystemEntry[]) => void) => {
if (read) {
return success([])
}
read = true
success(this._entries)
},
}
}

}

/**
* This mocks the File API's File class
* It will allow us to test the Filesystem API as well as the
* File API in the same test suite.
*/
export class DataTransferItem {

private _type: string
private _entry: FileSystemEntry

getAsEntry?: () => FileSystemEntry

constructor(type = '', entry: FileSystemEntry, isFileSystemAPIAvailable = true) {
this._type = type
this._entry = entry

// Only when the Files API is available we are
// able to get the entry
if (isFileSystemAPIAvailable) {
this.getAsEntry = () => this._entry
}
}

get kind() {
return 'file'
}

get type() {
return this._type
}

getAsFile(): File|null {
if (this._entry.isFile && this._entry instanceof FileSystemFileEntry) {
let file: File | null = null
this._entry.file((f) => {
file = f
})
return file
}

// The browser will return an empty File object if the entry is a directory
return new File([], this._entry.name, { type: '' })
}

}

export const fileSystemEntryToDataTransferItem = (entry: FileSystemEntry, isFileSystemAPIAvailable = true): DataTransferItem => {
return new DataTransferItem(
entry.isFile ? 'text/plain' : 'httpd/unix-directory',
entry,
isFileSystemAPIAvailable,
)
}

+ 5
- 2
apps/files/src/components/DragAndDropNotice.vue View File

@@ -180,7 +180,7 @@ export default defineComponent({

// If another button is pressed, cancel it. This
// allows cancelling the drag with the right click.
if (event.button !== 0) {
if (event.button) {
return
}

@@ -192,9 +192,12 @@ export default defineComponent({
// Scroll to last successful upload in current directory if terminated
const lastUpload = uploads.findLast((upload) => upload.status !== UploadStatus.FAILED
&& !upload.file.webkitRelativePath.includes('/')
&& upload.response?.headers?.['oc-fileid'])
&& upload.response?.headers?.['oc-fileid']
// Only use the last ID if it's in the current folder
&& upload.source.replace(folder.source, '').split('/').length === 2)

if (lastUpload !== undefined) {
logger.debug('Scrolling to last upload in current folder', { lastUpload })
this.$router.push({
...this.$route,
params: {

+ 1
- 1
apps/files/src/components/FileEntryMixin.ts View File

@@ -332,7 +332,7 @@ export default defineComponent({

// If another button is pressed, cancel it. This
// allows cancelling the drag with the right click.
if (!this.canDrop || event.button !== 0) {
if (!this.canDrop || event.button) {
return
}


+ 7
- 171
apps/files/src/services/DropService.ts View File

@@ -22,186 +22,21 @@
*/

import type { Upload } from '@nextcloud/upload'
import type { FileStat, ResponseDataDetailed } from 'webdav'
import type { RootDirectory } from './DropServiceUtils'

import { emit } from '@nextcloud/event-bus'
import { Folder, Node, NodeStatus, davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
import { getUploader, hasConflict, openConflictPicker } from '@nextcloud/upload'
import { Folder, Node, NodeStatus, davRootPath } from '@nextcloud/files'
import { getUploader, hasConflict } from '@nextcloud/upload'
import { join } from 'path'
import { joinPaths } from '@nextcloud/paths'
import { showError, showInfo, showSuccess, showWarning } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import Vue from 'vue'

import { Directory, traverseTree, resolveConflict, createDirectoryIfNotExists } from './DropServiceUtils'
import { handleCopyMoveNodeTo } from '../actions/moveOrCopyAction'
import { MoveCopyAction } from '../actions/moveOrCopyActionUtils'
import logger from '../logger.js'

/**
* This represents a Directory in the file tree
* We extend the File class to better handling uploading
* and stay as close as possible as the Filesystem API.
* This also allow us to hijack the size or lastModified
* properties to compute them dynamically.
*/
class Directory extends File {

/* eslint-disable no-use-before-define */
_contents: (Directory|File)[]

constructor(name, contents: (Directory|File)[] = []) {
super([], name, { type: 'httpd/unix-directory' })
this._contents = contents
}

set contents(contents: (Directory|File)[]) {
this._contents = contents
}

get contents(): (Directory|File)[] {
return this._contents
}

get size() {
return this._computeDirectorySize(this)
}

get lastModified() {
if (this._contents.length === 0) {
return Date.now()
}
return this._computeDirectoryMtime(this)
}

/**
* Get the last modification time of a file tree
* This is not perfect, but will get us a pretty good approximation
* @param directory the directory to traverse
*/
_computeDirectoryMtime(directory: Directory): number {
return directory.contents.reduce((acc, file) => {
return file.lastModified > acc
// If the file is a directory, the lastModified will
// also return the results of its _computeDirectoryMtime method
// Fancy recursion, huh?
? file.lastModified
: acc
}, 0)
}

/**
* Get the size of a file tree
* @param directory the directory to traverse
*/
_computeDirectorySize(directory: Directory): number {
return directory.contents.reduce((acc: number, entry: Directory|File) => {
// If the file is a directory, the size will
// also return the results of its _computeDirectorySize method
// Fancy recursion, huh?
return acc + entry.size
}, 0)
}

}

type RootDirectory = Directory & {
name: 'root'
}

/**
* Traverse a file tree using the Filesystem API
* @param entry the entry to traverse
*/
const traverseTree = async (entry: FileSystemEntry): Promise<Directory|File> => {
// Handle file
if (entry.isFile) {
return new Promise<File>((resolve, reject) => {
(entry as FileSystemFileEntry).file(resolve, reject)
})
}

// Handle directory
logger.debug('Handling recursive file tree', { entry: entry.name })
const directory = entry as FileSystemDirectoryEntry
const entries = await readDirectory(directory)
const contents = (await Promise.all(entries.map(traverseTree))).flat()
return new Directory(directory.name, contents)
}

/**
* Read a directory using Filesystem API
* @param directory the directory to read
*/
const readDirectory = (directory: FileSystemDirectoryEntry): Promise<FileSystemEntry[]> => {
const dirReader = directory.createReader()

return new Promise<FileSystemEntry[]>((resolve, reject) => {
const entries = [] as FileSystemEntry[]
const getEntries = () => {
dirReader.readEntries((results) => {
if (results.length) {
entries.push(...results)
getEntries()
} else {
resolve(entries)
}
}, (error) => {
reject(error)
})
}

getEntries()
})
}

const createDirectoryIfNotExists = async (absolutePath: string) => {
const davClient = davGetClient()
const dirExists = await davClient.exists(absolutePath)
if (!dirExists) {
logger.debug('Directory does not exist, creating it', { absolutePath })
await davClient.createDirectory(absolutePath, { recursive: true })
const stat = await davClient.stat(absolutePath, { details: true, data: davGetDefaultPropfind() }) as ResponseDataDetailed<FileStat>
emit('files:node:created', davResultToNode(stat.data))
}
}

const resolveConflict = async <T extends ((Directory|File)|Node)>(files: Array<T>, destination: Folder, contents: Node[]): Promise<T[]> => {
try {
// List all conflicting files
const conflicts = files.filter((file: File|Node) => {
return contents.find((node: Node) => node.basename === (file instanceof File ? file.name : file.basename))
}).filter(Boolean) as (File|Node)[]

// List of incoming files that are NOT in conflict
const uploads = files.filter((file: File|Node) => {
return !conflicts.includes(file)
})

// Let the user choose what to do with the conflicting files
const { selected, renamed } = await openConflictPicker(destination.path, conflicts, contents)

logger.debug('Conflict resolution', { uploads, selected, renamed })

// If the user selected nothing, we cancel the upload
if (selected.length === 0 && renamed.length === 0) {
// User skipped
showInfo(t('files', 'Conflicts resolution skipped'))
logger.info('User skipped the conflict resolution')
return []
}

// Update the list of files to upload
return [...uploads, ...selected, ...renamed] as (typeof files)
} catch (error) {
console.error(error)
// User cancelled
showError(t('files', 'Upload cancelled'))
logger.error('User cancelled the upload')
}

return []
}

/**
* This function converts a list of DataTransferItems to a file tree.
* It uses the Filesystem API if available, otherwise it falls back to the File API.
@@ -225,7 +60,7 @@ export const dataTransferToFileTree = async (items: DataTransferItem[]): Promise
}).map((item) => {
// MDN recommends to try both, as it might be renamed in the future
return (item as unknown as { getAsEntry?: () => FileSystemEntry|undefined })?.getAsEntry?.()
?? item.webkitGetAsEntry()
?? item?.webkitGetAsEntry?.()
?? item
}) as (FileSystemEntry | DataTransferItem)[]

@@ -249,7 +84,8 @@ export const dataTransferToFileTree = async (items: DataTransferItem[]): Promise
// we therefore cannot upload directories recursively.
if (file.type === 'httpd/unix-directory' || !file.type) {
if (!warned) {
showWarning(t('files', 'Your browser does not support the Filesystem API. Directories will not be uploaded.'))
logger.warn('Browser does not support Filesystem API. Directories will not be uploaded')
showWarning(t('files', 'Your browser does not support the Filesystem API. Directories will not be uploaded'))
warned = true
}
continue

+ 142
- 0
apps/files/src/services/DropServiceUtils.spec.ts View File

@@ -0,0 +1,142 @@
import { describe, it, expect } from '@jest/globals'

import { FileSystemDirectoryEntry, FileSystemFileEntry, fileSystemEntryToDataTransferItem, DataTransferItem as DataTransferItemMock } from '../../../../__tests__/FileSystemAPIUtils'
import { join } from 'node:path'
import { Directory, traverseTree } from './DropServiceUtils'
import { dataTransferToFileTree } from './DropService'
import logger from '../logger'

const dataTree = {
'file0.txt': ['Hello, world!', 1234567890],
dir1: {
'file1.txt': ['Hello, world!', 4567891230],
'file2.txt': ['Hello, world!', 7891234560],
},
dir2: {
'file3.txt': ['Hello, world!', 1234567890],
},
}

// This is mocking a file tree using the FileSystem API
const buildFileSystemDirectoryEntry = (path: string, tree: any): FileSystemDirectoryEntry => {
const entries = Object.entries(tree).map(([name, contents]) => {
const fullPath = join(path, name)
if (Array.isArray(contents)) {
return new FileSystemFileEntry(fullPath, contents[0], contents[1])
} else {
return buildFileSystemDirectoryEntry(fullPath, contents)
}
})
return new FileSystemDirectoryEntry(path, entries)
}

const buildDataTransferItemArray = (path: string, tree: any, isFileSystemAPIAvailable = true): DataTransferItemMock[] => {
return Object.entries(tree).map(([name, contents]) => {
const fullPath = join(path, name)
if (Array.isArray(contents)) {
const entry = new FileSystemFileEntry(fullPath, contents[0], contents[1])
return fileSystemEntryToDataTransferItem(entry, isFileSystemAPIAvailable)
}

const entry = buildFileSystemDirectoryEntry(fullPath, contents)
return fileSystemEntryToDataTransferItem(entry, isFileSystemAPIAvailable)
})
}

describe('Filesystem API traverseTree', () => {
it('Should traverse a file tree from root', async () => {
// Fake a FileSystemEntry tree
const root = buildFileSystemDirectoryEntry('root', dataTree)
const tree = await traverseTree(root as unknown as FileSystemEntry) as Directory

expect(tree.name).toBe('root')
expect(tree).toBeInstanceOf(Directory)
expect(tree.contents).toHaveLength(3)
expect(tree.size).toBe(13 * 4) // 13 bytes from 'Hello, world!'
})

it('Should traverse a file tree from a subdirectory', async () => {
// Fake a FileSystemEntry tree
const dir2 = buildFileSystemDirectoryEntry('dir2', dataTree.dir2)
const tree = await traverseTree(dir2 as unknown as FileSystemEntry) as Directory

expect(tree.name).toBe('dir2')
expect(tree).toBeInstanceOf(Directory)
expect(tree.contents).toHaveLength(1)
expect(tree.contents[0].name).toBe('file3.txt')
expect(tree.size).toBe(13) // 13 bytes from 'Hello, world!'
})

it('Should properly compute the last modified', async () => {
// Fake a FileSystemEntry tree
const root = buildFileSystemDirectoryEntry('root', dataTree)
const rootTree = await traverseTree(root as unknown as FileSystemEntry) as Directory

expect(rootTree.lastModified).toBe(7891234560)

// Fake a FileSystemEntry tree
const dir2 = buildFileSystemDirectoryEntry('root', dataTree.dir2)
const dir2Tree = await traverseTree(dir2 as unknown as FileSystemEntry) as Directory
expect(dir2Tree.lastModified).toBe(1234567890)
})
})

describe('DropService dataTransferToFileTree', () => {

beforeAll(() => {
// DataTransferItem doesn't exists in jsdom, let's mock
// a dumb one so we can check the instanceof
// @ts-expect-error jsdom doesn't have DataTransferItem
window.DataTransferItem = DataTransferItemMock
})

afterAll(() => {
// @ts-expect-error jsdom doesn't have DataTransferItem
delete window.DataTransferItem
})

it('Should return a RootDirectory with Filesystem API', async () => {
jest.spyOn(logger, 'error').mockImplementation(() => jest.fn())
jest.spyOn(logger, 'warn').mockImplementation(() => jest.fn())

const dataTransferItems = buildDataTransferItemArray('root', dataTree)
const fileTree = await dataTransferToFileTree(dataTransferItems as unknown as DataTransferItem[])

expect(fileTree.name).toBe('root')
expect(fileTree).toBeInstanceOf(Directory)
expect(fileTree.contents).toHaveLength(3)

// The file tree should be recursive when using the Filesystem API
expect(fileTree.contents[1]).toBeInstanceOf(Directory)
expect((fileTree.contents[1] as Directory).contents).toHaveLength(2)
expect(fileTree.contents[2]).toBeInstanceOf(Directory)
expect((fileTree.contents[2] as Directory).contents).toHaveLength(1)

expect(logger.error).not.toBeCalled()
expect(logger.warn).not.toBeCalled()
})

it('Should return a RootDirectory with legacy File API ignoring recursive directories', async () => {
jest.spyOn(logger, 'error').mockImplementation(() => jest.fn())
jest.spyOn(logger, 'warn').mockImplementation(() => jest.fn())

const dataTransferItems = buildDataTransferItemArray('root', dataTree, false)

const fileTree = await dataTransferToFileTree(dataTransferItems as unknown as DataTransferItem[])

expect(fileTree.name).toBe('root')
expect(fileTree).toBeInstanceOf(Directory)
expect(fileTree.contents).toHaveLength(1)

// The file tree should be recursive when using the Filesystem API
expect(fileTree.contents[0]).not.toBeInstanceOf(Directory)
expect((fileTree.contents[0].name)).toBe('file0.txt')

expect(logger.error).not.toBeCalled()
expect(logger.warn).toHaveBeenNthCalledWith(1, 'Could not get FilesystemEntry of item, falling back to file')
expect(logger.warn).toHaveBeenNthCalledWith(2, 'Could not get FilesystemEntry of item, falling back to file')
expect(logger.warn).toHaveBeenNthCalledWith(3, 'Browser does not support Filesystem API. Directories will not be uploaded')
expect(logger.warn).toHaveBeenNthCalledWith(4, 'Could not get FilesystemEntry of item, falling back to file')
expect(logger.warn).toHaveBeenCalledTimes(4)
})
})

+ 195
- 0
apps/files/src/services/DropServiceUtils.ts View File

@@ -0,0 +1,195 @@
/**
* @copyright Copyright (c) 2024 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { FileStat, ResponseDataDetailed } from 'webdav'

import { emit } from '@nextcloud/event-bus'
import { Folder, Node, davGetClient, davGetDefaultPropfind, davResultToNode } from '@nextcloud/files'
import { openConflictPicker } from '@nextcloud/upload'
import { showError, showInfo } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'

import logger from '../logger.js'

/**
* This represents a Directory in the file tree
* We extend the File class to better handling uploading
* and stay as close as possible as the Filesystem API.
* This also allow us to hijack the size or lastModified
* properties to compute them dynamically.
*/
export class Directory extends File {

/* eslint-disable no-use-before-define */
_contents: (Directory|File)[]

constructor(name, contents: (Directory|File)[] = []) {
super([], name, { type: 'httpd/unix-directory' })
this._contents = contents
}

set contents(contents: (Directory|File)[]) {
this._contents = contents
}

get contents(): (Directory|File)[] {
return this._contents
}

get size() {
return this._computeDirectorySize(this)
}

get lastModified() {
if (this._contents.length === 0) {
return Date.now()
}
return this._computeDirectoryMtime(this)
}

/**
* Get the last modification time of a file tree
* This is not perfect, but will get us a pretty good approximation
* @param directory the directory to traverse
*/
_computeDirectoryMtime(directory: Directory): number {
return directory.contents.reduce((acc, file) => {
return file.lastModified > acc
// If the file is a directory, the lastModified will
// also return the results of its _computeDirectoryMtime method
// Fancy recursion, huh?
? file.lastModified
: acc
}, 0)
}

/**
* Get the size of a file tree
* @param directory the directory to traverse
*/
_computeDirectorySize(directory: Directory): number {
return directory.contents.reduce((acc: number, entry: Directory|File) => {
// If the file is a directory, the size will
// also return the results of its _computeDirectorySize method
// Fancy recursion, huh?
return acc + entry.size
}, 0)
}

}

export type RootDirectory = Directory & {
name: 'root'
}

/**
* Traverse a file tree using the Filesystem API
* @param entry the entry to traverse
*/
export const traverseTree = async (entry: FileSystemEntry): Promise<Directory|File> => {
// Handle file
if (entry.isFile) {
return new Promise<File>((resolve, reject) => {
(entry as FileSystemFileEntry).file(resolve, reject)
})
}

// Handle directory
logger.debug('Handling recursive file tree', { entry: entry.name })
const directory = entry as FileSystemDirectoryEntry
const entries = await readDirectory(directory)
const contents = (await Promise.all(entries.map(traverseTree))).flat()
return new Directory(directory.name, contents)
}

/**
* Read a directory using Filesystem API
* @param directory the directory to read
*/
const readDirectory = (directory: FileSystemDirectoryEntry): Promise<FileSystemEntry[]> => {
const dirReader = directory.createReader()

return new Promise<FileSystemEntry[]>((resolve, reject) => {
const entries = [] as FileSystemEntry[]
const getEntries = () => {
dirReader.readEntries((results) => {
if (results.length) {
entries.push(...results)
getEntries()
} else {
resolve(entries)
}
}, (error) => {
reject(error)
})
}

getEntries()
})
}

export const createDirectoryIfNotExists = async (absolutePath: string) => {
const davClient = davGetClient()
const dirExists = await davClient.exists(absolutePath)
if (!dirExists) {
logger.debug('Directory does not exist, creating it', { absolutePath })
await davClient.createDirectory(absolutePath, { recursive: true })
const stat = await davClient.stat(absolutePath, { details: true, data: davGetDefaultPropfind() }) as ResponseDataDetailed<FileStat>
emit('files:node:created', davResultToNode(stat.data))
}
}

export const resolveConflict = async <T extends ((Directory|File)|Node)>(files: Array<T>, destination: Folder, contents: Node[]): Promise<T[]> => {
try {
// List all conflicting files
const conflicts = files.filter((file: File|Node) => {
return contents.find((node: Node) => node.basename === (file instanceof File ? file.name : file.basename))
}).filter(Boolean) as (File|Node)[]

// List of incoming files that are NOT in conflict
const uploads = files.filter((file: File|Node) => {
return !conflicts.includes(file)
})

// Let the user choose what to do with the conflicting files
const { selected, renamed } = await openConflictPicker(destination.path, conflicts, contents)

logger.debug('Conflict resolution', { uploads, selected, renamed })

// If the user selected nothing, we cancel the upload
if (selected.length === 0 && renamed.length === 0) {
// User skipped
showInfo(t('files', 'Conflicts resolution skipped'))
logger.info('User skipped the conflict resolution')
return []
}

// Update the list of files to upload
return [...uploads, ...selected, ...renamed] as (typeof files)
} catch (error) {
console.error(error)
// User cancelled
showError(t('files', 'Upload cancelled'))
logger.error('User cancelled the upload')
}

return []
}

+ 0
- 59
cypress/e2e/files/FileSystemAPIUtils.ts View File

@@ -1,59 +0,0 @@
import { basename } from 'node:path'

class FileSystemEntry {

private _isFile: boolean
private _fullPath: string

constructor(isFile: boolean, fullPath: string) {
this._isFile = isFile
this._fullPath = fullPath
}

get isFile() {
return !!this._isFile
}

get isDirectory() {
return !this.isFile
}

get name() {
return basename(this._fullPath)
}

}

export class FileSystemFileEntry extends FileSystemEntry {

private _contents: string

constructor(fullPath: string, contents: string) {
super(true, fullPath)
this._contents = contents
}

file(success: (file: File) => void) {
success(new File([this._contents], this.name))
}

}

export class FileSystemDirectoryEntry extends FileSystemEntry {

private _entries: FileSystemEntry[]

constructor(fullPath: string, entries: FileSystemEntry[]) {
super(false, fullPath)
this._entries = entries || []
}

createReader() {
return {
readEntries: (success: (entries: FileSystemEntry[]) => void) => {
success(this._entries)
},
}
}

}

+ 1
- 1
cypress/e2e/files/drag-n-drop.cy.ts View File

@@ -1,6 +1,6 @@
import { getRowForFile } from './FilesUtils.ts'

describe('files: Drag and Drop legacy', { testIsolation: true }, () => {
describe('files: Drag and Drop', { testIsolation: true }, () => {
beforeEach(() => {
cy.createRandomUser().then((user) => {
cy.login(user)

+ 7
- 0
jest.config.ts View File

@@ -26,18 +26,25 @@ const ignorePatterns = [
'@buttercup/fetch',
'@juliushaertl',
'@mdi/svg',
'@nextcloud/upload',
'@nextcloud/vue',
'ansi-regex',
'camelcase',
'char-regex',
'hot-patcher',
'is-svg',
'mime',
'p-cancelable',
'p-limit',
'p-queue',
'p-timeout',
'splitpanes',
'string-length',
'strip-ansi',
'tributejs',
'vue-material-design-icons',
'webdav',
'yocto-queue',
]

const config: Config = {

+ 34
- 30
package-lock.json View File

@@ -29,7 +29,7 @@
"@nextcloud/paths": "^2.1.0",
"@nextcloud/router": "^3.0.0",
"@nextcloud/sharing": "^0.1.0",
"@nextcloud/upload": "^1.0.5",
"@nextcloud/upload": "^1.1.0",
"@nextcloud/vue": "^8.11.1",
"@skjnldsv/sanitize-svg": "^1.0.2",
"@vueuse/components": "^10.7.2",
@@ -142,6 +142,7 @@
"karma-jasmine-sinon": "^1.0.4",
"karma-spec-reporter": "^0.0.36",
"karma-viewport": "^1.0.9",
"mime": "^4.0.1",
"puppeteer": "^22.5.0",
"raw-loader": "^4.0.2",
"regextras": "^0.8.0",
@@ -4528,19 +4529,20 @@
}
},
"node_modules/@nextcloud/upload": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@nextcloud/upload/-/upload-1.0.5.tgz",
"integrity": "sha512-QMojKvnBnxmxiKaFTpFIugaGsVQtjCvOrLdKzpa5IoNhouupI0vrE77aEXZuoOrhUHga9unN1YSA2hY0n8WrOQ==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@nextcloud/upload/-/upload-1.1.0.tgz",
"integrity": "sha512-SRBNKrPWZNMLwCkIiDfSvcDlbGisaliAbUDW0p7D0s4nA1zAG8Xfew87NQxmxNeqVeAM7IP8O83jd5MSPjKYDw==",
"dependencies": {
"@nextcloud/auth": "^2.2.1",
"@nextcloud/axios": "^2.4.0",
"@nextcloud/dialogs": "^5.0.0-beta.6",
"@nextcloud/files": "^3.0.0",
"@nextcloud/dialogs": "^5.2.0",
"@nextcloud/files": "^3.1.1",
"@nextcloud/l10n": "^2.2.0",
"@nextcloud/logger": "^2.7.0",
"@nextcloud/moment": "^1.3.1",
"@nextcloud/paths": "^2.1.0",
"@nextcloud/router": "^2.2.0",
"axios": "^1.6.2",
"@nextcloud/router": "^3.0.0",
"axios": "^1.6.8",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.0",
"p-cancelable": "^4.0.1",
@@ -4557,19 +4559,6 @@
"vue": "^2.7.16"
}
},
"node_modules/@nextcloud/upload/node_modules/@nextcloud/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-2.2.0.tgz",
"integrity": "sha512-M4AVGnB5tt3MYO5RpH/R2jq7z/nW05AmRhk4Lh68krVwRIYGo8pgNikKrPGogHd2Q3UgzF5Py1drHz3uuV99bQ==",
"dependencies": {
"@nextcloud/typings": "^1.7.0",
"core-js": "^3.6.4"
},
"engines": {
"node": "^20.0.0",
"npm": "^9.0.0"
}
},
"node_modules/@nextcloud/upload/node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
@@ -7554,11 +7543,11 @@
}
},
"node_modules/axios": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
"dependencies": {
"follow-redirects": "^1.15.4",
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
@@ -18470,6 +18459,18 @@
"wrap-ansi": "^7.0.0"
}
},
"node_modules/karma/node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"dev": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/karma/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -20127,15 +20128,18 @@
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/mime/-/mime-4.0.1.tgz",
"integrity": "sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA==",
"dev": true,
"funding": [
"https://github.com/sponsors/broofa"
],
"bin": {
"mime": "cli.js"
"mime": "bin/cli.js"
},
"engines": {
"node": ">=4.0.0"
"node": ">=16"
}
},
"node_modules/mime-db": {

+ 2
- 1
package.json View File

@@ -56,7 +56,7 @@
"@nextcloud/paths": "^2.1.0",
"@nextcloud/router": "^3.0.0",
"@nextcloud/sharing": "^0.1.0",
"@nextcloud/upload": "^1.0.5",
"@nextcloud/upload": "^1.1.0",
"@nextcloud/vue": "^8.11.1",
"@skjnldsv/sanitize-svg": "^1.0.2",
"@vueuse/components": "^10.7.2",
@@ -169,6 +169,7 @@
"karma-jasmine-sinon": "^1.0.4",
"karma-spec-reporter": "^0.0.36",
"karma-viewport": "^1.0.9",
"mime": "^4.0.1",
"puppeteer": "^22.5.0",
"raw-loader": "^4.0.2",
"regextras": "^0.8.0",

Loading…
Cancel
Save