Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 12 additions & 21 deletions apps/files/src/components/DragAndDropNotice.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
- @copyright Copyright (c) 2023 John Molakvoæ <[email protected]>
-
- @author John Molakvoæ <[email protected]>
- @author Ferdinand Thiessen <[email protected]>
-
- @license GNU AGPL version 3 or any later version
- @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
Expand Down Expand Up @@ -33,14 +34,14 @@
</template>

<script lang="ts">
import { showError, showSuccess } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { getUploader } from '@nextcloud/upload'
import { defineComponent } from 'vue'

import TrayArrowDownIcon from 'vue-material-design-icons/TrayArrowDown.vue'

import logger from '../logger.js'
import { handleDrop } from '../services/DropService'
import { showSuccess } from '@nextcloud/dialogs'

export default defineComponent({
name: 'DragAndDropNotice',
Expand Down Expand Up @@ -98,39 +99,29 @@ export default defineComponent({
event.preventDefault()
event.stopPropagation()

if (event.dataTransfer && event.dataTransfer.files?.length > 0) {
const uploader = getUploader()
uploader.destination = this.currentFolder

if (event.dataTransfer && event.dataTransfer.items.length > 0) {
// Start upload
logger.debug(`Uploading files to ${this.currentFolder.path}`)
const promises = [...event.dataTransfer.files].map(async (file: File) => {
try {
return await uploader.upload(file.name, file)
} catch (e) {
showError(t('files', 'Uploading "{filename}" failed', { filename: file.name }))
throw e
}
})

// Process finished uploads
Promise.all(promises).then((uploads) => {
handleDrop(event.dataTransfer).then((uploads) => {
logger.debug('Upload terminated', { uploads })
showSuccess(t('files', 'Upload successful'))

// Scroll to last upload if terminated
const lastUpload = uploads[uploads.length - 1]
if (lastUpload?.response?.headers?.['oc-fileid']) {
// Scroll to last upload in current directory if terminated
const lastUpload = uploads.findLast((upload) => !upload.file.webkitRelativePath.includes('/') && upload.response?.headers?.['oc-fileid'])
if (lastUpload !== undefined) {
this.$router.push({
...this.$route,
params: {
view: this.$route.params?.view ?? 'files',
// Remove instanceid from header response
fileid: parseInt(lastUpload.response?.headers?.['oc-fileid']),
fileid: parseInt(lastUpload.response!.headers['oc-fileid']),
},
})
}
})
}
this.dragover = false
},
t,
},
Expand Down
133 changes: 133 additions & 0 deletions apps/files/src/services/DropService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* @copyright Copyright (c) 2023 Ferdinand Thiessen <[email protected]>
*
* @author Ferdinand Thiessen <[email protected]>
*
* @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 { Upload } from '@nextcloud/upload'
import type { FileStat, ResponseDataDetailed } from 'webdav'

import { showError } from '@nextcloud/dialogs'
import { emit } from '@nextcloud/event-bus'
import { davGetClient, davGetDefaultPropfind, davResultToNode, davRootPath } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import { getUploader } from '@nextcloud/upload'
import logger from '../logger.js'

export const handleDrop = async (data: DataTransfer) => {
// TODO: Maybe handle `getAsFileSystemHandle()` in the future

const uploads = [] as Upload[]
for (const item of data.items) {
if (item.kind !== 'file') {
logger.debug('Skipping dropped item', { kind: item.kind, type: item.type })
continue
}

// MDN recommends to try both, as it might be renamed in the future
const entry = (item as unknown as { getAsEntry?: () => FileSystemEntry|undefined})?.getAsEntry?.() ?? item.webkitGetAsEntry()

// Handle browser issues if Filesystem API is not available. Fallback to File API
if (entry === null) {
logger.debug('Could not get FilesystemEntry of item, falling back to file')
const file = item.getAsFile()
if (file === null) {
logger.warn('Could not process DataTransferItem', { type: item.type, kind: item.kind })
showError(t('files', 'One of the dropped files could not be processed'))
} else {
uploads.push(await handleFileUpload(file))
}
} else {
logger.debug('Handle recursive upload', { entry: entry.name })
// Use Filesystem API
uploads.push(...await handleRecursiveUpload(entry))
}
}
return uploads
}

const handleFileUpload = async (file: File, path: string = '') => {
const uploader = getUploader()

try {
return await uploader.upload(`${path}${file.name}`, file)
} catch (e) {
showError(t('files', 'Uploading "{filename}" failed', { filename: file.name }))
throw e
}
}

const handleRecursiveUpload = async (entry: FileSystemEntry, path: string = ''): Promise<Upload[]> => {
if (entry.isFile) {
return [
await new Promise<Upload>((resolve, reject) => {
(entry as FileSystemFileEntry).file(
async (file) => resolve(await handleFileUpload(file, path)),
(error) => reject(error),
)
}),
]
} else {
const directory = entry as FileSystemDirectoryEntry
logger.debug('Handle directory recursivly', { name: directory.name })

// TODO: Implement this on `@nextcloud/upload`
const absolutPath = `${davRootPath}${getUploader().destination.path}${path}${directory.name}`
const davClient = davGetClient()
const dirExists = await davClient.exists(absolutPath)
if (!dirExists) {
logger.debug('Directory does not exist, creating it', { absolutPath })
await davClient.createDirectory(absolutPath, { recursive: true })
const stat = await davClient.stat(absolutPath, { details: true, data: davGetDefaultPropfind() }) as ResponseDataDetailed<FileStat>
emit('files:node:created', davResultToNode(stat.data))
}

const entries = await readDirectory(directory)
// sorted so we upload files first before starting next level
const promises = entries.sort((a) => a.isFile ? -1 : 1)
.map((file) => handleRecursiveUpload(file, `${path}${directory.name}/`))
return (await Promise.all(promises)).flat()
}
}

/**
* Read a directory using Filesystem API
* @param directory the directory to read
*/
function readDirectory(directory: FileSystemDirectoryEntry) {
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()
})
}
4 changes: 2 additions & 2 deletions dist/files-main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files-main.js.map

Large diffs are not rendered by default.