Merge pull request #682 from ONLYOFFICE/feature/upload-empty-folders

Feature/upload empty folders
This commit is contained in:
Nikita Gopienko 2022-05-30 19:37:05 +03:00 committed by GitHub
commit 5795586f0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 326 additions and 12 deletions

View File

@ -0,0 +1,190 @@
function toFileWithPath(file, path) {
if (typeof file?.path === "string") return file;
// on electron, path is already set to the absolute path
const { webkitRelativePath } = file;
Object.defineProperty(file, "path", {
value:
typeof path === "string"
? path
: // If <input webkitdirectory> is set,
// the File will have a {webkitRelativePath} property
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory
typeof webkitRelativePath === "string" && webkitRelativePath.length > 0
? webkitRelativePath
: file.name,
});
return file;
}
const FILES_TO_IGNORE = [
// Thumbnail cache files for macOS and Windows
".DS_Store",
"Thumbs.db", // Windows
];
/**
* Convert a DragEvent's DataTrasfer object to a list of File objects
* NOTE: If some of the items are folders,
* everything will be flattened and placed in the same list but the paths will be kept as a {path} property.
* @param evt
*/
export default async function getFilesFromEvent(evt) {
return isDragEvt(evt) && evt.dataTransfer
? getDataTransferFiles(evt.dataTransfer, evt.type)
: getInputFiles(evt);
}
function isDragEvt(value) {
return !!value.dataTransfer;
}
function getInputFiles(evt) {
const files = isInput(evt.target)
? evt.target.files
? fromList(evt.target.files)
: []
: [];
return files.map((file) => toFileWithPath(file));
}
function isInput(value) {
return value !== null;
}
async function getDataTransferFiles(dt, type) {
// IE11 does not support dataTransfer.items
// See https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/items#Browser_compatibility
if (dt.items) {
const items = fromList(dt.items).filter((item) => item.kind === "file");
// According to https://html.spec.whatwg.org/multipage/dnd.html#dndevents,
// only 'dragstart' and 'drop' has access to the data (source node)
if (type !== "drop") {
return items;
}
const files = await Promise.all(items.map(toFilePromises));
return noIgnoredFiles(flatten(files));
}
return noIgnoredFiles(fromList(dt.files).map((file) => toFileWithPath(file)));
}
function noIgnoredFiles(files) {
return files.filter((file) => FILES_TO_IGNORE.indexOf(file.name) === -1);
}
// IE11 does not support Array.from()
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Browser_compatibility
// https://developer.mozilla.org/en-US/docs/Web/API/FileList
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItemList
function fromList(items) {
const files = [];
// tslint:disable: prefer-for-of
for (let i = 0; i < items.length; i++) {
const file = items[i];
files.push(file);
}
return files;
}
// https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem
function toFilePromises(item) {
if (typeof item.webkitGetAsEntry !== "function") {
return fromDataTransferItem(item);
}
const entry = item.webkitGetAsEntry();
// Safari supports dropping an image node from a different window and can be retrieved using
// the DataTransferItem.getAsFile() API
// NOTE: FileSystemEntry.file() throws if trying to get the file
if (entry && entry.isDirectory) {
return fromDirEntry(entry);
}
return fromDataTransferItem(item);
}
function flatten(items) {
return items.reduce(
(acc, files) => [
...acc,
...(Array.isArray(files) ? flatten(files) : [files]),
],
[]
);
}
function fromDataTransferItem(item) {
const file = item.getAsFile();
if (!file) {
return Promise.reject(`${item} is not a File`);
}
const fwp = toFileWithPath(file);
return Promise.resolve(fwp);
}
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemEntry
async function fromEntry(entry) {
return entry.isDirectory ? fromDirEntry(entry) : fromFileEntry(entry);
}
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryEntry
function fromDirEntry(entry) {
const reader = entry.createReader();
return new Promise((resolve, reject) => {
const entries = [];
let empty = true;
function readEntries() {
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryEntry/createReader
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryReader/readEntries
reader.readEntries(
async (batch) => {
if (!batch.length) {
// Done reading directory
try {
const files = await Promise.all(entries);
if (empty) {
files.push([createEmptyDirFile(entry)]);
}
resolve(files);
} catch (err) {
reject(err);
}
} else {
const items = Promise.all(batch.map(fromEntry));
entries.push(items);
// Continue reading
empty = false;
readEntries();
}
},
(err) => {
reject(err);
}
);
}
readEntries();
});
}
function createEmptyDirFile(entry) {
const file = new File([], entry.name);
const fwp = toFileWithPath(file, entry.fullPath + "/");
Object.defineProperty(fwp, "isEmptyDirectory", {
value: true,
});
return fwp;
}
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemFileEntry
async function fromFileEntry(entry) {
return new Promise((resolve, reject) => {
entry.file(
(file) => {
const fwp = toFileWithPath(file, entry.fullPath);
resolve(fwp);
},
(err) => {
reject(err);
}
);
});
}

View File

@ -1,5 +1,6 @@
import React from "react";
import React, { useCallback } from "react";
import { useDropzone } from "react-dropzone";
import getFilesFromEvent from "./get-files-from-event";
import PropTypes from "prop-types";
import StyledDragAndDrop from "./styled-drag-and-drop";
@ -8,7 +9,7 @@ const DragAndDrop = (props) => {
const { isDropZone, children, dragging, className, ...rest } = props;
const classNameProp = className ? className : "";
const onDrop = (acceptedFiles, array) => {
const onDrop = (acceptedFiles) => {
acceptedFiles.length && props.onDrop && props.onDrop(acceptedFiles);
};
@ -25,6 +26,7 @@ const DragAndDrop = (props) => {
onDrop,
onDragOver,
onDragLeave,
getFilesFromEvent: (event) => getFilesFromEvent(event),
});
return (

View File

@ -30,7 +30,7 @@
"react-countdown": "2.3.2",
"react-custom-scrollbars": "^4.2.1",
"react-device-detect": "^1.17.0",
"react-dropzone": "^11.2.4",
"react-dropzone": "^11.4.2",
"react-lifecycles-compat": "^3.0.4",
"react-onclickoutside": "^6.11.2",
"react-svg": "^12.1.0",

View File

@ -34,10 +34,26 @@ export default function withFileActions(WrappedFileItem) {
};
onDropZoneUpload = (files, uploadToFolder) => {
const { t, dragging, setDragging, startUpload } = this.props;
const {
t,
dragging,
setDragging,
startUpload,
uploadEmptyFolders,
} = this.props;
dragging && setDragging(false);
startUpload(files, uploadToFolder, t);
const emptyFolders = files.filter((f) => f.isEmptyDirectory);
if (emptyFolders.length > 0) {
uploadEmptyFolders(emptyFolders, uploadToFolder).then(() => {
const onlyFiles = files.filter((f) => !f.isEmptyDirectory);
if (onlyFiles.length > 0) startUpload(onlyFiles, uploadToFolder, t);
});
} else {
startUpload(files, uploadToFolder, t);
}
};
onDrop = (items) => {
@ -247,6 +263,7 @@ export default function withFileActions(WrappedFileItem) {
onSelectItem,
setNewBadgeCount,
openFileAction,
uploadEmptyFolders,
} = filesActionsStore;
const { setSharingPanelVisible } = dialogsStore;
const {
@ -315,6 +332,7 @@ export default function withFileActions(WrappedFileItem) {
dragging,
setDragging,
startUpload,
uploadEmptyFolders,
draggable,
setTooltipPosition,
setStartDrag,

View File

@ -27,6 +27,7 @@ const Item = ({
onBadgeClick,
showDragItems,
startUpload,
uploadEmptyFolders,
setDragging,
}) => {
const [isDragActive, setIsDragActive] = React.useState(false);
@ -41,9 +42,18 @@ const Item = ({
const onDropZoneUpload = React.useCallback(
(files, uploadToFolder) => {
dragging && setDragging(false);
startUpload(files, uploadToFolder, t);
const emptyFolders = files.filter((f) => f.isEmptyDirectory);
if (emptyFolders.length > 0) {
uploadEmptyFolders(emptyFolders, uploadToFolder).then(() => {
const onlyFiles = files.filter((f) => !f.isEmptyDirectory);
if (onlyFiles.length > 0) startUpload(onlyFiles, uploadToFolder, t);
});
} else {
startUpload(files, uploadToFolder, t);
}
},
[t, dragging, setDragging, startUpload]
[t, dragging, setDragging, startUpload, uploadEmptyFolders]
);
const onDrop = React.useCallback(
@ -118,6 +128,7 @@ const Items = ({
dragging,
setDragging,
startUpload,
uploadEmptyFolders,
isAdmin,
myId,
@ -273,6 +284,7 @@ const Items = ({
t={t}
setDragging={setDragging}
startUpload={startUpload}
uploadEmptyFolders={uploadEmptyFolders}
item={item}
dragging={dragging}
getFolderIcon={getFolderIcon}
@ -301,6 +313,7 @@ const Items = ({
showText,
setDragging,
startUpload,
uploadEmptyFolders,
]
);
@ -337,7 +350,7 @@ export default inject(
} = treeFoldersStore;
const { id } = selectedFolderStore;
const { moveDragItems, uploadEmptyFolders } = filesActionsStore;
return {
isAdmin: auth.isAdmin,
myId: myFolderId,
@ -352,8 +365,9 @@ export default inject(
dragging,
setDragging,
setStartDrag,
moveDragItems: filesActionsStore.moveDragItems,
moveDragItems,
startUpload,
uploadEmptyFolders,
};
}
)(withTranslation(["Home", "Common", "Translations"])(observer(Items)));

View File

@ -205,9 +205,24 @@ class PureHome extends React.Component {
};
onDrop = (files, uploadToFolder) => {
const { t, startUpload, setDragging, dragging } = this.props;
const {
t,
startUpload,
setDragging,
dragging,
uploadEmptyFolders,
} = this.props;
dragging && setDragging(false);
startUpload(files, uploadToFolder, t);
const emptyFolders = files.filter((f) => f.isEmptyDirectory);
if (emptyFolders.length > 0) {
uploadEmptyFolders(emptyFolders, uploadToFolder).then(() => {
const onlyFiles = files.filter((f) => !f.isEmptyDirectory);
if (onlyFiles.length > 0) startUpload(onlyFiles, uploadToFolder, t);
});
} else {
startUpload(files, uploadToFolder, t);
}
};
showOperationToast = (type, qty, title) => {
@ -395,6 +410,7 @@ export default inject(
treeFoldersStore,
mediaViewerDataStore,
settingsStore,
filesActionsStore,
}) => {
const {
secondaryProgressDataStore,
@ -453,6 +469,8 @@ export default inject(
converted,
} = uploadDataStore;
const { uploadEmptyFolders } = filesActionsStore;
const selectionLength = isProgressFinished ? selection.length : null;
const selectionTitle = isProgressFinished
? filesStore.selectionTitle
@ -511,6 +529,7 @@ export default inject(
setUploadPanelVisible,
setSelections,
startUpload,
uploadEmptyFolders,
isHeaderVisible: auth.settingsStore.isHeaderVisible,
setHeaderVisible: auth.settingsStore.setHeaderVisible,
personal: auth.settingsStore.personal,

View File

@ -10,6 +10,7 @@ import {
markAsRead,
removeFiles,
removeShareFiles,
createFolder,
} from "@appserver/common/api/files";
import {
ConflictResolveType,
@ -108,6 +109,76 @@ class FilesActionStore {
});
};
convertToTree = (folders) => {
let result = [];
let level = { result };
try {
folders.forEach((folder) => {
folder.path
.split("/")
.filter((name) => name !== "")
.reduce((r, name, i, a) => {
if (!r[name]) {
r[name] = { result: [] };
r.result.push({ name, children: r[name].result });
}
return r[name];
}, level);
});
} catch (e) {
console.error("convertToTree", e);
}
return result;
};
createFolderTree = async (treeList, parentFolderId) => {
if (!treeList || !treeList.length) return;
for (let i = 0; i < treeList.length; i++) {
const treeNode = treeList[i];
// console.log(
// `createFolderTree parent id = ${parentFolderId} name '${treeNode.name}': `,
// treeNode.children
// );
const folder = await createFolder(parentFolderId, treeNode.name);
const parentId = folder.id;
if (treeNode.children.length == 0) continue;
await this.createFolderTree(treeNode.children, parentId);
}
};
uploadEmptyFolders = async (emptyFolders, folderId) => {
//console.log("uploadEmptyFolders", emptyFolders, folderId);
const { secondaryProgressDataStore } = this.uploadDataStore;
const {
setSecondaryProgressBarData,
clearSecondaryProgressData,
} = secondaryProgressDataStore;
const toFolderId = folderId ? folderId : this.selectedFolderStore.id;
setSecondaryProgressBarData({
icon: "file",
visible: true,
percent: 0,
label: "",
alert: false,
});
const tree = this.convertToTree(emptyFolders);
await this.createFolderTree(tree, toFolderId);
this.updateCurrentFolder(null, [folderId]);
setTimeout(() => clearSecondaryProgressData(), TIMEOUT);
};
deleteAction = async (
translations,
newSelection = null,

View File

@ -16713,7 +16713,7 @@ react-draggable@^4.4.3:
clsx "^1.1.1"
prop-types "^15.6.0"
react-dropzone@^11.2.4:
react-dropzone@^11.4.2:
version "11.7.1"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.7.1.tgz#3851bb75b26af0bf1b17ce1449fd980e643b9356"
integrity sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ==