+
diff --git a/packages/web-app-files/src/components/AppBar/Upload/FileUpload.vue b/packages/web-app-files/src/components/AppBar/Upload/FileUpload.vue
index 3303719fdf9..1a22d1217a0 100644
--- a/packages/web-app-files/src/components/AppBar/Upload/FileUpload.vue
+++ b/packages/web-app-files/src/components/AppBar/Upload/FileUpload.vue
@@ -1,46 +1,43 @@
+ publicLinkPassword?: Ref
+}) => {
+ return async (files: UppyResource[]) => {
+ const { owncloudSdk: client } = clientService
+ const createdFolders = []
+ for (const file of files) {
+ const currentFolder = file.meta.currentFolder
+ const directory = file.meta.relativeFolder
+
+ if (!directory || createdFolders.includes(directory)) {
+ continue
+ }
+
+ const folders = directory.split('/')
+ let createdSubFolders = ''
+ for (const subFolder of folders) {
+ if (!subFolder) {
+ continue
+ }
+
+ const folderToCreate = `${createdSubFolders}/${subFolder}`
+ if (createdFolders.includes(folderToCreate)) {
+ createdSubFolders += `/${subFolder}`
+ createdFolders.push(createdSubFolders)
+ continue
+ }
+
+ if (unref(isPublicLocation)) {
+ await client.publicFiles.createFolder(
+ currentFolder,
+ folderToCreate,
+ unref(publicLinkPassword)
+ )
+ } else {
+ await client.files.createFolder(`${file.meta.webDavBasePath}/${folderToCreate}`)
+ }
+
+ createdSubFolders += `/${subFolder}`
+ createdFolders.push(createdSubFolders)
+ }
+ }
+ }
+}
diff --git a/packages/web-runtime/src/container/bootstrap.ts b/packages/web-runtime/src/container/bootstrap.ts
index f1c3875f0a4..d5f8717b4ef 100644
--- a/packages/web-runtime/src/container/bootstrap.ts
+++ b/packages/web-runtime/src/container/bootstrap.ts
@@ -14,6 +14,7 @@ import { useLocalStorage } from 'web-pkg/src/composables'
import { unref } from '@vue/composition-api'
import { useDefaultThemeName } from '../composables'
import { clientService } from 'web-pkg/src/services'
+import { UppyService } from '../services/uppyService'
/**
* fetch runtime configuration, this step is optional, all later steps can use a static
@@ -249,6 +250,15 @@ export const announceClientService = ({
vue.prototype.$clientService.owncloudSdk = sdk
}
+/**
+ * announce uppyService and owncloud SDK and inject it into vue
+ *
+ * @param vue
+ */
+export const announceUppyService = ({ vue }: { vue: VueConstructor }): void => {
+ vue.prototype.$uppyService = new UppyService()
+}
+
/**
* announce runtime defaults, this is usual the last needed announcement before rendering the actual ui
*
diff --git a/packages/web-runtime/src/defaults/vue.js b/packages/web-runtime/src/defaults/vue.js
index fa86623564f..afe9ff9733d 100644
--- a/packages/web-runtime/src/defaults/vue.js
+++ b/packages/web-runtime/src/defaults/vue.js
@@ -2,7 +2,6 @@ import 'vue-resize/dist/vue-resize.css'
import Vue from 'vue'
import MediaSource from '../plugins/mediaSource.js'
import WebPlugin from '../plugins/web'
-import ChunkedUpload from '../plugins/upload'
import Avatar from '../components/Avatar.vue'
import focusMixin from '../mixins/focusMixin'
import lifecycleMixin from '../mixins/lifecycleMixin'
@@ -13,7 +12,6 @@ import VueResize from 'vue-resize'
import VueMeta from 'vue-meta'
import PortalVue from 'portal-vue'
import AsyncComputed from 'vue-async-computed'
-import { Drag, Drop } from 'vue-drag-drop'
import VueRouter from 'vue-router'
import Vuex from 'vuex'
import VueCompositionAPI from '@vue/composition-api'
@@ -29,12 +27,9 @@ Vue.use(VueResize)
Vue.use(VueMeta, {
refreshOnceOnNavigation: true
})
-Vue.use(ChunkedUpload)
Vue.use(PortalVue)
Vue.use(AsyncComputed)
-Vue.component('drag', Drag)
-Vue.component('drop', Drop)
Vue.component('avatar-image', Avatar)
Vue.mixin(focusMixin)
diff --git a/packages/web-runtime/src/index.ts b/packages/web-runtime/src/index.ts
index 4eb8b8b6b7c..6706b51d4c1 100644
--- a/packages/web-runtime/src/index.ts
+++ b/packages/web-runtime/src/index.ts
@@ -19,12 +19,14 @@ import {
announceTheme,
announceTranslations,
announceVersions,
- applicationStore
+ applicationStore,
+ announceUppyService
} from './container'
export const bootstrap = async (configurationPath: string): Promise => {
const runtimeConfiguration = await requestConfiguration(configurationPath)
announceClientService({ vue: Vue, runtimeConfiguration })
+ announceUppyService({ vue: Vue })
await announceClient(runtimeConfiguration)
await announceStore({ vue: Vue, store, runtimeConfiguration })
await announceApplications({
diff --git a/packages/web-runtime/src/layouts/Application.vue b/packages/web-runtime/src/layouts/Application.vue
index 5880ff5cbcf..548844f9ad3 100644
--- a/packages/web-runtime/src/layouts/Application.vue
+++ b/packages/web-runtime/src/layouts/Application.vue
@@ -18,6 +18,7 @@
/>
+
-
+
- Files
+ {{ btnLabel }}
+
+
diff --git a/packages/web-runtime/src/composables/upload/index.ts b/packages/web-runtime/src/composables/upload/index.ts
new file mode 100644
index 00000000000..7e3692274ba
--- /dev/null
+++ b/packages/web-runtime/src/composables/upload/index.ts
@@ -0,0 +1 @@
+export * from './useUpload'
diff --git a/packages/web-runtime/src/composables/upload/uppyPlugins/customDropTarget.ts b/packages/web-runtime/src/composables/upload/uppyPlugins/customDropTarget.ts
new file mode 100644
index 00000000000..79591547abe
--- /dev/null
+++ b/packages/web-runtime/src/composables/upload/uppyPlugins/customDropTarget.ts
@@ -0,0 +1,39 @@
+import DropTarget from '@uppy/drop-target'
+
+/**
+ * Custom Drop Target plugin
+ *
+ * It adds the possibility to have a custom method for adding files to uppy.
+ */
+export class CustomDropTarget extends DropTarget {
+ addFiles = (files) => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ if (this.opts.uppyService) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ this.opts.uppyService.$emit('filesSelected', files)
+ return
+ }
+
+ const descriptors = files.map((file) => {
+ return {
+ source: this.id,
+ name: file.name,
+ type: file.type,
+ data: file,
+ meta: {
+ relativePath: file.relativePath || null
+ }
+ }
+ })
+
+ try {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ this.uppy.addFiles(descriptors)
+ } catch (err) {
+ this.uppy.log(err)
+ }
+ }
+}
diff --git a/packages/web-runtime/src/composables/upload/uppyPlugins/customTus.ts b/packages/web-runtime/src/composables/upload/uppyPlugins/customTus.ts
new file mode 100644
index 00000000000..049a48affaf
--- /dev/null
+++ b/packages/web-runtime/src/composables/upload/uppyPlugins/customTus.ts
@@ -0,0 +1,14 @@
+import Tus from '@uppy/tus'
+
+export class CustomTus extends Tus {
+ upload(file) {
+ if (file.meta.tusEndpoint) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ this.opts.endpoint = file.meta.tusEndpoint
+ }
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ return super.upload(file)
+ }
+}
diff --git a/packages/web-runtime/src/composables/upload/useUpload.ts b/packages/web-runtime/src/composables/upload/useUpload.ts
new file mode 100644
index 00000000000..b425717a652
--- /dev/null
+++ b/packages/web-runtime/src/composables/upload/useUpload.ts
@@ -0,0 +1,151 @@
+import { Route } from 'vue-router'
+import { ClientService } from 'web-pkg/src/services'
+import {
+ useCapabilityFilesTusSupportHttpMethodOverride,
+ useCapabilityFilesTusSupportMaxChunkSize,
+ useClientService,
+ useStore
+} from 'web-pkg/src/composables'
+import { computed, Ref, unref, watch } from '@vue/composition-api'
+import { useActiveLocation } from 'files/src/composables'
+import { isLocationPublicActive } from 'files/src/router'
+import { UppyService } from '../../services/uppyService'
+
+export interface UppyResource {
+ id?: string
+ source: string
+ name: string
+ type: string
+ data: Blob
+ meta: {
+ currentFolder: string
+ relativeFolder: string
+ relativePath: string
+ route: Route
+ tusEndpoint: string
+ webDavBasePath: string
+ }
+}
+
+interface UploadOptions {
+ uppyService: UppyService
+}
+
+interface UploadResult {
+ createDirectoryTree(files: UppyResource[]): void
+}
+
+export function useUpload(options: UploadOptions): UploadResult {
+ const store = useStore()
+ const publicLinkPassword = computed((): string => store.getters['Files/publicLinkPassword'])
+ const isPublicLocation = useActiveLocation(isLocationPublicActive, 'files-public-files')
+ const isPublicDropLocation = useActiveLocation(isLocationPublicActive, 'files-public-drop')
+ const clientService = useClientService()
+ const getToken = computed((): string => store.getters.getToken)
+
+ const tusHttpMethodOverride = useCapabilityFilesTusSupportHttpMethodOverride()
+ const tusMaxChunkSize = useCapabilityFilesTusSupportMaxChunkSize()
+ const uploadChunkSize = computed((): number => store.getters.configuration.uploadChunkSize)
+
+ const headers = computed((): { [key: string]: string } => {
+ if (unref(isPublicLocation) || unref(isPublicDropLocation)) {
+ const password = unref(publicLinkPassword)
+ if (password) {
+ return { Authorization: 'Basic ' + Buffer.from('public:' + password).toString('base64') }
+ }
+
+ return {}
+ }
+ return {
+ Authorization: 'Bearer ' + unref(getToken)
+ }
+ })
+
+ const uppyOptions = computed(() => {
+ const isTusSupported = unref(tusMaxChunkSize) > 0
+
+ if (isTusSupported) {
+ return {
+ isTusSupported,
+ tusMaxChunkSize: unref(tusMaxChunkSize),
+ uploadChunkSize: unref(uploadChunkSize),
+ tusHttpMethodOverride: unref(tusHttpMethodOverride),
+ headers: unref(headers)
+ }
+ }
+
+ return { isTusSupported, headers: unref(headers) }
+ })
+
+ watch(
+ uppyOptions,
+ () => {
+ // @TODO use Tus once the backend supports it on password protected links
+ if (unref(uppyOptions).isTusSupported && !unref(publicLinkPassword)) {
+ options.uppyService.useTus(unref(uppyOptions) as any)
+ return
+ }
+ options.uppyService.useXhr(unref(uppyOptions) as any)
+ },
+ { immediate: true }
+ )
+
+ return {
+ createDirectoryTree: createDirectoryTree({
+ clientService,
+ isPublicLocation,
+ publicLinkPassword
+ })
+ }
+}
+
+const createDirectoryTree = ({
+ clientService,
+ isPublicLocation,
+ publicLinkPassword
+}: {
+ clientService: ClientService
+ isPublicLocation: Ref