-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(webdav): initial implementation for a WebDAV simple auth provider
- Loading branch information
Showing
12 changed files
with
581 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
125 changes: 125 additions & 0 deletions
125
packages/@uppy/companion/src/server/provider/webdav/common.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
const Provider = require('../Provider') | ||
const logger = require('../../logger') | ||
const { getProtectedHttpAgent, validateURL } = require('../../helpers/request') | ||
const { ProviderApiError, ProviderAuthError } = require('../error') | ||
|
||
/** | ||
* WebdavProvider base class provides implementations that could be shared by simple and oauth providers | ||
*/ | ||
class WebdavProvider extends Provider { | ||
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 }) | ||
|
||
const { createClient } = await import('webdav') | ||
return createClient(url, { | ||
...options, | ||
[`${protocol}Agent`] : new HttpAgentClass(), | ||
}) | ||
} | ||
|
||
async getClient ({ username, token, providerUserSession }) { // eslint-disable-line no-unused-vars,class-methods-use-this | ||
logger.error('call to getUsername is not implemented', 'provider.webdav.getUsername.error') | ||
throw new Error('call to getUsername is not implemented') | ||
// todo: use @returns to specify the return type | ||
return this.getClientHelper() // eslint-disable-line | ||
} | ||
|
||
async getUsername ({ token, providerUserSession }) { // eslint-disable-line no-unused-vars,class-methods-use-this | ||
logger.error('call to getUsername is not implemented', 'provider.webdav.getUsername.error') | ||
throw new Error('call to getUsername is not implemented') | ||
} | ||
|
||
/** @protected */ | ||
// eslint-disable-next-line class-methods-use-this | ||
isAuthenticated () { | ||
throw new Error('Not implemented') | ||
} | ||
|
||
async list ({ directory, token, providerUserSession }) { | ||
return this.withErrorHandling('provider.webdav.list.error', async () => { | ||
// @ts-ignore | ||
if (!this.isAuthenticated({ providerUserSession })) { | ||
throw new ProviderAuthError() | ||
} | ||
|
||
const username = await this.getUsername({ token, providerUserSession }) | ||
const data = { username, items: [] } | ||
const client = await this.getClient({ username, token, providerUserSession }) | ||
|
||
/** @type {any} */ | ||
const dir = await client.getDirectoryContents(directory || '/') | ||
|
||
dir.forEach(item => { | ||
const isFolder = item.type === 'directory' | ||
const requestPath = encodeURIComponent(`${directory || ''}/${item.basename}`) | ||
data.items.push({ | ||
isFolder, | ||
id: requestPath, | ||
name: item.basename, | ||
requestPath, // TODO FIXME | ||
modifiedDate: item.lastmod, // TODO FIXME: convert 'Tue, 04 Jul 2023 13:09:47 GMT' to ISO 8601 | ||
...(!isFolder && { | ||
mimeType: item.mime, | ||
size: item.size, | ||
thumbnail: null, | ||
|
||
}), | ||
}) | ||
}) | ||
|
||
return data | ||
}) | ||
} | ||
|
||
async download ({ id, token, providerUserSession }) { | ||
return this.withErrorHandling('provider.webdav.download.error', async () => { | ||
// maybe we can avoid this by putting the username in front of the request path/id | ||
const username = await this.getUsername({ token, providerUserSession }) | ||
const client = await this.getClient({ username, token, 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') | ||
} | ||
|
||
// todo fixme implement | ||
// eslint-disable-next-line | ||
async size ({ id, token, providerUserSession }) { | ||
return this.withErrorHandling('provider.webdav.size.error', async () => { | ||
const username = await this.getUsername({ token, providerUserSession }) | ||
const client = await this.getClient({ username, token, 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 |
80 changes: 80 additions & 0 deletions
80
packages/@uppy/companion/src/server/provider/webdav/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
|
||
const { validateURL } = require('../../helpers/request') | ||
const WebdavProvider = require('./common') | ||
const { ProviderUserError } = require('../error') | ||
const logger = require('../../logger') | ||
|
||
const defaultDirectory = '/' | ||
|
||
/** | ||
* Adapter for WebDAV servers that support simple auth (non-OAuth). | ||
*/ | ||
class WebdavSimpleAuthProvider extends WebdavProvider { | ||
static get hasSimpleAuth () { | ||
return true | ||
} | ||
|
||
async getUsername () { // eslint-disable-line class-methods-use-this | ||
return null | ||
} | ||
|
||
// 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') | ||
} | ||
|
||
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 | ||
} | ||
} | ||
} | ||
|
||
module.exports = WebdavSimpleAuthProvider |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
{ | ||
"name": "@uppy/webdav", | ||
"description": "Import files from WebDAV into Uppy.", | ||
"version": "3.1.1", | ||
"license": "MIT", | ||
"main": "lib/index.js", | ||
"types": "types/index.d.ts", | ||
"type": "module", | ||
"keywords": [ | ||
"file uploader", | ||
"uppy", | ||
"uppy-plugin", | ||
"instagram", | ||
"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:^" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { h } from 'preact' | ||
import { useCallback, useState } from 'preact/hooks' | ||
|
||
import { UIPlugin } from '@uppy/core' | ||
import { Provider, tokenStorage } from '@uppy/companion-client' | ||
import { ProviderViews } from '@uppy/provider-views' | ||
|
||
import packageJson from '../package.json' | ||
import locale from './locale.ts' | ||
|
||
class WebdavSimpleAuthProvider extends Provider { | ||
async login({ authFormData, uppyVersions, signal }) { | ||
return this.loginSimpleAuth({ uppyVersions, authFormData, signal }) | ||
} | ||
|
||
async logout() { | ||
this.removeAuthToken() | ||
return { ok: true, revoked: true } | ||
} | ||
} | ||
|
||
const AuthForm = ({ loading, i18n, onAuth }) => { | ||
const [webdavUrl, setWebdavUrl] = useState('') | ||
|
||
const onSubmit = useCallback( | ||
(e) => { | ||
e.preventDefault() | ||
onAuth({ webdavUrl: webdavUrl.trim() }) | ||
}, | ||
[onAuth, webdavUrl], | ||
) | ||
|
||
return ( | ||
<form onSubmit={onSubmit}> | ||
<label htmlFor="uppy-Provider-publicLinkURL"> | ||
<span style={{ display: 'block' }}>{i18n('publicLinkURLLabel')}</span> | ||
<input | ||
id="uppy-Provider-publicLinkURL" | ||
name="webdavUrl" | ||
type="text" | ||
value={webdavUrl} | ||
onChange={(e) => setWebdavUrl(e.target.value)} | ||
disabled={loading} | ||
/> | ||
</label> | ||
<span style={{ display: 'block' }}> | ||
{i18n('publicLinkURLDescription')} | ||
</span> | ||
|
||
<button style={{ display: 'block' }} disabled={loading} type="submit"> | ||
Submit | ||
</button> | ||
</form> | ||
) | ||
} | ||
|
||
export default class Webdav extends UIPlugin { | ||
static VERSION = packageJson.version | ||
|
||
constructor(uppy, opts) { | ||
super(uppy, opts) | ||
this.id = this.opts.id || 'webdav' | ||
this.type = 'acquirer' | ||
this.storage = this.opts.storage || tokenStorage | ||
|
||
this.defaultLocale = locale | ||
|
||
console.log(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, loading, onAuth }) => ( | ||
<AuthForm loading={loading} onAuth={onAuth} i18n={i18n} /> | ||
), | ||
}) | ||
|
||
const { target } = this.opts | ||
if (target) { | ||
this.mount(target, this) | ||
} | ||
} | ||
|
||
uninstall() { | ||
this.view.tearDown() | ||
this.unmount() | ||
} | ||
|
||
onFirstRender() { | ||
return this.view.getFolder() | ||
} | ||
|
||
render(state) { | ||
return this.view.render(state) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { default } from './Webdav.tsx' | ||
export type { WebcamOptions } from './Webcam.tsx' |
Oops, something went wrong.