diff --git a/index.js b/index.js index b10c181..688eedc 100644 --- a/index.js +++ b/index.js @@ -48,6 +48,14 @@ import { listView } from '@osjs/gui'; + +/** + * flag indicating whether uploading folders is supported + */ +const supportsUploadingFolders = core => + core.config('filemanager.disableMultiUpload', false) !== true + && !!(window.DataTransferItem && DataTransferItem.prototype.webkitGetAsEntry); + /** * Creates default settings */ @@ -66,7 +74,8 @@ const createWindowOptions = (core, proc, title) => ({ attributes: { mediaQueries: { small: 'screen and (max-width: 400px)' - } + }, + droppable: {dataTransferProperty: supportsUploadingFolders(core) ? 'items' : 'files'}, }, dimension: Object.assign({ width: 400, @@ -284,7 +293,7 @@ const vfsActionFactory = (core, proc, win, dialog, state) => { }; const writeRelative = f => { - const d = dialog('progress', f); + const d = dialog('progress', f.name); return vfs.writefile({ path: pathJoin(state.currentPath.path, f.name) @@ -300,12 +309,122 @@ const vfsActionFactory = (core, proc, win, dialog, state) => { }); }; - const uploadBrowserFiles = (files) => { + const legacyUploadBrowserFiles = (files) => { Promise.all(files.map(writeRelative)) .then(() => refresh(files[0].name)) // FIXME: Select all ? .catch(error => dialog('error', error, __('MSG_UPLOAD_ERROR'))); }; + const getUploadList = async (items) => { + /* + type: [{dirPath: string, file?: any}] + Directories do not have the `file` property. + They only have their relative path stored in `dirPath`. + Files store their containing folders path as their `dirPath`. + */ + const uploadList = []; + + const getDirectoryEntries = (directory) => { + const reader = directory.createReader(); + return new Promise(resolve => { + reader.readEntries(async (entries) => { + resolve(entries); + }); + }); + }; + + const getFileFromEntry = (entry) => { + return new Promise((resolve, reject) => { + entry.file((file) => { + resolve(file); + }, (error) => { + reject(error); + }); + }); + }; + + const checkDirectory = async (directory, dirPath) => { + uploadList.push({dirPath}); + const entries = await getDirectoryEntries(directory); + for (let entry of entries) { + if (entry.isFile) { + const file = await getFileFromEntry(entry); + uploadList.push({dirPath, file}); + } else if (entry.isDirectory) { + const subDirPath = dirPath + '/' + entry.name; + await checkDirectory(entry, subDirPath); + } + } + }; + + try { + for (let item of items) { + const entry = item.webkitGetAsEntry(); + if (entry.isFile) { + const file = item.getAsFile(); + const dirPath = ''; + uploadList.push({dirPath, file}); + } else if (entry.isDirectory) { + await checkDirectory(entry, entry.name); + } + } + } catch (error) { + console.warn(error); + } + + return uploadList; + }; + + const uploadFileAndFolderList = async (list) => { + const files = list.map(({file}) => file).filter((file) => file); + const totalSize = files.reduce((sum, {size}) => sum + size, 0); + const dialogTitle = files.length === 1 ? files[0].name : 'multiple files'; + const abortController = new AbortController(); + let isCancelled = false; + const onCancel = () => { + isCancelled = false; + abortController.abort(); + }; + const d = dialog('progress', dialogTitle, onCancel); + try { + let uploaded = 0; + for (let {dirPath, file} of list) { + if (isCancelled) { + return; + } + if (file) { + // upload file + await vfs.writefile({ + path: pathJoin(state.currentPath.path, dirPath, file.name) + }, file, { + signal: abortController.signal, + pid: proc.pid, + onProgress: (ev, progress) => { + d.setProgress(Math.round((uploaded + progress * file.size / 100) * 100 / totalSize)); + } + }); + uploaded += file.size; + } else { + // create folder + await vfs.mkdir({path: pathJoin(state.currentPath.path, dirPath)}, {pid: proc.pid}); + } + } + } catch (error) { + dialog('error', error, __('MSG_UPLOAD_ERROR')); + } + d.destroy(); + }; + + const uploadBrowserFiles = async (items) => { + if (!supportsUploadingFolders(core)) { + return legacyUploadBrowserFiles(items); + } + + const uploadList = await getUploadList(items); + await uploadFileAndFolderList(uploadList); + refresh(items[0].name); // FIXME: Select all ? + }; + const uploadVirtualFile = (data) => { const dest = {path: pathJoin(state.currentPath.path, data.filename)}; if (dest.path !== data.path) { @@ -449,10 +568,10 @@ const dialogFactory = (core, proc, win) => { action(() => vfs.unlink(file, {pid: proc.pid}), true, __('MSG_DELETE_ERROR')); })); - const progressDialog = (file) => dialog('progress', { - message: __('DIALOG_PROGRESS_MESSAGE', file.name), - buttons: [] - }, () => {}, false); + const progressDialog = (name, cb = (() => {})) => dialog('progress', { + message: __('DIALOG_PROGRESS_MESSAGE', name), + buttons: ['Cancel'] + }, cb, false); const errorDialog = (error, message) => dialog('alert', { type: 'error',