@nephele/adapter-s3
Version:
S3 (or compatible) object storage adapter for the Nephele WebDAV server.
206 lines (179 loc) • 4.76 kB
text/typescript
import path from 'node:path';
import type { Request } from 'express';
import type { S3ClientConfig } from '@aws-sdk/client-s3';
import { S3 } from '@aws-sdk/client-s3';
import type {
Adapter as AdapterInterface,
AuthResponse,
Method,
User,
} from 'nephele';
import {
BadGatewayError,
MethodNotImplementedError,
MethodNotSupportedError,
ResourceNotFoundError,
} from 'nephele';
import Resource from './Resource.js';
export type AdapterConfig = {
/**
* The S3 client config object.
*
* See https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-s3/TypeAlias/S3ClientConfigType/
*/
s3Config: S3ClientConfig;
/**
* The S3 bucket.
*/
bucket: string;
/**
* The number of chunks to upload simultaneously to the storage.
*/
uploadQueueSize?: number;
/**
* The path in the S3 bucket to be the root of the adapter. '' means the root
* of the bucket.
*/
root?: string;
};
/**
* Nephele S3 adapter.
*/
export default class Adapter implements AdapterInterface {
s3: S3;
bucket: string;
uploadQueueSize = 4;
root = '';
constructor({ s3Config, bucket, uploadQueueSize, root }: AdapterConfig) {
this.s3 = new S3(s3Config);
this.bucket = bucket;
if (uploadQueueSize != null) {
this.uploadQueueSize = uploadQueueSize;
}
if (root != null) {
this.root = root.split('/').map(encodeURIComponent).join('/');
}
}
urlToRelativePath(url: URL, baseUrl: URL) {
if (
!decodeURIComponent(url.pathname)
.replace(/\/?$/, () => '/')
.startsWith(decodeURIComponent(baseUrl.pathname))
) {
return null;
}
return path.join(
path.sep,
...decodeURIComponent(url.pathname)
.substring(decodeURIComponent(baseUrl.pathname).length)
.replace(/\/?$/, '')
.split('/'),
);
}
relativePathToUrl(pathname: string, baseUrl: URL) {
const urlPath = pathname
.split(path.sep)
.filter((part) => part !== '')
.map(encodeURIComponent)
.join('/');
return new URL(urlPath, baseUrl);
}
relativePathToKey(pathname: string) {
return [this.root, ...pathname.split(path.sep).map(encodeURIComponent)]
.filter((part) => part != '')
.join('/');
}
keyToRelativePath(key: string) {
return (
path.sep +
[...key.substring(this.root.length).split('/')]
.filter((part) => part != '')
.map(decodeURIComponent)
.join(path.sep)
);
}
async getComplianceClasses(
_url: URL,
_request: Request,
_response: AuthResponse,
) {
// This adapter supports locks.
return ['2'];
}
async getAllowedMethods(
_url: URL,
_request: Request,
_response: AuthResponse,
) {
// This adapter doesn't support any WebDAV extensions that require
// additional methods.
return [];
}
async getOptionsResponseCacheControl(
_url: URL,
_request: Request,
_response: AuthResponse,
) {
// This adapter doesn't do anything special for individual URLs, so a max
// age of one week is fine.
return 'max-age=604800';
}
async isAuthorized(_url: URL, _method: string, _baseUrl: URL, _user: User) {
// This adapter doesn't do any access control.
return true;
}
async getResource(url: URL, baseUrl: URL) {
const path = this.urlToRelativePath(url, baseUrl);
if (path == null) {
throw new BadGatewayError(
'The given path is not managed by this server.',
);
}
const resource = new Resource({
adapter: this,
baseUrl,
path,
});
if (!(await resource.exists())) {
throw new ResourceNotFoundError('Resource not found.');
}
return resource;
}
async newResource(url: URL, baseUrl: URL) {
const path = this.urlToRelativePath(url, baseUrl);
if (path == null) {
throw new BadGatewayError(
'The given path is not managed by this server.',
);
}
return new Resource({
adapter: this,
baseUrl,
path,
exists: false,
collection: false,
});
}
async newCollection(url: URL, baseUrl: URL) {
const path = this.urlToRelativePath(url, baseUrl);
if (path == null) {
throw new BadGatewayError(
'The given path is not managed by this server.',
);
}
return new Resource({
adapter: this,
baseUrl,
path,
exists: false,
collection: true,
});
}
getMethod(method: string): typeof Method {
// No additional methods to handle.
if (method === 'POST' || method === 'PATCH') {
throw new MethodNotSupportedError('Method not supported.');
}
throw new MethodNotImplementedError('Method not implemented.');
}
}