@nephele/adapter-nymph
Version:
Nymph.js based deduping file adapter for the Nephele WebDAV server.
365 lines • 13.8 kB
JavaScript
import path from 'node:path';
import fs from 'node:fs';
import { constants } from 'node:fs';
import { v4 as uuidv4 } from 'uuid';
import { Nymph, TilmeldAccessLevels, } from '@nymphjs/nymph';
import { SQLite3Driver } from '@nymphjs/driver-sqlite3';
import { User as NymphUser, enforceTilmeld, AccessControlError, } from '@nymphjs/tilmeld';
import { BadGatewayError, InternalServerError, MethodNotImplementedError, MethodNotSupportedError, ResourceNotFoundError, ResourceTreeNotCompleteError, } from 'nephele';
import { Lock as NymphLock } from './entities/Lock.js';
import { Resource as NymphResource, } from './entities/Resource.js';
import Resource from './Resource.js';
import { EMPTY_HASH } from './constants.js';
export default class Adapter {
get tempRoot() {
return path.resolve(this.root, 'temp');
}
get blobRoot() {
return path.resolve(this.root, 'blob');
}
constructor({ nymph, root, getRootResource }) {
this._rootResource = null;
this.root = root.replace(new RegExp(`${path.sep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}?$`), () => path.sep);
this.nymph =
nymph ||
new Nymph({}, new SQLite3Driver({
filename: path.resolve(this.root, 'nephele.db'),
wal: true,
}));
try {
this.NymphLock = this.nymph.getEntityClass(NymphLock);
}
catch (e) {
this.NymphLock = this.nymph.addEntityClass(NymphLock);
}
try {
this.NymphResource = this.nymph.getEntityClass(NymphResource);
}
catch (e) {
this.NymphResource = this.nymph.addEntityClass(NymphResource);
}
this.getRootResource =
getRootResource ??
(async () => {
if (this._rootResource == null) {
let rootResource = await this.nymph.getEntity({ class: this.NymphResource }, {
type: '&',
equal: [
['parent', null],
['collection', true],
],
});
if (rootResource == null) {
rootResource = await this.nymph.getEntity({ class: this.NymphResource }, {
type: '&',
'!defined': 'parent',
equal: ['collection', true],
});
if (rootResource) {
rootResource.parent = null;
await rootResource.$save();
}
}
if (rootResource == null) {
rootResource = await this.NymphResource.factory();
rootResource.name = uuidv4();
rootResource.size = 0;
rootResource.contentType = 'inode/directory';
rootResource.collection = true;
rootResource.hash = EMPTY_HASH;
rootResource.parent = null;
if (!(await rootResource.$save())) {
throw new InternalServerError('Root resource could not be created.');
}
}
this._rootResource = rootResource;
}
return this._rootResource;
});
try {
fs.accessSync(this.root, constants.R_OK);
}
catch (e) {
throw new Error("Can't read from given file system root. Does the directory exist?");
}
}
urlToPathParts(url, baseUrl) {
if (!decodeURIComponent(url.pathname)
.replace(/\/?$/, () => '/')
.startsWith(decodeURIComponent(baseUrl.pathname))) {
return null;
}
return decodeURIComponent(url.pathname)
.substring(decodeURIComponent(baseUrl.pathname).length)
.replace(/\/?$/, '')
.split('/')
.filter((str) => str !== '');
}
pathPartsToUrl(pathParts, baseUrl) {
return new URL(pathParts.map(encodeURIComponent).join('/'), baseUrl);
}
async getComplianceClasses(_url, _request, _response) {
return ['2'];
}
async getAllowedMethods(_url, _request, _response) {
return [];
}
async getOptionsResponseCacheControl(_url, _request, _response) {
return 'max-age=604800';
}
async isAuthorized(url, method, baseUrl, user) {
let tilmeld;
try {
tilmeld = enforceTilmeld(this.nymph);
}
catch (e) {
return true;
}
if (!(user instanceof NymphUser)) {
return false;
}
let access = TilmeldAccessLevels.NO_ACCESS;
if (['GET', 'HEAD', 'COPY', 'OPTIONS', 'PROPFIND'].includes(method)) {
access = TilmeldAccessLevels.READ_ACCESS;
}
if (['POST', 'PUT', 'MKCOL', 'PROPPATCH'].includes(method)) {
access = TilmeldAccessLevels.WRITE_ACCESS;
}
if (['DELETE', 'MOVE'].includes(method)) {
access = TilmeldAccessLevels.FULL_ACCESS;
}
if (['SEARCH'].includes(method)) {
access = TilmeldAccessLevels.READ_ACCESS;
}
if (['LOCK', 'UNLOCK'].includes(method)) {
access = TilmeldAccessLevels.WRITE_ACCESS;
}
if (access === TilmeldAccessLevels.NO_ACCESS) {
return false;
}
const pathParts = this.urlToPathParts(url, baseUrl);
if (pathParts == null) {
return false;
}
if (pathParts.length === 0) {
return access <= TilmeldAccessLevels.WRITE_ACCESS;
}
try {
let rootResource = await this.getRootResource();
const parent = await this.getNymphParent(pathParts, rootResource);
if (!parent || parent.collection !== true) {
return false;
}
const curResource = await this.nymph.getEntity({ class: this.NymphResource }, {
type: '&',
equal: ['name', pathParts[pathParts.length - 1]],
ref: ['parent', parent],
});
if (curResource == null) {
return tilmeld.checkPermissions(parent, Math.max(access, TilmeldAccessLevels.WRITE_ACCESS), user);
}
else {
return tilmeld.checkPermissions(curResource, access, user);
}
}
catch (e) {
if (e instanceof AccessControlError) {
return false;
}
throw e;
}
return false;
}
async getNymphResource(pathParts, rootResource) {
if (pathParts.length === 0) {
return rootResource;
}
let query = [
{ class: this.NymphResource },
{
type: '&',
guid: rootResource.guid,
},
];
let depth = 0;
for (let i = 0; i < pathParts.length; i++) {
if (depth === 2) {
const resource = await this.nymph.getEntity(...query);
if (resource == null) {
return resource;
}
query = [
{ class: this.NymphResource },
{
type: '&',
guid: resource.guid,
},
];
depth = 0;
}
const part = pathParts[i];
query = [
{ class: this.NymphResource },
{
type: '&',
equal: [
['name', part],
...(i < pathParts.length - 1 ? [['collection', true]] : []),
],
qref: ['parent', query],
},
];
depth++;
}
return await this.nymph.getEntity(...query);
}
async getNymphParent(pathParts, rootResource) {
if (pathParts.length <= 1) {
return rootResource;
}
const resource = await this.getNymphResource(pathParts.slice(0, -1), rootResource);
if (resource == null || resource.collection !== true) {
return false;
}
return resource;
}
async getResource(url, baseUrl) {
const pathParts = this.urlToPathParts(url, baseUrl);
if (pathParts == null) {
throw new BadGatewayError('The given path is not managed by this server.');
}
try {
const rootResource = await this.getRootResource();
if (pathParts.length === 0) {
return new Resource({
adapter: this,
baseUrl,
path: '/',
nymphResource: rootResource,
rootResource,
});
}
const nymphResource = await this.getNymphResource(pathParts, rootResource);
if (nymphResource == null) {
throw new ResourceNotFoundError('Resource not found.');
}
const resource = new Resource({
adapter: this,
baseUrl,
path: `/${pathParts.join('/')}`,
nymphResource,
rootResource,
});
return resource;
}
catch (e) {
if (e instanceof AccessControlError) {
throw new ResourceNotFoundError('Resource not found.');
}
throw e;
}
}
async newResource(url, baseUrl) {
const pathParts = this.urlToPathParts(url, baseUrl);
if (pathParts == null) {
throw new BadGatewayError('The given path is not managed by this server.');
}
const rootResource = await this.getRootResource();
if (pathParts.length === 0) {
return new Resource({
adapter: this,
baseUrl,
path: '/',
nymphResource: rootResource,
rootResource,
});
}
const parent = await this.getNymphParent(pathParts, rootResource);
if (!parent) {
throw new ResourceTreeNotCompleteError('One or more intermediate collections must be created before this resource.');
}
const nymphResource = await this.nymph.getEntity({ class: this.NymphResource }, {
type: '&',
equal: ['name', pathParts[pathParts.length - 1]],
ref: ['parent', parent],
});
if (nymphResource == null) {
const newResource = await this.NymphResource.factory();
newResource.name = pathParts[pathParts.length - 1];
newResource.hash = EMPTY_HASH;
newResource.size = 0;
newResource.contentType = 'application/octet-stream';
newResource.collection = false;
newResource.parent = parent;
return new Resource({
adapter: this,
baseUrl,
path: `/${pathParts.join('/')}`,
nymphResource: newResource,
rootResource,
});
}
return new Resource({
adapter: this,
baseUrl,
path: `/${pathParts.join('/')}`,
nymphResource,
rootResource,
});
}
async newCollection(url, baseUrl) {
const pathParts = this.urlToPathParts(url, baseUrl);
if (pathParts == null) {
throw new BadGatewayError('The given path is not managed by this server.');
}
const rootResource = await this.getRootResource();
if (pathParts.length === 0) {
return new Resource({
adapter: this,
baseUrl,
path: '/',
nymphResource: rootResource,
rootResource,
});
}
const parent = await this.getNymphParent(pathParts, rootResource);
if (!parent) {
throw new ResourceTreeNotCompleteError('One or more intermediate collections must be created before this resource.');
}
const nymphResource = await this.nymph.getEntity({ class: this.NymphResource }, {
type: '&',
equal: ['name', pathParts[pathParts.length - 1]],
ref: ['parent', parent],
});
if (nymphResource == null) {
const newResource = await this.NymphResource.factory();
newResource.name = pathParts[pathParts.length - 1];
newResource.hash = EMPTY_HASH;
newResource.size = 0;
newResource.contentType = 'inode/directory';
newResource.collection = true;
newResource.parent = parent;
return new Resource({
adapter: this,
baseUrl,
path: `/${pathParts.join('/')}`,
nymphResource: newResource,
rootResource,
});
}
return new Resource({
adapter: this,
baseUrl,
path: `/${pathParts.join('/')}`,
nymphResource,
rootResource,
});
}
getMethod(method) {
if (method === 'POST' || method === 'PATCH') {
throw new MethodNotSupportedError('Method not supported.');
}
throw new MethodNotImplementedError('Method not implemented.');
}
}
//# sourceMappingURL=Adapter.js.map