@uppy/companion
Version:
OAuth helper and remote fetcher for Uppy's (https://uppy.io) extensible file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox and Google Drive, S3 and more :dog:
181 lines (156 loc) • 5.07 kB
JavaScript
import { AuthType, createClient } from 'webdav'
import { getProtectedHttpAgent, validateURL } from '../../helpers/request.js'
import logger from '../../logger.js'
import {
ProviderApiError,
ProviderAuthError,
ProviderUserError,
} from '../error.js'
import Provider from '../Provider.js'
const defaultDirectory = '/'
/**
* Adapter for WebDAV servers that support simple auth (non-OAuth).
*/
export default class WebdavProvider extends Provider {
static get hasSimpleAuth() {
return true
}
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')
}
// 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() {
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' })
}
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,
})
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
}
// Determine icon based on type and MIME type
let icon = 'file'
if (isFolder) {
icon = 'folder'
} else if (item.mime?.startsWith('video/')) {
icon = 'video'
}
data.items.push({
isFolder,
icon,
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 })
/** @type {any} */
const stat = await client.stat(id)
const stream = client.createReadStream(`/${id}`)
return { stream, size: stat.size }
},
)
}
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')
}
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)
}
logger.error(err2, tag)
throw err2
}
}
}