@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:
152 lines (151 loc) • 6.29 kB
JavaScript
;
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;
}
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');
// 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' });
}
// 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 });
/** @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); // todo improve (read err?.response?.body readable stream and parse response)
}
logger.error(err2, tag);
throw err2;
}
}
}
module.exports = WebdavProvider;