Signed-off-by: skjnldsv <skjnldsv@protonmail.com>tags/v28.0.5rc1
@@ -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, | |||
) | |||
} |
@@ -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: { |
@@ -329,7 +329,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 | |||
} | |||
@@ -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 |
@@ -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) | |||
}) | |||
}) |
@@ -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 [] | |||
} |
@@ -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,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) |
@@ -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 = { |
@@ -29,7 +29,7 @@ | |||
"@nextcloud/paths": "^2.1.0", | |||
"@nextcloud/router": "^2.2.1", | |||
"@nextcloud/sharing": "^0.1.0", | |||
"@nextcloud/upload": "^1.0.5", | |||
"@nextcloud/upload": "^1.1.0", | |||
"@nextcloud/vue": "^8.10.0", | |||
"@skjnldsv/sanitize-svg": "^1.0.2", | |||
"@vueuse/components": "^10.7.2", | |||
@@ -143,6 +143,7 @@ | |||
"karma-jasmine-sinon": "^1.0.4", | |||
"karma-spec-reporter": "^0.0.36", | |||
"karma-viewport": "^1.0.9", | |||
"mime": "^4.0.1", | |||
"node-polyfill-webpack-plugin": "^2.0.1", | |||
"puppeteer": "^21.4.1", | |||
"raw-loader": "^4.0.2", | |||
@@ -1952,9 +1953,9 @@ | |||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" | |||
}, | |||
"node_modules/@buttercup/fetch": { | |||
"version": "0.1.2", | |||
"resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.1.2.tgz", | |||
"integrity": "sha512-mDBtsysQ0Gnrp4FamlRJGpu7HUHwbyLC4uUav1I7QAqThFAa/4d1cdZCxrV5gKvh6zO1fu95bILNJi4Y2hALhQ==", | |||
"version": "0.2.1", | |||
"resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz", | |||
"integrity": "sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==", | |||
"optionalDependencies": { | |||
"node-fetch": "^3.3.0" | |||
} | |||
@@ -3817,30 +3818,31 @@ | |||
} | |||
}, | |||
"node_modules/@nextcloud/dialogs": { | |||
"version": "5.1.2", | |||
"resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-5.1.2.tgz", | |||
"integrity": "sha512-JhWUdjjJwjY2K2O2d5CMgcSn+46RMt28Uv1ToFpm1lcdwP7swOp7u9tE8P4p1vA7hZBOKgRGEuTWI/qz/3dxHQ==", | |||
"version": "5.2.0", | |||
"resolved": "https://registry.npmjs.org/@nextcloud/dialogs/-/dialogs-5.2.0.tgz", | |||
"integrity": "sha512-+nO9/obNXGZUc0AJzzGbK4kniJborfbTeohN17owffFHGHB5TzDE0P1wiMimM3ki4Itfx+9aYuHyMCbv+43E1Q==", | |||
"dependencies": { | |||
"@mdi/js": "^7.4.47", | |||
"@nextcloud/auth": "^2.2.1", | |||
"@nextcloud/axios": "^2.4.0", | |||
"@nextcloud/event-bus": "^3.1.0", | |||
"@nextcloud/files": "^3.1.0", | |||
"@nextcloud/initial-state": "^2.1.0", | |||
"@nextcloud/l10n": "^2.2.0", | |||
"@nextcloud/router": "^3.0.0", | |||
"@nextcloud/typings": "^1.7.0", | |||
"@nextcloud/typings": "^1.8.0", | |||
"@types/toastify-js": "^1.12.3", | |||
"@vueuse/core": "^10.7.2", | |||
"@vueuse/core": "^10.9.0", | |||
"toastify-js": "^1.12.0", | |||
"vue-frag": "^1.4.3", | |||
"webdav": "^5.3.2" | |||
"webdav": "^5.4.0" | |||
}, | |||
"engines": { | |||
"node": "^20.0.0", | |||
"npm": "^10.0.0" | |||
}, | |||
"peerDependencies": { | |||
"@nextcloud/vue": "^8.2.0", | |||
"@nextcloud/vue": "^8.9.1", | |||
"vue": "^2.7.16" | |||
} | |||
}, | |||
@@ -3856,6 +3858,89 @@ | |||
"npm": "^10.0.0" | |||
} | |||
}, | |||
"node_modules/@nextcloud/dialogs/node_modules/@vueuse/core": { | |||
"version": "10.9.0", | |||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.9.0.tgz", | |||
"integrity": "sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==", | |||
"dependencies": { | |||
"@types/web-bluetooth": "^0.0.20", | |||
"@vueuse/metadata": "10.9.0", | |||
"@vueuse/shared": "10.9.0", | |||
"vue-demi": ">=0.14.7" | |||
}, | |||
"funding": { | |||
"url": "https://github.com/sponsors/antfu" | |||
} | |||
}, | |||
"node_modules/@nextcloud/dialogs/node_modules/@vueuse/core/node_modules/vue-demi": { | |||
"version": "0.14.7", | |||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", | |||
"integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", | |||
"hasInstallScript": true, | |||
"bin": { | |||
"vue-demi-fix": "bin/vue-demi-fix.js", | |||
"vue-demi-switch": "bin/vue-demi-switch.js" | |||
}, | |||
"engines": { | |||
"node": ">=12" | |||
}, | |||
"funding": { | |||
"url": "https://github.com/sponsors/antfu" | |||
}, | |||
"peerDependencies": { | |||
"@vue/composition-api": "^1.0.0-rc.1", | |||
"vue": "^3.0.0-0 || ^2.6.0" | |||
}, | |||
"peerDependenciesMeta": { | |||
"@vue/composition-api": { | |||
"optional": true | |||
} | |||
} | |||
}, | |||
"node_modules/@nextcloud/dialogs/node_modules/@vueuse/metadata": { | |||
"version": "10.9.0", | |||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.9.0.tgz", | |||
"integrity": "sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==", | |||
"funding": { | |||
"url": "https://github.com/sponsors/antfu" | |||
} | |||
}, | |||
"node_modules/@nextcloud/dialogs/node_modules/@vueuse/shared": { | |||
"version": "10.9.0", | |||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.9.0.tgz", | |||
"integrity": "sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==", | |||
"dependencies": { | |||
"vue-demi": ">=0.14.7" | |||
}, | |||
"funding": { | |||
"url": "https://github.com/sponsors/antfu" | |||
} | |||
}, | |||
"node_modules/@nextcloud/dialogs/node_modules/@vueuse/shared/node_modules/vue-demi": { | |||
"version": "0.14.7", | |||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", | |||
"integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", | |||
"hasInstallScript": true, | |||
"bin": { | |||
"vue-demi-fix": "bin/vue-demi-fix.js", | |||
"vue-demi-switch": "bin/vue-demi-switch.js" | |||
}, | |||
"engines": { | |||
"node": ">=12" | |||
}, | |||
"funding": { | |||
"url": "https://github.com/sponsors/antfu" | |||
}, | |||
"peerDependencies": { | |||
"@vue/composition-api": "^1.0.0-rc.1", | |||
"vue": "^3.0.0-0 || ^2.6.0" | |||
}, | |||
"peerDependenciesMeta": { | |||
"@vue/composition-api": { | |||
"optional": true | |||
} | |||
} | |||
}, | |||
"node_modules/@nextcloud/eslint-config": { | |||
"version": "8.3.0", | |||
"resolved": "https://registry.npmjs.org/@nextcloud/eslint-config/-/eslint-config-8.3.0.tgz", | |||
@@ -3943,23 +4028,35 @@ | |||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" | |||
}, | |||
"node_modules/@nextcloud/files": { | |||
"version": "3.1.0", | |||
"resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.1.0.tgz", | |||
"integrity": "sha512-i0g9L5HRBJ2vr/gXYb0Gtg379u6nYZJFL30W50OV0F0qlf8OtkAlNpfOVOg3sJf9zklARE2lVY9g2Y9sv/iQ3g==", | |||
"version": "3.1.1", | |||
"resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.1.1.tgz", | |||
"integrity": "sha512-PwGxh/AcKeDehYSf/L+OpYNzZ2eK5xA1l/lVjufwa7I+u2onCo6qjYSqvc9Dh4Myzixjmt5YiA+Um/gx/Kq4NA==", | |||
"dependencies": { | |||
"@nextcloud/auth": "^2.2.1", | |||
"@nextcloud/l10n": "^2.2.0", | |||
"@nextcloud/logger": "^2.7.0", | |||
"@nextcloud/paths": "^2.1.0", | |||
"@nextcloud/router": "^2.2.0", | |||
"@nextcloud/router": "^3.0.0", | |||
"is-svg": "^5.0.0", | |||
"webdav": "^5.3.1" | |||
"webdav": "^5.4.0" | |||
}, | |||
"engines": { | |||
"node": "^20.0.0", | |||
"npm": "^9.0.0" | |||
} | |||
}, | |||
"node_modules/@nextcloud/files/node_modules/@nextcloud/router": { | |||
"version": "3.0.0", | |||
"resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-3.0.0.tgz", | |||
"integrity": "sha512-RlPrOPw94yT9rmt3+2sUs2cmWzqhX5eFW+i/EHymJEKgURVtnqCcXjIcAiLTfgsCCdAS1hGapBL8j8rhHk1FHQ==", | |||
"dependencies": { | |||
"@nextcloud/typings": "^1.7.0" | |||
}, | |||
"engines": { | |||
"node": "^20.0.0", | |||
"npm": "^10.0.0" | |||
} | |||
}, | |||
"node_modules/@nextcloud/initial-state": { | |||
"version": "2.1.0", | |||
"resolved": "https://registry.npmjs.org/@nextcloud/initial-state/-/initial-state-2.1.0.tgz", | |||
@@ -3999,14 +4096,12 @@ | |||
} | |||
}, | |||
"node_modules/@nextcloud/moment": { | |||
"version": "1.2.2", | |||
"resolved": "https://registry.npmjs.org/@nextcloud/moment/-/moment-1.2.2.tgz", | |||
"integrity": "sha512-66jJJurd4JdqqlGIpqfxMWOvpG7i6dMibkNCPcpe8i+C+bGSFRMxMe74m1abehcaysj164is4juiT2ikVbZ4yg==", | |||
"version": "1.3.1", | |||
"resolved": "https://registry.npmjs.org/@nextcloud/moment/-/moment-1.3.1.tgz", | |||
"integrity": "sha512-+1CtYlc4Lu4soa1RKXvUsTJdsHS0kHUCzNBtb02BADMY5PMGUTCiCQx5xf1Ez15h2ehuwg0vESr8VyKem9sGAQ==", | |||
"dependencies": { | |||
"@nextcloud/l10n": "^2.2.0", | |||
"core-js": "^3.21.1", | |||
"jed": "^1.1.1", | |||
"moment": "^2.29.2", | |||
"moment": "^2.30.1", | |||
"node-gettext": "^3.0.0" | |||
}, | |||
"engines": { | |||
@@ -4077,33 +4172,34 @@ | |||
} | |||
}, | |||
"node_modules/@nextcloud/typings": { | |||
"version": "1.7.0", | |||
"resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-1.7.0.tgz", | |||
"integrity": "sha512-fK1i09FYTfSUBdXswyiCr8ng5MwdWjEWOF7hRvNvq5i+XFUSmGjSsRmpQZFM2AONroHqGGQBkvQqpONUshFBJQ==", | |||
"version": "1.8.0", | |||
"resolved": "https://registry.npmjs.org/@nextcloud/typings/-/typings-1.8.0.tgz", | |||
"integrity": "sha512-q9goE0wc+1BCI9Ku0MebCHmqOMwz2K7ESKQrcHDs6O+HqbKA8zGiEtXL5XGrMS7Ovtl1YOIwxlP9kEvgvXt52Q==", | |||
"dependencies": { | |||
"@types/jquery": "3.5.16", | |||
"vue": "^2.7.14", | |||
"vue": "^2.7.15", | |||
"vue-router": "<4" | |||
}, | |||
"engines": { | |||
"node": "^16.0.0", | |||
"npm": "^7.0.0 || ^8.0.0" | |||
"node": "^20.0.0", | |||
"npm": "^10.0.0" | |||
} | |||
}, | |||
"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", | |||
@@ -4120,6 +4216,18 @@ | |||
"vue": "^2.7.16" | |||
} | |||
}, | |||
"node_modules/@nextcloud/upload/node_modules/@nextcloud/router": { | |||
"version": "3.0.0", | |||
"resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-3.0.0.tgz", | |||
"integrity": "sha512-RlPrOPw94yT9rmt3+2sUs2cmWzqhX5eFW+i/EHymJEKgURVtnqCcXjIcAiLTfgsCCdAS1hGapBL8j8rhHk1FHQ==", | |||
"dependencies": { | |||
"@nextcloud/typings": "^1.7.0" | |||
}, | |||
"engines": { | |||
"node": "^20.0.0", | |||
"npm": "^10.0.0" | |||
} | |||
}, | |||
"node_modules/@nextcloud/upload/node_modules/eventemitter3": { | |||
"version": "5.0.1", | |||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", | |||
@@ -7181,11 +7289,11 @@ | |||
} | |||
}, | |||
"node_modules/axios": { | |||
"version": "1.6.2", | |||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", | |||
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", | |||
"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.0", | |||
"follow-redirects": "^1.15.6", | |||
"form-data": "^4.0.0", | |||
"proxy-from-env": "^1.1.0" | |||
} | |||
@@ -13038,9 +13146,9 @@ | |||
} | |||
}, | |||
"node_modules/follow-redirects": { | |||
"version": "1.15.5", | |||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", | |||
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", | |||
"version": "1.15.6", | |||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", | |||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", | |||
"funding": [ | |||
{ | |||
"type": "individual", | |||
@@ -15364,11 +15472,6 @@ | |||
"node": ">=0.1.103" | |||
} | |||
}, | |||
"node_modules/jed": { | |||
"version": "1.1.1", | |||
"resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz", | |||
"integrity": "sha512-z35ZSEcXHxLW4yumw0dF6L464NT36vmx3wxJw8MDpraBcWuNVgUPZgPJKcu1HekNgwlMFNqol7i/IpSbjhqwqA==" | |||
}, | |||
"node_modules/jest": { | |||
"version": "29.7.0", | |||
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", | |||
@@ -17863,6 +17966,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/mkdirp": { | |||
"version": "0.5.6", | |||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", | |||
@@ -19507,15 +19622,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": { | |||
@@ -19714,9 +19832,9 @@ | |||
} | |||
}, | |||
"node_modules/moment": { | |||
"version": "2.29.4", | |||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", | |||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", | |||
"version": "2.30.1", | |||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", | |||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", | |||
"engines": { | |||
"node": "*" | |||
} | |||
@@ -26616,20 +26734,20 @@ | |||
} | |||
}, | |||
"node_modules/web-streams-polyfill": { | |||
"version": "3.2.1", | |||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", | |||
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", | |||
"version": "3.3.3", | |||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", | |||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", | |||
"optional": true, | |||
"engines": { | |||
"node": ">= 8" | |||
} | |||
}, | |||
"node_modules/webdav": { | |||
"version": "5.3.2", | |||
"resolved": "https://registry.npmjs.org/webdav/-/webdav-5.3.2.tgz", | |||
"integrity": "sha512-wfUh68rccDcH1A9W5gdcAflBm0EOeXrX3LwKbdDLWR0SDFE5QTPfLXPkDit+zGC0tRihCD9qzPfIVEUFoc7MwA==", | |||
"version": "5.5.0", | |||
"resolved": "https://registry.npmjs.org/webdav/-/webdav-5.5.0.tgz", | |||
"integrity": "sha512-SHSDe6n8lBuwwyX+uePB1N1Yn35ebd3locl/LbADMWpcEoowyFdIbnH3fv17T4Jf2tOa1Vwjr/Lld3t0dOio1w==", | |||
"dependencies": { | |||
"@buttercup/fetch": "^0.1.1", | |||
"@buttercup/fetch": "^0.2.1", | |||
"base-64": "^1.0.0", | |||
"byte-length": "^1.0.2", | |||
"fast-xml-parser": "^4.2.4", |
@@ -56,7 +56,7 @@ | |||
"@nextcloud/paths": "^2.1.0", | |||
"@nextcloud/router": "^2.2.1", | |||
"@nextcloud/sharing": "^0.1.0", | |||
"@nextcloud/upload": "^1.0.5", | |||
"@nextcloud/upload": "^1.1.0", | |||
"@nextcloud/vue": "^8.10.0", | |||
"@skjnldsv/sanitize-svg": "^1.0.2", | |||
"@vueuse/components": "^10.7.2", | |||
@@ -170,6 +170,7 @@ | |||
"karma-jasmine-sinon": "^1.0.4", | |||
"karma-spec-reporter": "^0.0.36", | |||
"karma-viewport": "^1.0.9", | |||
"mime": "^4.0.1", | |||
"node-polyfill-webpack-plugin": "^2.0.1", | |||
"puppeteer": "^21.4.1", | |||
"raw-loader": "^4.0.2", |