UNPKG

@nephele/adapter-nymph

Version:

Nymph.js based deduping file adapter for the Nephele WebDAV server.

365 lines 13.8 kB
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