UBERF-7632 Upload folders to drive (#6104)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-07-22 22:56:53 +07:00 committed by GitHub
parent fe12d4930f
commit 434163f00a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 229 additions and 83 deletions

View File

@ -142,7 +142,7 @@
list,
{ objectId: object._id, objectClass: object._class },
{},
async (uuid, name, file, metadata) => {
async (uuid, name, file, path, metadata) => {
await createAttachment(uuid, name, file, metadata)
}
)
@ -158,7 +158,7 @@
files,
{ objectId: object._id, objectClass: object._class },
{},
async (uuid, name, file, metadata) => {
async (uuid, name, file, path, metadata) => {
await createAttachment(uuid, name, file, metadata)
}
)

View File

@ -15,13 +15,14 @@
//
-->
<script lang="ts">
import core, { Ref, generateId } from '@hcengineering/core'
import { Drive, Folder } from '@hcengineering/drive'
import core, { Data, Ref } from '@hcengineering/core'
import { type Drive, type Folder, createFolder } from '@hcengineering/drive'
import { Card, SpaceSelector, getClient } from '@hcengineering/presentation'
import { EditBox, FocusHandler, createFocusManager } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { ObjectBox } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import drive from '../plugin'
export function canClose (): boolean {
@ -31,8 +32,6 @@
export let space: Ref<Drive> | undefined
export let parent: Ref<Folder> | undefined
const id: Ref<Folder> = generateId()
const dispatch = createEventDispatcher()
const client = getClient()
@ -52,26 +51,12 @@
return
}
let path: Ref<Folder>[] = []
if (_parent != null && _parent !== drive.ids.Root) {
const parent = await client.findOne(drive.class.Folder, { _id: _parent })
if (parent === undefined) {
throw new Error('parent not found')
}
path = [parent._id, ...parent.path]
const data: Omit<Data<Folder>, 'path'> = {
name: getTitle(name),
parent: _parent ?? drive.ids.Root
}
await client.createDoc(
drive.class.Folder,
_space,
{
name: getTitle(name),
parent: _parent ?? drive.ids.Root,
path
},
id
)
const id = await createFolder(client, _space, data)
dispatch('close', id)
}

View File

@ -22,7 +22,7 @@
import drive from '../plugin'
import { getFolderIdFromFragment } from '../navigation'
import { createDrive, createFolder } from '../utils'
import { showCreateDrivePopup, showCreateFolderPopup } from '../utils'
export let currentSpace: Ref<Drive> | undefined
export let currentFragment: string | undefined
@ -57,11 +57,11 @@
}
async function handleCreateDrive (): Promise<void> {
await createDrive()
await showCreateDrivePopup()
}
async function handleCreateFolder (): Promise<void> {
await createFolder(currentSpace, parent, true)
await showCreateFolderPopup(currentSpace, parent, true)
}
async function handleUploadFile (): Promise<void> {
@ -71,7 +71,7 @@
parent !== drive.ids.Root
? { objectId: parent, objectClass: drive.class.Folder }
: { objectId: space, objectClass: drive.class.Drive }
await showFilesUploadPopup(target, {}, async (uuid, name, file, metadata) => {
await showFilesUploadPopup(target, {}, async (uuid, name, file, path, metadata) => {
try {
const data = {
file: uuid,

View File

@ -14,17 +14,13 @@
-->
<script lang="ts">
import { type Ref } from '@hcengineering/core'
import drive, { createFile, type Drive, type Folder } from '@hcengineering/drive'
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { uploadFiles } from '@hcengineering/uploader'
import { type Drive, type Folder } from '@hcengineering/drive'
import { uploadFilesToDrive } from '../utils'
export let space: Ref<Drive>
export let parent: Ref<Folder>
export let canDrop: ((e: DragEvent) => boolean) | undefined = undefined
const client = getClient()
let dragover = false
let counter = 0
@ -66,28 +62,8 @@
e.preventDefault()
e.stopPropagation()
const list = e.dataTransfer?.files
if (list !== undefined && list.length !== 0) {
const target =
parent !== drive.ids.Root
? { objectId: parent, objectClass: drive.class.Folder }
: { objectId: space, objectClass: drive.class.Drive }
await uploadFiles(list, target, {}, async (uuid, name, file, metadata) => {
try {
const data = {
file: uuid,
size: file.size,
type: file.type,
lastModified: file instanceof File ? file.lastModified : Date.now(),
name,
metadata
}
await createFile(client, space, parent, data)
} catch (err) {
void setPlatformStatus(unknownError(err))
}
})
if (e.dataTransfer != null) {
await uploadFilesToDrive(e.dataTransfer, space, parent)
}
}
</script>

View File

@ -74,7 +74,7 @@
maxNumberOfFiles: 1,
hideProgress: true
},
async (uuid, name, file, metadata) => {
async (uuid, name, file, path, metadata) => {
const data = {
file: uuid,
name,

View File

@ -38,14 +38,14 @@ import MoveResource from './components/MoveResource.svelte'
import ResourcePresenter from './components/ResourcePresenter.svelte'
import { getDriveLink, getFileLink, getFolderLink, resolveLocation } from './navigation'
import { createFolder, renameResource, restoreFileVersion } from './utils'
import { showCreateFolderPopup, showRenameResourcePopup, restoreFileVersion } from './utils'
async function CreateRootFolder (doc: Drive): Promise<void> {
await createFolder(doc._id, drive.ids.Root)
await showCreateFolderPopup(doc._id, drive.ids.Root)
}
async function CreateChildFolder (doc: Folder): Promise<void> {
await createFolder(doc.space, doc._id)
await showCreateFolderPopup(doc.space, doc._id)
}
async function EditDrive (drive: Drive): Promise<void> {
@ -82,13 +82,13 @@ async function FileLinkProvider (doc: Doc): Promise<Location> {
async function RenameFile (doc: File | File[]): Promise<void> {
if (!Array.isArray(doc)) {
await renameResource(doc)
await showRenameResourcePopup(doc)
}
}
async function RenameFolder (doc: Folder | Folder[]): Promise<void> {
if (!Array.isArray(doc)) {
await renameResource(doc)
await showRenameResourcePopup(doc)
}
}

View File

@ -14,11 +14,12 @@
//
import { type Class, type Doc, type Ref, toIdMap } from '@hcengineering/core'
import type { Drive, FileVersion, Folder, Resource } from '@hcengineering/drive'
import drive from '@hcengineering/drive'
import { type Asset } from '@hcengineering/platform'
import { type Drive, type FileVersion, type Folder, type Resource, createFolder } from '@hcengineering/drive'
import drive, { createFile } from '@hcengineering/drive'
import { type Asset, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { type AnySvelteComponent, showPopup } from '@hcengineering/ui'
import { uploadFiles } from '@hcengineering/uploader'
import { openDoc } from '@hcengineering/view-resources'
import CreateDrive from './components/CreateDrive.svelte'
@ -43,7 +44,11 @@ export function formatFileVersion (version: number): string {
return `v${version}`
}
export async function createFolder (space: Ref<Drive> | undefined, parent: Ref<Folder>, open = false): Promise<void> {
export async function showCreateFolderPopup (
space: Ref<Drive> | undefined,
parent: Ref<Folder>,
open = false
): Promise<void> {
showPopup(CreateFolder, { space, parent }, 'top', async (id) => {
if (open && id !== undefined && id !== null) {
await navigateToDoc(id, drive.class.Folder)
@ -51,7 +56,7 @@ export async function createFolder (space: Ref<Drive> | undefined, parent: Ref<F
})
}
export async function createDrive (open = false): Promise<void> {
export async function showCreateDrivePopup (open = false): Promise<void> {
showPopup(CreateDrive, {}, 'top', async (id) => {
if (open && id !== undefined && id !== null) {
await navigateToDoc(id, drive.class.Folder)
@ -59,11 +64,11 @@ export async function createDrive (open = false): Promise<void> {
})
}
export async function editDrive (drive: Drive): Promise<void> {
export async function showEditDrivePopup (drive: Drive): Promise<void> {
showPopup(CreateDrive, { drive })
}
export async function renameResource (resource: Resource): Promise<void> {
export async function showRenameResourcePopup (resource: Resource): Promise<void> {
showPopup(RenamePopup, { value: resource.name, format: 'text' }, undefined, async (res) => {
if (res != null && res !== resource.name) {
const client = getClient()
@ -148,3 +153,63 @@ export async function resolveParents (object: Resource): Promise<Doc[]> {
return parents.reverse()
}
export async function uploadFilesToDrive (files: DataTransfer, space: Ref<Drive>, parent: Ref<Folder>): Promise<void> {
const client = getClient()
const query = parent !== drive.ids.Root ? { space, path: parent } : { space }
const folders = await client.findAll(drive.class.Folder, query)
const foldersByName = new Map(folders.map((folder) => [folder.name, folder]))
const findParent = async (path: string | undefined): Promise<Ref<Folder>> => {
if (path == null || path.length === 0) {
return parent
}
const segments = path.split('/').filter((p) => p.length > 0)
if (segments.length <= 1) {
return parent
}
let current = parent
while (segments.length > 1) {
const name = segments.shift()
if (name !== undefined) {
let folder = foldersByName.get(name)
if (folder !== undefined) {
current = folder._id
} else {
current = await createFolder(client, space, { name, parent: current })
folder = await client.findOne(drive.class.Folder, { _id: current })
if (folder !== undefined) {
foldersByName.set(folder.name, folder)
}
}
}
}
return current
}
const target =
parent !== drive.ids.Root
? { objectId: parent, objectClass: drive.class.Folder }
: { objectId: space, objectClass: drive.class.Drive }
await uploadFiles(files, target, {}, async (uuid, name, file, path, metadata) => {
const folder = await findParent(path)
try {
const data = {
file: uuid,
size: file.size,
type: file.type,
lastModified: file instanceof File ? file.lastModified : Date.now(),
name,
metadata
}
await createFile(client, space, folder, data)
} catch (err) {
void setPlatformStatus(unknownError(err))
}
})
}

View File

@ -13,11 +13,30 @@
// limitations under the License.
//
import { type AttachedData, type Ref, type TxOperations, generateId } from '@hcengineering/core'
import { type AttachedData, type Data, type Ref, type TxOperations, generateId } from '@hcengineering/core'
import drive from './plugin'
import type { Drive, File, FileVersion, Folder } from './types'
/** @public */
export async function createFolder (
client: TxOperations,
space: Ref<Drive>,
data: Omit<Data<Folder>, 'path'>
): Promise<Ref<Folder>> {
let path: Array<Ref<Folder>> = []
if (data.parent !== drive.ids.Root) {
const parent = await client.findOne(drive.class.Folder, { _id: data.parent })
if (parent === undefined) {
throw new Error('parent not found')
}
path = [parent._id, ...parent.path]
}
return await client.createDoc(drive.class.Folder, space, { ...data, path })
}
/** @public */
export async function createFile (
client: TxOperations,

View File

@ -49,7 +49,9 @@ type Meta = IndexedObject<any>
type Body = IndexedObject<any>
/** @public */
export type UppyMeta = Meta
export type UppyMeta = Meta & {
relativePath?: string
}
/** @public */
export type UppyBody = Body & {
@ -89,7 +91,7 @@ export function getUppy (options: FileUploadOptions, onFileUploaded?: FileUpload
const uuid = file?.response?.body?.uuid as Ref<Blob>
if (uuid !== undefined) {
const metadata = await getFileMetadata(file.data, uuid)
await onFileUploaded(uuid, file.name, file.data, metadata)
await onFileUploaded(uuid, file.name, file.data, file.meta.relativePath, metadata)
}
}
})

View File

@ -14,7 +14,13 @@
//
import { showPopup } from '@hcengineering/ui'
import type { FileUploadCallback, FileUploadOptions, FileUploadTarget } from '@hcengineering/uploader'
import {
type FileUploadCallback,
type FileUploadOptions,
type FileUploadTarget,
getDataTransferFiles,
toFileWithPath
} from '@hcengineering/uploader'
import FileUploadPopup from './components/FileUploadPopup.svelte'
@ -38,19 +44,21 @@ export async function showFilesUploadPopup (
/** @public */
export async function uploadFiles (
files: File[] | FileList,
files: File[] | FileList | DataTransfer,
target: FileUploadTarget,
options: FileUploadOptions,
onFileUploaded: FileUploadCallback
): Promise<void> {
if (files.length === 0) return
const items =
files instanceof DataTransfer ? await getDataTransferFiles(files) : Array.from(files, (p) => toFileWithPath(p))
if (items.length === 0) return
const uppy = getUppy(options, onFileUploaded)
for (let index = 0; index < files.length; index++) {
const data = files[index]
const { name, type } = data
uppy.addFile({ name, type, data })
for (const data of items) {
const { name, type, relativePath } = data
uppy.addFile({ name, type, data, meta: { relativePath } })
}
if (options.hideProgress !== true) {

View File

@ -15,6 +15,11 @@
import type { Blob as PlatformBlob, Class, Doc, Ref } from '@hcengineering/core'
/** @public */
export interface FileWithPath extends File {
relativePath?: string
}
/** @public */
export type UploadFilesPopupFn = (
target: FileUploadTarget,
@ -24,7 +29,7 @@ export type UploadFilesPopupFn = (
/** @public */
export type UploadFilesFn = (
files: File[] | FileList,
files: File[] | FileList | DataTransfer,
target: FileUploadTarget,
options: FileUploadOptions,
onFileUploaded: FileUploadCallback
@ -48,6 +53,7 @@ export interface FileUploadOptions {
export type FileUploadCallback = (
uuid: Ref<PlatformBlob>,
name: string,
file: File | Blob,
file: FileWithPath | Blob,
path: string | undefined,
metadata: Record<string, any> | undefined
) => Promise<void>

View File

@ -16,7 +16,7 @@
import { getResource } from '@hcengineering/platform'
import uploader from './plugin'
import type { FileUploadCallback, FileUploadOptions, FileUploadTarget } from './types'
import type { FileUploadCallback, FileUploadOptions, FileUploadTarget, FileWithPath } from './types'
/** @public */
export async function showFilesUploadPopup (
@ -30,7 +30,7 @@ export async function showFilesUploadPopup (
/** @public */
export async function uploadFiles (
files: File[] | FileList,
files: File[] | FileList | DataTransfer,
target: FileUploadTarget,
options: FileUploadOptions,
onFileUploaded: FileUploadCallback
@ -38,3 +38,88 @@ export async function uploadFiles (
const fn = await getResource(uploader.function.UploadFiles)
await fn(files, target, options, onFileUploaded)
}
/** @public */
export async function getDataTransferFiles (dataTransfer: DataTransfer): Promise<FileWithPath[]> {
try {
const accumulator = []
const entries = Array.from(dataTransfer.items, getAsEntry)
for (const entry of entries) {
if (entry != null) {
const files = await fromEntry(entry)
if (Array.isArray(files)) {
accumulator.push(...files)
} else {
accumulator.push(files)
}
}
}
return accumulator
} catch {
return Array.from(dataTransfer.files, (file) => toFileWithPath(file))
}
}
/** @public */
export function toFileWithPath (file: File, path?: string): FileWithPath {
const { webkitRelativePath } = file
Object.defineProperty(file, 'relativePath', {
value:
typeof path === 'string'
? path
: typeof webkitRelativePath === 'string' && webkitRelativePath.length > 0
? webkitRelativePath
: file.name,
writable: false,
configurable: false,
enumerable: true
})
return file
}
function getAsEntry (item: DataTransferItem): FileSystemEntry | null | undefined {
// https://developer.mozilla.org/docs/Web/API/DataTransferItem/webkitGetAsEntry
return (item as any).getAsEntry === 'function' ? (item as any).getAsEntry() : item.webkitGetAsEntry()
}
async function fromEntry (entry: FileSystemEntry): Promise<FileWithPath | FileWithPath[]> {
return entry.isDirectory
? await fromDirEntry(entry as FileSystemDirectoryEntry)
: await fromFileEntry(entry as FileSystemFileEntry)
}
async function fromFileEntry (entry: FileSystemFileEntry): Promise<FileWithPath> {
return await new Promise((resolve, reject) => {
entry.file((file) => {
resolve(toFileWithPath(file, entry.fullPath))
}, reject)
})
}
async function fromDirEntry (entry: FileSystemDirectoryEntry): Promise<FileWithPath | FileWithPath[]> {
const reader = entry.createReader()
return await new Promise((resolve, reject) => {
const promises: Promise<File | File[]>[] = []
function readEntries (): void {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
reader.readEntries(async (entries: FileSystemEntry[]) => {
if (entries.length === 0) {
try {
const files = await Promise.all(promises)
resolve(files.flat())
} catch (err) {
reject(err)
}
} else {
// https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryReader/readEntries
promises.push(...entries.map(fromEntry))
readEntries()
}
}, reject)
}
readEntries()
})
}