UNPKG

@nephele/adapter-nymph

Version:

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

401 lines (360 loc) 10.7 kB
import { EntityUniqueConstraintError, type Nymph } from '@nymphjs/nymph'; import { Entity, nymphJoiProps } from '@nymphjs/nymph'; import type { AccessControlData } from '@nymphjs/tilmeld'; import { enforceTilmeld, tilmeldJoiProps } from '@nymphjs/tilmeld'; import Joi from 'joi'; import { BadRequestError, ForbiddenError, InternalServerError, ResourceExistsError, UnauthorizedError, } from 'nephele'; import { Lock as NymphLock } from './Lock.js'; export type ResourceData = { name: string; size: number; contentType: string; collection: boolean; hash: string; properties: { [k: string]: string }; parent: (Resource & ResourceData) | null; } & AccessControlData; export class Resource extends Entity<ResourceData> { static ETYPE = 'nephele_resource'; static class = 'Resource'; public static clientEnabledStaticMethods = []; protected $clientEnabledMethods = []; protected $allowlistData = []; protected $allowlistTags = []; protected $privateData = []; private $skipAcWhenSaving = false; private $skipAcWhenDeleting = false; constructor() { super(); this.$data.name = ''; this.$data.size = 0; this.$data.contentType = ''; this.$data.collection = false; this.$data.hash = ''; this.$data.properties = {}; this.$data.parent = null; } async $getUniques() { return [ `${this.$data.user?.guid}:${this.$data.parent?.guid}:${this.$data.name}`, ]; } $setNymph(nymph: Nymph) { this.$nymph = nymph; if (!this.$asleep()) { if (this.$data.user) { this.$data.user.$setNymph(nymph); } if (this.$data.group) { this.$data.group.$setNymph(nymph); } if (this.$data.parent) { this.$data.parent.$setNymph(nymph); } } } async $copy( destinationParent: Resource & ResourceData, name: string, existingResource?: Resource & ResourceData, ) { const transaction = 'resource-copy-' + this.guid; const nymph = this.$nymph; const tnymph = await nymph.startTransaction(transaction); this.$setNymph(tnymph); try { if (existingResource) { existingResource.$setNymph(tnymph); if ( await this.$nymph.getEntity( { class: this.$nymph.getEntityClass(Resource), skipAc: true }, { type: '&', ref: ['parent', existingResource], }, ) ) { throw new ForbiddenError('The destination resource is not empty.'); } if (!(await existingResource.$delete())) { throw new InternalServerError( "Couldn't delete destination resource.", ); } } const newNymphResource = await this.$nymph .getEntityClass(Resource) .factory(); newNymphResource.name = name; newNymphResource.size = this.$data.size; newNymphResource.contentType = this.$data.contentType; newNymphResource.collection = this.$data.collection; newNymphResource.hash = this.$data.hash; newNymphResource.properties = JSON.parse( JSON.stringify(this.$data.properties), ); newNymphResource.parent = destinationParent; if (!(await newNymphResource.$save())) { throw new InternalServerError("Couldn't save destination resource."); } await tnymph.commit(transaction); this.$setNymph(nymph); if (existingResource) { existingResource.$setNymph(nymph); } } catch (e: any) { await tnymph.rollback(transaction); this.$setNymph(nymph); if (existingResource) { existingResource.$setNymph(nymph); } try { // Refresh the entity, since there might be referenced entities that // think they're deleted, but aren't really because of the rollback. if (existingResource) { existingResource.$refresh(); } } catch (e: any) { // Ignore this error. } throw e; } } async $move( destinationParent: Resource & ResourceData, name: string, existingResource?: Resource & ResourceData, ) { if ( await this.$nymph.getEntity( { class: this.$nymph.getEntityClass(Resource), skipAc: true }, { type: '&', ref: ['parent', this], }, ) ) { throw new ForbiddenError('This resource is not empty.'); } const transaction = 'resource-move-' + this.guid; const nymph = this.$nymph; const tnymph = await nymph.startTransaction(transaction); this.$setNymph(tnymph); try { if (existingResource) { existingResource.$setNymph(tnymph); if ( await tnymph.getEntity( { class: tnymph.getEntityClass(Resource), skipAc: true }, { type: '&', ref: ['parent', existingResource], }, ) ) { throw new ForbiddenError('The destination resource is not empty.'); } // Delete the existing resource. if (!(await existingResource.$delete())) { throw new InternalServerError( "Couldn't delete destination resource.", ); } } // Delete existing locks on this resource. const locks = await tnymph.getEntities( { class: tnymph.getEntityClass(NymphLock), skipAc: true, }, { type: '&', ref: ['resource', this], }, ); for (let lock of locks) { if (!(await lock.$deleteSkipAC())) { throw new InternalServerError("Couldn't delete associated lock."); } } this.$data.name = name; this.$data.parent = destinationParent; if (!(await this.$save())) { throw new InternalServerError("Couldn't save destination resource."); } await tnymph.commit(transaction); this.$setNymph(nymph); if (existingResource) { existingResource.$setNymph(nymph); } } catch (e: any) { await tnymph.rollback(transaction); this.$setNymph(nymph); if (existingResource) { existingResource.$setNymph(nymph); } try { // Refresh the entity, since there might be referenced entities that // think they're deleted, but aren't really because of the rollback. await this.$refresh(); if (existingResource) { existingResource.$refresh(); } } catch (e: any) { // Ignore this error. } throw e; } } async $save() { try { const tilmeld = enforceTilmeld(this); if (!tilmeld.gatekeeper()) { throw new UnauthorizedError('You must be logged in.'); } } catch (e: any) { // No Tilmeld means auth happened elsewhere. } if (!this.$data.parent) { this.$data.parent = null; } // Validate the entity's data. try { Joi.attempt( this.$getValidatable(), Joi.object().keys({ ...nymphJoiProps, ...tilmeldJoiProps, name: Joi.string() .max(255) .pattern(/\//, { invert: true, name: 'must not contain forward slash', }) .required(), size: Joi.number().required(), contentType: Joi.string().trim(false).max(255).required(), collection: Joi.boolean().required(), hash: Joi.string().trim(false).hex().length(96).required(), properties: Joi.object().pattern( Joi.string().trim(false).max(2048), Joi.alternatives().try( Joi.string().trim(false).allow('').max(65536), Joi.array().items(Joi.string().trim(false).allow('').max(65536)), ), ), parent: Joi.alternatives().try( Joi.any().allow(null), Joi.object().instance(Resource), ), }), 'Invalid Resource: ', ); } catch (e: any) { throw new BadRequestError(e.message); } try { return await super.$save(); } catch (e: any) { if (e instanceof EntityUniqueConstraintError) { throw new ResourceExistsError('This resource already exists.'); } throw e; } } /* * This should *never* be accessible on the client. */ async $saveSkipAC() { this.$skipAcWhenSaving = true; return await this.$save(); } $tilmeldSaveSkipAC() { if (this.$skipAcWhenSaving) { this.$skipAcWhenSaving = false; return true; } return false; } async $delete() { if ( this.$data.collection && (await this.$nymph.getEntity( { class: this.$nymph.getEntityClass(Resource), skipAc: true }, { type: '&', ref: ['parent', this], }, )) ) { throw new ForbiddenError("This resource isn't empty."); } const transaction = 'resource-delete-' + this.guid; const nymph = this.$nymph; const tnymph = await nymph.startTransaction(transaction); this.$setNymph(tnymph); try { // Delete this entity's locks. const locks = await tnymph.getEntities( { class: tnymph.getEntityClass(NymphLock), skipAc: true, }, { type: '&', ref: ['resource', this], }, ); for (let lock of locks) { if (!(await lock.$deleteSkipAC())) { throw new InternalServerError("Couldn't delete associated lock."); } } // Delete resource. let success = await super.$delete(); if (success) { success = await tnymph.commit(transaction); } else { await tnymph.rollback(transaction); } this.$setNymph(nymph); if (!success) { // Refresh the entity, since there might be referenced entities that // think they're deleted, but aren't really because of the rollback. await this.$refresh(); } return success; } catch (e: any) { await tnymph.rollback(transaction); this.$setNymph(nymph); try { // Refresh the entity, since there might be referenced entities that // think they're deleted, but aren't really because of the rollback. await this.$refresh(); } catch (e: any) { // Ignore this error. } throw e; } } /* * This should *never* be accessible on the client. */ async $deleteSkipAC() { this.$skipAcWhenDeleting = true; return await this.$delete(); } $tilmeldDeleteSkipAC() { if (this.$skipAcWhenDeleting) { this.$skipAcWhenDeleting = false; return true; } return false; } }