Kaynağa Gözat

Add @uppy/webdav (#5551)

Co-authored-by: Dominik Schmidt <dev@dominik-schmidt.de>
Merlijn Vos 4 ay önce
ebeveyn
işleme
9164ad5feb

+ 1 - 0
packages/@uppy/companion/package.json

@@ -65,6 +65,7 @@
     "supports-color": "8.x",
     "tus-js-client": "^4.1.0",
     "validator": "^13.0.0",
+    "webdav": "5.7.1",
     "ws": "8.17.1"
   },
   "devDependencies": {

+ 1 - 1
packages/@uppy/companion/src/server/controllers/get.js

@@ -8,7 +8,7 @@ async function get (req, res) {
   const { provider } = req.companion
 
   async function getSize () {
-    return provider.size({ id, token: accessToken, query: req.query })
+    return provider.size({ id, token: accessToken, providerUserSession, query: req.query })
   }
 
   const download = () => provider.download({ id, token: accessToken, providerUserSession, query: req.query })

+ 2 - 1
packages/@uppy/companion/src/server/provider/index.js

@@ -9,6 +9,7 @@ const instagram = require('./instagram/graph')
 const facebook = require('./facebook')
 const onedrive = require('./onedrive')
 const unsplash = require('./unsplash')
+const webdav = require('./webdav')
 const zoom = require('./zoom')
 const { getURLBuilder } = require('../helpers/utils')
 const logger = require('../logger')
@@ -68,7 +69,7 @@ module.exports.getProviderMiddleware = (providers, grantConfig) => {
  * @returns {Record<string, typeof Provider>}
  */
 module.exports.getDefaultProviders = () => {
-  const providers = { dropbox, box, drive: Drive, googlephotos, facebook, onedrive, zoom, instagram, unsplash }
+  const providers = { dropbox, box, drive: Drive, googlephotos, facebook, onedrive, zoom, instagram, unsplash, webdav }
 
   return providers
 }

+ 182 - 0
packages/@uppy/companion/src/server/provider/webdav/index.js

@@ -0,0 +1,182 @@
+
+const Provider = require('../Provider')
+const { getProtectedHttpAgent, validateURL } = require('../../helpers/request')
+const { ProviderApiError, ProviderAuthError } = require('../error')
+const { ProviderUserError } = require('../error')
+const logger = require('../../logger')
+
+const defaultDirectory = '/'
+
+/**
+ * Adapter for WebDAV servers that support simple auth (non-OAuth).
+ */
+class WebdavProvider extends Provider {
+  static get hasSimpleAuth () {
+    return true
+  }
+
+  // eslint-disable-next-line class-methods-use-this
+  isAuthenticated ({ providerUserSession }) {
+    return providerUserSession.webdavUrl != null
+  }
+
+  async getClient ({ providerUserSession }) {
+    const webdavUrl = providerUserSession?.webdavUrl
+    const { allowLocalUrls } = this
+    if (!validateURL(webdavUrl, allowLocalUrls)) {
+      throw new Error('invalid public link url')
+    }
+
+    // dynamic import because Companion currently uses CommonJS and webdav is shipped as ESM
+    // todo implement as regular require as soon as Node 20.17 or 22 is required
+    // or as regular import when Companion is ported to ESM
+    const { AuthType } = await import('webdav') // eslint-disable-line import/no-unresolved
+
+    // Is this an ownCloud or Nextcloud public link URL? e.g. https://example.com/s/kFy9Lek5sm928xP
+    // they have specific urls that we can identify
+    // todo not sure if this is the right way to support nextcloud and other webdavs
+    if (/\/s\/([^/]+)/.test(webdavUrl)) {
+      const [baseURL, publicLinkToken] = webdavUrl.split('/s/')
+
+      return this.getClientHelper({
+        url: `${baseURL.replace('/index.php', '')}/public.php/webdav/`,
+        authType: AuthType.Password,
+        username: publicLinkToken,
+        password: 'null',
+      })
+    }
+
+    // normal public WebDAV urls
+    return this.getClientHelper({
+      url: webdavUrl,
+      authType: AuthType.None,
+    })
+  }
+
+  async logout () { // eslint-disable-line class-methods-use-this
+    return { revoked: true }
+  }
+
+  async simpleAuth ({ requestBody }) {
+    try {
+      const providerUserSession = { webdavUrl: requestBody.form.webdavUrl }
+
+      const client = await this.getClient({ providerUserSession })
+      // call the list operation as a way to validate the url
+      await client.getDirectoryContents(defaultDirectory)
+
+      return providerUserSession
+    } catch (err) {
+      logger.error(err, 'provider.webdav.error')
+      if (['ECONNREFUSED', 'ENOTFOUND'].includes(err.code)) {
+        throw new ProviderUserError({ message: 'Cannot connect to server' })
+      }
+      // todo report back to the user what actually went wrong
+      throw err
+    }
+  }
+
+  async getClientHelper ({ url, ...options }) {
+    const { allowLocalUrls } = this
+    if (!validateURL(url, allowLocalUrls)) {
+      throw new Error('invalid webdav url')
+    }
+    const { protocol } = new URL(url)
+    const HttpAgentClass = getProtectedHttpAgent({ protocol, allowLocalIPs: !allowLocalUrls })
+
+    // dynamic import because Companion currently uses CommonJS and webdav is shipped as ESM
+    // todo implement as regular require as soon as Node 20.17 or 22 is required
+    // or as regular import when Companion is ported to ESM
+    const { createClient } = await import('webdav')
+    return createClient(url, {
+      ...options,
+      [`${protocol}Agent`] : new HttpAgentClass(),
+    })
+  }
+
+  async list ({ directory, providerUserSession }) {
+    return this.withErrorHandling('provider.webdav.list.error', async () => {
+      // @ts-ignore
+      if (!this.isAuthenticated({ providerUserSession })) {
+        throw new ProviderAuthError()
+      }
+
+      const data = { items: [] }
+      const client = await this.getClient({ providerUserSession })
+
+      /** @type {any} */
+      const dir = await client.getDirectoryContents(directory || '/')
+
+      dir.forEach(item => {
+        const isFolder = item.type === 'directory'
+        const requestPath = encodeURIComponent(`${directory || ''}/${item.basename}`)
+
+        let modifiedDate
+        try {
+         modifiedDate = new Date(item.lastmod).toISOString()
+        } catch (e) {
+          // ignore invalid date from server
+        }
+
+        data.items.push({
+          isFolder,
+          id: requestPath,
+          name: item.basename,
+          modifiedDate,
+          requestPath,
+          ...(!isFolder && {
+            mimeType: item.mime,
+            size: item.size,
+            thumbnail: null,
+
+          }),
+        })
+      })
+
+      return data
+    })
+  }
+
+  async download ({ id, providerUserSession }) {
+    return this.withErrorHandling('provider.webdav.download.error', async () => {
+      const client = await this.getClient({ providerUserSession })
+      const stream = client.createReadStream(`/${id}`)
+      return { stream }
+    })
+  }
+
+  // eslint-disable-next-line
+  async thumbnail ({ id, providerUserSession }) {
+    // not implementing this because a public thumbnail from webdav will be used instead
+    logger.error('call to thumbnail is not implemented', 'provider.webdav.thumbnail.error')
+    throw new Error('call to thumbnail is not implemented')
+  }
+
+  // eslint-disable-next-line
+  async size ({ id, token, providerUserSession }) {
+    return this.withErrorHandling('provider.webdav.size.error', async () => {
+      const client = await this.getClient({ providerUserSession })
+
+      /** @type {any} */
+      const stat = await client.stat(id)
+      return stat.size
+    })
+  }
+
+  // eslint-disable-next-line class-methods-use-this
+  async withErrorHandling (tag, fn) {
+    try {
+      return await fn()
+    } catch (err) {
+      let err2 = err
+      if (err.status === 401) err2 = new ProviderAuthError()
+      if (err.response) {
+        err2 = new ProviderApiError('WebDAV API error', err.status) // todo improve (read err?.response?.body readable stream and parse response)
+      }
+      logger.error(err2, tag)
+      throw err2
+    }
+  }
+}
+
+module.exports = WebdavProvider

+ 4 - 0
packages/@uppy/locales/src/en_US.ts

@@ -35,6 +35,7 @@ en_US.strings = {
   aspectRatioPortrait: 'Crop portrait (9:16)',
   aspectRatioSquare: 'Crop square',
   authAborted: 'Authentication aborted',
+  authenticate: 'Connect',
   authenticateWith: 'Connect to %{pluginName}',
   authenticateWithTitle:
     'Please authenticate with %{pluginName} to select files',
@@ -148,7 +149,10 @@ en_US.strings = {
   pluginNameScreenCapture: 'Screencast',
   pluginNameUnsplash: 'Unsplash',
   pluginNameUrl: 'Link',
+  pluginNameWebdav: 'WebDAV',
   pluginNameZoom: 'Zoom',
+  pluginWebdavInputLabel:
+    'WebDAV URL for a file (e.g. from ownCloud or Nextcloud)',
   poweredBy: 'Powered by %{uppy}',
   processingXFiles: {
     '0': 'Processing %{smart_count} file',

+ 1 - 1
packages/@uppy/provider-views/src/GooglePicker/GooglePickerView.tsx

@@ -226,7 +226,7 @@ export default function GooglePickerView({
         }
         pluginIcon={pickerType === 'drive' ? GoogleDriveIcon : GooglePhotosIcon}
         handleAuth={showPicker}
-        i18n={uppy.i18nArray}
+        i18n={uppy.i18n}
         loading={loading}
       />
     )

+ 5 - 7
packages/@uppy/provider-views/src/ProviderView/AuthView.tsx

@@ -1,7 +1,7 @@
 import { h } from 'preact'
 import { useCallback } from 'preact/hooks'
 import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
-import type Translator from '@uppy/utils/lib/Translator'
+import type { I18n } from '@uppy/utils/lib/Translator'
 import type { Opts } from './ProviderView.ts'
 import type ProviderViews from './ProviderView.ts'
 
@@ -9,7 +9,7 @@ type AuthViewProps<M extends Meta, B extends Body> = {
   loading: boolean | string
   pluginName: string
   pluginIcon: () => h.JSX.Element
-  i18n: Translator['translateArray']
+  i18n: I18n
   handleAuth: ProviderViews<M, B>['handleAuth']
   renderForm?: Opts<M, B>['renderAuthForm']
 }
@@ -56,7 +56,7 @@ function DefaultForm<M extends Meta, B extends Body>({
   onAuth,
 }: {
   pluginName: string
-  i18n: Translator['translateArray']
+  i18n: I18n
   onAuth: AuthViewProps<M, B>['handleAuth']
 }) {
   // In order to comply with Google's brand we need to create a different button
@@ -100,7 +100,7 @@ const defaultRenderForm = ({
   onAuth,
 }: {
   pluginName: string
-  i18n: Translator['translateArray']
+  i18n: I18n
   onAuth: AuthViewProps<Meta, Body>['handleAuth']
 }) => <DefaultForm pluginName={pluginName} i18n={i18n} onAuth={onAuth} />
 
@@ -121,9 +121,7 @@ export default function AuthView<M extends Meta, B extends Body>({
         })}
       </div>
 
-      <div className="uppy-Provider-authForm">
-        {renderForm({ pluginName, i18n, loading, onAuth: handleAuth })}
-      </div>
+      {renderForm({ pluginName, i18n, loading, onAuth: handleAuth })}
     </div>
   )
 }

+ 3 - 3
packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx

@@ -10,10 +10,10 @@ import type {
 } from '@uppy/core/lib/Uppy.js'
 import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
 import type { CompanionFile } from '@uppy/utils/lib/CompanionFile'
-import type Translator from '@uppy/utils/lib/Translator'
 import classNames from 'classnames'
 import type { ValidateableFile } from '@uppy/core/lib/Restricter.js'
 import remoteFileObjToLocal from '@uppy/utils/lib/remoteFileObjToLocal'
+import type { I18n } from '@uppy/utils/lib/Translator'
 import AuthView from './AuthView.tsx'
 import Header from './Header.tsx'
 import Browser from '../Browser.tsx'
@@ -75,7 +75,7 @@ export interface Opts<M extends Meta, B extends Body> {
   loadAllFiles: boolean
   renderAuthForm?: (args: {
     pluginName: string
-    i18n: Translator['translateArray']
+    i18n: I18n
     loading: boolean | string
     onAuth: (authFormData: unknown) => Promise<void>
   }) => h.JSX.Element
@@ -434,7 +434,7 @@ export default class ProviderView<M extends Meta, B extends Body> {
           pluginName={this.plugin.title}
           pluginIcon={pluginIcon}
           handleAuth={this.handleAuth}
-          i18n={this.plugin.uppy.i18nArray}
+          i18n={this.plugin.uppy.i18n}
           renderForm={opts.renderAuthForm}
           loading={loading}
         />

+ 2 - 0
packages/@uppy/provider-views/src/index.ts

@@ -5,4 +5,6 @@ export {
 
 export { default as SearchProviderViews } from './SearchProviderView/index.ts'
 
+export { default as SearchInput } from './SearchInput.tsx'
+
 export { default as GooglePickerView } from './GooglePicker/GooglePickerView.tsx'

+ 35 - 0
packages/@uppy/webdav/package.json

@@ -0,0 +1,35 @@
+{
+  "name": "@uppy/webdav",
+  "description": "Import files from WebDAV into Uppy.",
+  "version": "0.1.0",
+  "license": "MIT",
+  "main": "lib/index.js",
+  "types": "types/index.d.ts",
+  "type": "module",
+  "keywords": [
+    "file uploader",
+    "uppy",
+    "uppy-plugin",
+    "webdav",
+    "provider",
+    "photos",
+    "videos"
+  ],
+  "homepage": "https://uppy.io",
+  "bugs": {
+    "url": "https://github.com/transloadit/uppy/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/transloadit/uppy.git"
+  },
+  "dependencies": {
+    "@uppy/companion-client": "workspace:^",
+    "@uppy/provider-views": "workspace:^",
+    "@uppy/utils": "workspace:^",
+    "preact": "^10.5.13"
+  },
+  "peerDependencies": {
+    "@uppy/core": "workspace:^"
+  }
+}

+ 159 - 0
packages/@uppy/webdav/src/Webdav.tsx

@@ -0,0 +1,159 @@
+import { h, type ComponentChild } from 'preact'
+import { useState, useCallback } from 'preact/hooks'
+
+import {
+  UIPlugin,
+  type Body,
+  type Meta,
+  type UnknownProviderPlugin,
+  type UppyFile,
+} from '@uppy/core'
+import {
+  Provider,
+  tokenStorage,
+  type CompanionPluginOptions,
+} from '@uppy/companion-client'
+import {
+  SearchInput,
+  defaultPickerIcon,
+  ProviderViews,
+} from '@uppy/provider-views'
+
+import type {
+  AsyncStore,
+  UnknownProviderPluginState,
+  Uppy,
+} from '@uppy/core/lib/Uppy.js'
+import type { I18n } from '@uppy/utils/lib/Translator'
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore We don't want TS to generate types for the package.json
+import packageJson from '../package.json'
+import locale from './locale.ts'
+
+class WebdavSimpleAuthProvider<M extends Meta, B extends Body> extends Provider<
+  M,
+  B
+> {
+  async login({
+    authFormData,
+    uppyVersions,
+    signal,
+  }: {
+    uppyVersions: string
+    authFormData: unknown
+    signal: AbortSignal
+  }) {
+    return this.loginSimpleAuth({ uppyVersions, authFormData, signal })
+  }
+
+  async logout<ResBody>(): Promise<ResBody> {
+    await this.removeAuthToken()
+    return {
+      ok: true,
+      revoked: true,
+    } as unknown as ResBody
+  }
+}
+
+const AuthForm = ({
+  i18n,
+  onAuth,
+}: {
+  i18n: I18n
+  onAuth: (arg: { webdavUrl: string }) => void
+}) => {
+  const [webdavUrl, setWebdavUrl] = useState('')
+
+  const onSubmit = useCallback(() => {
+    onAuth({ webdavUrl: webdavUrl.trim() })
+  }, [onAuth, webdavUrl])
+
+  return (
+    <SearchInput
+      searchString={webdavUrl}
+      setSearchString={setWebdavUrl}
+      submitSearchString={onSubmit}
+      inputLabel={i18n('pluginWebdavInputLabel')}
+      buttonLabel={i18n('authenticate')}
+      wrapperClassName="uppy-SearchProvider"
+      inputClassName="uppy-c-textInput uppy-SearchProvider-input"
+      showButton
+      buttonCSSClassName="uppy-SearchProvider-searchButton"
+    />
+  )
+}
+
+export type WebdavOptions = CompanionPluginOptions
+
+export default class Webdav<M extends Meta, B extends Body>
+  extends UIPlugin<WebdavOptions, M, B, UnknownProviderPluginState>
+  implements UnknownProviderPlugin<M, B>
+{
+  static VERSION = packageJson.version
+
+  icon: () => h.JSX.Element = defaultPickerIcon
+
+  provider: Provider<M, B>
+
+  view!: ProviderViews<M, B>
+
+  storage: AsyncStore
+
+  files: UppyFile<M, B>[]
+
+  rootFolderId: string | null = null
+
+  constructor(uppy: Uppy<M, B>, opts: WebdavOptions) {
+    super(uppy, opts)
+    this.id = this.opts.id || 'WebDav'
+    this.type = 'acquirer'
+    this.files = []
+    this.storage = this.opts.storage || tokenStorage
+
+    this.defaultLocale = locale
+
+    this.i18nInit()
+
+    this.title = this.i18n('pluginNameWebdav')
+
+    this.provider = new WebdavSimpleAuthProvider(uppy, {
+      companionUrl: this.opts.companionUrl,
+      companionHeaders: this.opts.companionHeaders,
+      companionKeysParams: this.opts.companionKeysParams,
+      companionCookiesRule: this.opts.companionCookiesRule,
+      provider: 'webdav',
+      pluginId: this.id,
+      supportsRefreshToken: false,
+    })
+
+    // this.onFirstRender = this.onFirstRender.bind(this)
+    this.render = this.render.bind(this)
+  }
+
+  install() {
+    this.view = new ProviderViews(this, {
+      provider: this.provider,
+      viewType: 'list',
+      showTitles: true,
+      showFilter: true,
+      showBreadcrumbs: true,
+      renderAuthForm: ({ i18n, onAuth }) => (
+        <AuthForm onAuth={onAuth} i18n={i18n} />
+      ),
+    })
+
+    const { target } = this.opts
+    if (target) {
+      this.mount(target, this)
+    }
+  }
+
+  uninstall() {
+    this.view.tearDown()
+    this.unmount()
+  }
+
+  render(state: unknown): ComponentChild {
+    return this.view.render(state)
+  }
+}

+ 1 - 0
packages/@uppy/webdav/src/index.ts

@@ -0,0 +1 @@
+export { default } from './Webdav.tsx'

+ 8 - 0
packages/@uppy/webdav/src/locale.ts

@@ -0,0 +1,8 @@
+export default {
+  strings: {
+    pluginNameWebdav: 'WebDAV',
+    authenticate: 'Connect',
+    pluginWebdavInputLabel:
+      'WebDAV URL for a file (e.g. from ownCloud or Nextcloud)',
+  },
+}

+ 33 - 0
packages/@uppy/webdav/tsconfig.build.json

@@ -0,0 +1,33 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "outDir": "./lib",
+    "paths": {
+      "@uppy/utils/lib/*": ["../utils/src/*"],
+      "@uppy/core": ["../core/src/index.js"],
+      "@uppy/core/lib/*": ["../core/src/*"],
+      "@uppy/companion-client": ["../companion-client/src/index.js"],
+      "@uppy/companion-client/lib/*": ["../companion-client/src/*"],
+      "@uppy/provider-views": ["../provider-views/src/index.js"],
+      "@uppy/provider-views/lib/*": ["../provider-views/src/*"]
+    },
+    "resolveJsonModule": false,
+    "rootDir": "./src"
+  },
+  "include": ["./src/**/*.*"],
+  "exclude": ["./src/**/*.test.ts"],
+  "references": [
+    {
+      "path": "../utils/tsconfig.build.json"
+    },
+    {
+      "path": "../core/tsconfig.build.json"
+    },
+    {
+      "path": "../provider-views/tsconfig.build.json"
+    },
+    {
+      "path": "../companion-client/tsconfig.build.json"
+    }
+  ]
+}

+ 31 - 0
packages/@uppy/webdav/tsconfig.json

@@ -0,0 +1,31 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "emitDeclarationOnly": false,
+    "noEmit": true,
+    "paths": {
+      "@uppy/utils/lib/*": ["../utils/src/*"],
+      "@uppy/core": ["../core/src/index.js"],
+      "@uppy/core/lib/*": ["../core/src/*"],
+      "@uppy/companion-client": ["../companion-client/src/index.js"],
+      "@uppy/companion-client/lib/*": ["../companion-client/src/*"],
+      "@uppy/provider-views": ["../provider-views/src/index.js"],
+      "@uppy/provider-views/lib/*": ["../provider-views/src/*"],
+    },
+  },
+  "include": ["./package.json", "./src/**/*.*"],
+  "references": [
+    {
+      "path": "../utils/tsconfig.build.json",
+    },
+    {
+      "path": "../core/tsconfig.build.json",
+    },
+    {
+      "path": "../provider-views/tsconfig.build.json",
+    },
+    {
+      "path": "../companion-client/tsconfig.build.json",
+    },
+  ],
+}

+ 1 - 0
packages/uppy/package.json

@@ -67,6 +67,7 @@
     "@uppy/unsplash": "workspace:^",
     "@uppy/url": "workspace:^",
     "@uppy/webcam": "workspace:^",
+    "@uppy/webdav": "workspace:^",
     "@uppy/xhr-upload": "workspace:^",
     "@uppy/zoom": "workspace:^"
   }

+ 6 - 0
private/dev/Dashboard.js

@@ -15,6 +15,7 @@ import DropTarget from '@uppy/drop-target'
 import Audio from '@uppy/audio'
 import Compressor from '@uppy/compressor'
 import GoogleDrive from '@uppy/google-drive'
+import Webdav from '@uppy/webdav'
 import english from '@uppy/locales/lib/en_US.js'
 import GoogleDrivePicker from '@uppy/google-drive-picker'
 import GooglePhotosPicker from '@uppy/google-photos-picker'
@@ -164,6 +165,11 @@ export default () => {
       showVideoSourceDropdown: true,
       showRecordingLength: true,
     })
+    .use(Webdav, {
+      target: Dashboard,
+      companionUrl: COMPANION_URL,
+      companionAllowedHosts
+    })
     .use(Audio, {
       target: Dashboard,
       showRecordingLength: true,

+ 3 - 0
tsconfig.json

@@ -121,6 +121,9 @@
     {
       "path": "./packages/@uppy/zoom/tsconfig.build.json",
     },
+    {
+      "path": "./packages/@uppy/webdav/tsconfig.build.json",
+    },
     {
       "path": "./packages/uppy/tsconfig.build.json",
     },

+ 203 - 2
yarn.lock

@@ -3100,6 +3100,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@buttercup/fetch@npm:^0.2.1":
+  version: 0.2.1
+  resolution: "@buttercup/fetch@npm:0.2.1"
+  dependencies:
+    node-fetch: "npm:^3.3.0"
+  dependenciesMeta:
+    node-fetch:
+      optional: true
+  checksum: 10/ad2fc2ce964409a44238cca94a93ad3817666c477fd1c1ace914a3c589c638522747545bf53f3a684c10a2edf957134dc2d3fa7effe5a773d1bea2c2bee9dd2c
+  languageName: node
+  linkType: hard
+
 "@cnakazawa/watch@npm:^1.0.3":
   version: 1.0.4
   resolution: "@cnakazawa/watch@npm:1.0.4"
@@ -8765,6 +8777,7 @@ __metadata:
     tus-js-client: "npm:^4.1.0"
     typescript: "npm:~5.4"
     validator: "npm:^13.0.0"
+    webdav: "npm:5.7.1"
     ws: "npm:8.17.1"
   bin:
     companion: ./bin/companion
@@ -9312,6 +9325,19 @@ __metadata:
   languageName: unknown
   linkType: soft
 
+"@uppy/webdav@workspace:^, @uppy/webdav@workspace:packages/@uppy/webdav":
+  version: 0.0.0-use.local
+  resolution: "@uppy/webdav@workspace:packages/@uppy/webdav"
+  dependencies:
+    "@uppy/companion-client": "workspace:^"
+    "@uppy/provider-views": "workspace:^"
+    "@uppy/utils": "workspace:^"
+    preact: "npm:^10.5.13"
+  peerDependencies:
+    "@uppy/core": "workspace:^"
+  languageName: unknown
+  linkType: soft
+
 "@uppy/xhr-upload@workspace:*, @uppy/xhr-upload@workspace:^, @uppy/xhr-upload@workspace:packages/@uppy/xhr-upload":
   version: 0.0.0-use.local
   resolution: "@uppy/xhr-upload@workspace:packages/@uppy/xhr-upload"
@@ -10854,6 +10880,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"base-64@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "base-64@npm:1.0.0"
+  checksum: 10/d10b64a1fc9b2c5a5f39f1ce1e6c9d1c5b249222bbfa3a0604c592d90623caf74419983feadd8a170f27dc0c3389704f72faafa3e645aeb56bfc030c93ff074a
+  languageName: node
+  linkType: hard
+
 "base-x@npm:^3.0.8":
   version: 3.0.9
   resolution: "base-x@npm:3.0.9"
@@ -11256,6 +11289,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"byte-length@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "byte-length@npm:1.0.2"
+  checksum: 10/69e2b00a14a81f675ea9946135c42ee1a1d9f689d5ba1327eb6700fcde2ccacbd09b42f7e514de1d2b763960251d8c790b3d7304a5a1a27b1457e34c129be8c7
+  languageName: node
+  linkType: hard
+
 "bytes@npm:3.0.0":
   version: 3.0.0
   resolution: "bytes@npm:3.0.0"
@@ -11591,6 +11631,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"charenc@npm:0.0.2":
+  version: 0.0.2
+  resolution: "charenc@npm:0.0.2"
+  checksum: 10/81dcadbe57e861d527faf6dd3855dc857395a1c4d6781f4847288ab23cffb7b3ee80d57c15bba7252ffe3e5e8019db767757ee7975663ad2ca0939bb8fcaf2e5
+  languageName: node
+  linkType: hard
+
 "check-error@npm:^1.0.3":
   version: 1.0.3
   resolution: "check-error@npm:1.0.3"
@@ -12546,6 +12593,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"crypt@npm:0.0.2":
+  version: 0.0.2
+  resolution: "crypt@npm:0.0.2"
+  checksum: 10/2c72768de3d28278c7c9ffd81a298b26f87ecdfe94415084f339e6632f089b43fe039f2c93f612bcb5ffe447238373d93b2e8c90894cba6cfb0ac7a74616f8b9
+  languageName: node
+  linkType: hard
+
 "css-declaration-sorter@npm:^7.2.0":
   version: 7.2.0
   resolution: "css-declaration-sorter@npm:7.2.0"
@@ -12873,6 +12927,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"data-uri-to-buffer@npm:^4.0.0":
+  version: 4.0.1
+  resolution: "data-uri-to-buffer@npm:4.0.1"
+  checksum: 10/0d0790b67ffec5302f204c2ccca4494f70b4e2d940fea3d36b09f0bb2b8539c2e86690429eb1f1dc4bcc9e4df0644193073e63d9ee48ac9fce79ec1506e4aa4c
+  languageName: node
+  linkType: hard
+
 "data-urls@npm:^5.0.0":
   version: 5.0.0
   resolution: "data-urls@npm:5.0.0"
@@ -13806,6 +13867,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"entities@npm:^5.0.0":
+  version: 5.0.0
+  resolution: "entities@npm:5.0.0"
+  checksum: 10/d7d1b855abee681e7570c1357c94986662c890890c30601ddaf59b7b2b63fcc225118e784ffa96f6bea39bfa675252831e32af49b1f7ebe5ceabf89d820f2beb
+  languageName: node
+  linkType: hard
+
 "env-paths@npm:^2.2.0, env-paths@npm:^2.2.1":
   version: 2.2.1
   resolution: "env-paths@npm:2.2.1"
@@ -15521,6 +15589,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"fast-xml-parser@npm:^4.4.1":
+  version: 4.5.0
+  resolution: "fast-xml-parser@npm:4.5.0"
+  dependencies:
+    strnum: "npm:^1.0.5"
+  bin:
+    fxparser: src/cli/cli.js
+  checksum: 10/dc9571c10e7b57b5be54bcd2d92f50c446eb42ea5df347d253e94dd14eb99b5300a6d172e840f151e0721933ca2406165a8d9b316a6d777bf0596dc4fe1df756
+  languageName: node
+  linkType: hard
+
 "fastest-levenshtein@npm:^1.0.16":
   version: 1.0.16
   resolution: "fastest-levenshtein@npm:1.0.16"
@@ -15619,6 +15698,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4":
+  version: 3.2.0
+  resolution: "fetch-blob@npm:3.2.0"
+  dependencies:
+    node-domexception: "npm:^1.0.0"
+    web-streams-polyfill: "npm:^3.0.3"
+  checksum: 10/5264ecceb5fdc19eb51d1d0359921f12730941e333019e673e71eb73921146dceabcb0b8f534582be4497312d656508a439ad0f5edeec2b29ab2e10c72a1f86b
+  languageName: node
+  linkType: hard
+
 "figures@npm:^3.2.0":
   version: 3.2.0
   resolution: "figures@npm:3.2.0"
@@ -15955,6 +16044,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"formdata-polyfill@npm:^4.0.10":
+  version: 4.0.10
+  resolution: "formdata-polyfill@npm:4.0.10"
+  dependencies:
+    fetch-blob: "npm:^3.1.2"
+  checksum: 10/9b5001d2edef3c9449ac3f48bd4f8cc92e7d0f2e7c1a5c8ba555ad4e77535cc5cf621fabe49e97f304067037282dd9093b9160a3cb533e46420b446c4e6bc06f
+  languageName: node
+  linkType: hard
+
 "formidable@npm:^2.1.2":
   version: 2.1.2
   resolution: "formidable@npm:2.1.2"
@@ -16835,6 +16933,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"hot-patcher@npm:^2.0.1":
+  version: 2.0.1
+  resolution: "hot-patcher@npm:2.0.1"
+  checksum: 10/c207007dd55609527bcb26450606c714c313ac52af466746d25916f722da2c48fd1257871c07bd07af5ee586f2c1bf8ab59919db66dcea4a2c82b7c51c9a1317
+  languageName: node
+  linkType: hard
+
 "hpack.js@npm:^2.1.6":
   version: 2.1.6
   resolution: "hpack.js@npm:2.1.6"
@@ -17601,7 +17706,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-buffer@npm:^1.1.5":
+"is-buffer@npm:^1.1.5, is-buffer@npm:~1.1.6":
   version: 1.1.6
   resolution: "is-buffer@npm:1.1.6"
   checksum: 10/f63da109e74bbe8947036ed529d43e4ae0c5fcd0909921dce4917ad3ea212c6a87c29f525ba1d17c0858c18331cf1046d4fc69ef59ed26896b25c8288a627133
@@ -19531,6 +19636,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"layerr@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "layerr@npm:3.0.0"
+  checksum: 10/5b9518795ef5d447f2b4c66ebab223b0a4fb37505ba27013920f71691726211298e630750d5fa8b03ddb5ce191d24e26d49fa40e0042193ecd680f15d89311f9
+  languageName: node
+  linkType: hard
+
 "lazy-ass@npm:1.6.0, lazy-ass@npm:^1.6.0":
   version: 1.6.0
   resolution: "lazy-ass@npm:1.6.0"
@@ -20564,6 +20676,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"md5@npm:^2.3.0":
+  version: 2.3.0
+  resolution: "md5@npm:2.3.0"
+  dependencies:
+    charenc: "npm:0.0.2"
+    crypt: "npm:0.0.2"
+    is-buffer: "npm:~1.1.6"
+  checksum: 10/88dce9fb8df1a084c2385726dcc18c7f54e0b64c261b5def7cdfe4928c4ee1cd68695c34108b4fab7ecceb05838c938aa411c6143df9fdc0026c4ddb4e4e72fa
+  languageName: node
+  linkType: hard
+
 "mdast-comment-marker@npm:^2.0.0":
   version: 2.1.2
   resolution: "mdast-comment-marker@npm:2.1.2"
@@ -21985,6 +22108,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"minimatch@npm:^9.0.5":
+  version: 9.0.5
+  resolution: "minimatch@npm:9.0.5"
+  dependencies:
+    brace-expansion: "npm:^2.0.1"
+  checksum: 10/dd6a8927b063aca6d910b119e1f2df6d2ce7d36eab91de83167dd136bb85e1ebff97b0d3de1cb08bd1f7e018ca170b4962479fefab5b2a69e2ae12cb2edc8348
+  languageName: node
+  linkType: hard
+
 "minimatch@npm:~3.0.2":
   version: 3.0.8
   resolution: "minimatch@npm:3.0.8"
@@ -22409,6 +22541,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"nested-property@npm:^4.0.0":
+  version: 4.0.0
+  resolution: "nested-property@npm:4.0.0"
+  checksum: 10/5653a67d68e19ed045d571b44f1a84ee07f8fd03f314e5876783d17e600d8799c51ff3f6e93892427ab011b81f885300126f231f2d6c7bb192234e6d97bbe245
+  languageName: node
+  linkType: hard
+
 "ng-packagr@npm:^18.0.0":
   version: 18.0.0
   resolution: "ng-packagr@npm:18.0.0"
@@ -22577,6 +22716,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"node-domexception@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "node-domexception@npm:1.0.0"
+  checksum: 10/e332522f242348c511640c25a6fc7da4f30e09e580c70c6b13cb0be83c78c3e71c8d4665af2527e869fc96848924a4316ae7ec9014c091e2156f41739d4fa233
+  languageName: node
+  linkType: hard
+
 "node-fetch@npm:^1.0.1":
   version: 1.7.3
   resolution: "node-fetch@npm:1.7.3"
@@ -22601,6 +22747,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"node-fetch@npm:^3.3.0, node-fetch@npm:^3.3.2":
+  version: 3.3.2
+  resolution: "node-fetch@npm:3.3.2"
+  dependencies:
+    data-uri-to-buffer: "npm:^4.0.0"
+    fetch-blob: "npm:^3.1.4"
+    formdata-polyfill: "npm:^4.0.10"
+  checksum: 10/24207ca8c81231c7c59151840e3fded461d67a31cf3e3b3968e12201a42f89ce4a0b5fb7079b1fa0a4655957b1ca9257553200f03a9f668b45ebad265ca5593d
+  languageName: node
+  linkType: hard
+
 "node-forge@npm:^1":
   version: 1.3.1
   resolution: "node-forge@npm:1.3.1"
@@ -23910,6 +24067,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"path-posix@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "path-posix@npm:1.0.0"
+  checksum: 10/b4eae5cd4b7c943719c2f8679c53d02988bf1701583065cc5b301bb671e6ec13d6e4257257fe92a5c7b34c35e215b322a8976ce89d29dcf8801c0ee2cc75ca18
+  languageName: node
+  linkType: hard
+
 "path-scurry@npm:^1.11.1":
   version: 1.11.1
   resolution: "path-scurry@npm:1.11.1"
@@ -29831,6 +29995,7 @@ __metadata:
     "@uppy/unsplash": "workspace:^"
     "@uppy/url": "workspace:^"
     "@uppy/webcam": "workspace:^"
+    "@uppy/webdav": "workspace:^"
     "@uppy/xhr-upload": "workspace:^"
     "@uppy/zoom": "workspace:^"
   languageName: unknown
@@ -29852,7 +30017,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"url-parse@npm:^1.4.4, url-parse@npm:^1.5.3, url-parse@npm:^1.5.7":
+"url-join@npm:^5.0.0":
+  version: 5.0.0
+  resolution: "url-join@npm:5.0.0"
+  checksum: 10/5921384a8ad4395b49ce4b50aa26efbc429cebe0bc8b3660ad693dd12fd859747b5369be0443e60e53a7850b2bc9d7d0687bcb94386662b40e743596bbf38101
+  languageName: node
+  linkType: hard
+
+"url-parse@npm:^1.4.4, url-parse@npm:^1.5.10, url-parse@npm:^1.5.3, url-parse@npm:^1.5.7":
   version: 1.5.10
   resolution: "url-parse@npm:1.5.10"
   dependencies:
@@ -30419,6 +30591,35 @@ __metadata:
   languageName: node
   linkType: hard
 
+"web-streams-polyfill@npm:^3.0.3":
+  version: 3.3.3
+  resolution: "web-streams-polyfill@npm:3.3.3"
+  checksum: 10/8e7e13501b3834094a50abe7c0b6456155a55d7571312b89570012ef47ec2a46d766934768c50aabad10a9c30dd764a407623e8bfcc74fcb58495c29130edea9
+  languageName: node
+  linkType: hard
+
+"webdav@npm:5.7.1":
+  version: 5.7.1
+  resolution: "webdav@npm:5.7.1"
+  dependencies:
+    "@buttercup/fetch": "npm:^0.2.1"
+    base-64: "npm:^1.0.0"
+    byte-length: "npm:^1.0.2"
+    entities: "npm:^5.0.0"
+    fast-xml-parser: "npm:^4.4.1"
+    hot-patcher: "npm:^2.0.1"
+    layerr: "npm:^3.0.0"
+    md5: "npm:^2.3.0"
+    minimatch: "npm:^9.0.5"
+    nested-property: "npm:^4.0.0"
+    node-fetch: "npm:^3.3.2"
+    path-posix: "npm:^1.0.0"
+    url-join: "npm:^5.0.0"
+    url-parse: "npm:^1.5.10"
+  checksum: 10/3b22e32b430d4815a52c5b51d8c2b7ce9b9ff6b03fd32c66ee73d208bd5142a9a15fab18cde674531a37cf8cf4bef2ca0f0d7ca613b4f2d7141f9a6be7c0c135
+  languageName: node
+  linkType: hard
+
 "webidl-conversions@npm:^3.0.0":
   version: 3.0.1
   resolution: "webidl-conversions@npm:3.0.1"