@nephele/adapter-virtual
Version:
Virtual resource adapter for the Nephele WebDAV server.
412 lines • 16.3 kB
JavaScript
import { Readable } from 'node:stream';
import mime from 'mime';
import crc32 from 'cyclic-32';
import { BadGatewayError, ForbiddenError, MethodNotSupportedError, ResourceExistsError, ResourceNotFoundError, ResourceTreeNotCompleteError, UnauthorizedError, } from 'nephele';
import Properties from './Properties.js';
import Lock from './Lock.js';
export default class Resource {
constructor({ adapter, baseUrl, path: filePath, collection, }) {
this.adapter = adapter;
this.baseUrl = baseUrl;
this.path = filePath;
const basename = this.adapter.basename(this.path);
const file = this.getFile();
this.exists = !!file;
this.file =
file != null
? file
: collection
? {
name: basename,
properties: {
creationdate: new Date(),
getlastmodified: new Date(),
},
locks: {},
children: [],
}
: {
name: basename,
properties: {
creationdate: new Date(),
getlastmodified: new Date(),
},
locks: {},
content: Buffer.from([]),
};
this.collection = 'children' in this.file;
}
getFile() {
const barePath = this.path.replace(/^\//, '').replace(/\/$/, '');
if (barePath === '') {
return this.adapter.files;
}
const pathParts = barePath.split('/');
let current = this.adapter.files;
do {
const part = pathParts.shift();
if (!part || part === '.') {
return undefined;
}
if (current == null || !('children' in current)) {
return undefined;
}
current = current.children.find((child) => child.name === part);
} while (pathParts.length);
return current;
}
setFile(file) {
const barePath = this.path.replace(/^\//, '').replace(/\/$/, '');
if (barePath === '') {
if ('name' in file || 'content' in file) {
throw new Error('Tried to set root folder to non-root entry.');
}
this.adapter.files = file;
return;
}
const parentParts = this.adapter.dirname(barePath).split('/');
const basename = this.adapter.basename(barePath);
let parent = this.adapter.files;
do {
const part = parentParts.shift();
if (!part || part === '.') {
continue;
}
if (parent == null || !('children' in parent)) {
throw new ResourceTreeNotCompleteError('One or more intermediate collections must be created before this resource.');
}
parent = parent.children.find((child) => 'children' in child && child.name === part);
} while (parentParts.length);
let current = parent.children.find((child) => child.name === basename);
if (current) {
if ('name' in file) {
current.name = file.name;
}
if ('content' in file) {
current.content = file.content;
}
current.properties = file.properties;
current.locks = file.locks;
this.file = current;
}
else {
parent.children.push(file);
this.file = file;
}
if (!('creationdate' in this.file.properties) ||
this.file.properties.creationdate == null) {
this.file.properties.creationdate = new Date();
}
this.file.properties.getlastmodified = new Date();
this.collection = 'children' in this.file;
this.exists = true;
}
unsetFile() {
const barePath = this.path.replace(/^\//, '').replace(/\/$/, '');
if (barePath === '') {
throw new Error('Tried to delete root folder.');
}
const parentParts = this.adapter.dirname(barePath).split('/');
const basename = this.adapter.basename(barePath);
let parent = this.adapter.files;
do {
const part = parentParts.shift();
if (!part || part === '.') {
break;
}
if (parent == null || !('children' in parent)) {
throw new ResourceNotFoundError('The resource does not exist.');
}
parent = parent.children.find((child) => 'children' in child && child.name === part);
} while (parentParts.length);
let index = parent.children.findIndex((child) => child.name === basename);
if (index !== -1) {
parent.children.splice(index, 1);
this.exists = false;
}
}
async getLocks() {
return Object.entries(this.file.locks).map(([token, entry]) => {
const lock = new Lock({ resource: this, username: entry.username });
lock.token = token;
lock.date = new Date(entry.date);
lock.timeout = entry.timeout;
lock.scope = entry.scope;
lock.depth = entry.depth;
lock.provisional = entry.provisional;
lock.owner = entry.owner;
return lock;
});
}
async getLocksByUser(user) {
return Object.entries(this.file.locks)
.filter(([_token, entry]) => user.username === entry.username)
.map(([token, entry]) => {
const lock = new Lock({ resource: this, username: user.username });
lock.token = token;
lock.date = new Date(entry.date);
lock.timeout = entry.timeout;
lock.scope = entry.scope;
lock.depth = entry.depth;
lock.provisional = entry.provisional;
lock.owner = entry.owner;
return lock;
});
}
async createLockForUser(user) {
return new Lock({ resource: this, username: user.username });
}
async getProperties() {
return new Properties({ resource: this });
}
async getStream(range) {
if (!('content' in this.file)) {
return Readable.from([]);
}
if (range) {
return Readable.from(this.file.content.subarray(range.start, range.end));
}
return Readable.from(this.file.content);
}
async setStream(input, user) {
if (!('content' in this.file)) {
throw new MethodNotSupportedError('This resource is an existing collection.');
}
const dir = new Resource({
adapter: this.adapter,
baseUrl: this.baseUrl,
path: this.adapter.dirname(this.path),
}).getFile();
if (dir == null) {
throw new ResourceTreeNotCompleteError('One or more intermediate collections must be created before this resource.');
}
if ('owner' in this.file.properties &&
this.file.properties.owner !== user.username) {
throw new UnauthorizedError('You do not have permission to modify this resource.');
}
this.file.content = await new Promise((resolve, reject) => {
const bufs = [];
input.on('data', (chunk) => {
bufs.push(chunk);
});
input.on('error', (error) => {
reject(error);
});
input.on('end', () => {
resolve(Buffer.concat(bufs));
});
});
this.setFile(this.file);
}
async create(user) {
if (this.exists) {
throw new ResourceExistsError('A resource already exists here.');
}
if (!('owner' in this.file.properties)) {
this.file.properties.owner = user.username;
}
this.setFile(this.file);
}
async delete(user) {
if (!this.exists) {
throw new ResourceNotFoundError("This resource couldn't be found.");
}
if ('owner' in this.file.properties &&
this.file.properties.owner !== user.username) {
throw new UnauthorizedError('You do not have permission to delete this resource.');
}
if ('children' in this.file && this.file.children.length) {
throw new ForbiddenError('This collection is not empty.');
}
this.unsetFile();
}
async copy(destination, baseUrl, user) {
const destinationPath = this.adapter.urlToRelativePath(destination, baseUrl);
if (destinationPath == null) {
throw new BadGatewayError('The destination URL is not under the namespace of this server.');
}
if (this.path === destinationPath ||
(!('content' in this.file) &&
destinationPath.startsWith(this.path.replace(/\/?$/, () => '/')))) {
throw new ForbiddenError('The destination cannot be the same as or contained within the source.');
}
try {
const parent = await this.adapter.getResource(new URL(this.adapter
.dirname(destinationPath)
.split('/')
.map(encodeURIComponent)
.join('/'), baseUrl), baseUrl);
if ('owner' in parent.file.properties &&
parent.file.properties.owner !== user.username) {
throw new UnauthorizedError("You don't have permission to copy the resource to this destination.");
}
}
catch (e) {
if (e instanceof ResourceNotFoundError) {
throw new ResourceTreeNotCompleteError('One or more intermediate collections must be created before this resource.');
}
throw e;
}
let destinationResource;
try {
destinationResource = await this.adapter.getResource(destination, baseUrl);
if ('owner' in destinationResource.file.properties &&
destinationResource.file.properties.owner !== user.username) {
throw new UnauthorizedError("You don't have permission to modify the destination.");
}
}
catch (e) {
if (this.collection) {
destinationResource = await this.adapter.newCollection(destination, baseUrl);
}
else {
destinationResource = await this.adapter.newResource(destination, baseUrl);
}
}
let file;
if ('content' in this.file) {
file = {
name: this.adapter.basename(destinationPath),
properties: JSON.parse(JSON.stringify(this.file.properties)),
locks: {},
content: Buffer.from(this.file.content),
};
}
else {
file = {
name: this.adapter.basename(destinationPath),
properties: JSON.parse(JSON.stringify(this.file.properties)),
locks: {},
children: [],
};
}
file.properties.creationdate = new Date();
file.properties.getlastmodified = this.file.properties.getlastmodified;
file.properties.owner = user.username;
destinationResource.setFile(file);
}
async move(destination, baseUrl, user) {
if (!('content' in this.file)) {
throw new Error('Move called on a collection resource.');
}
const destinationPath = this.adapter.urlToRelativePath(destination, baseUrl);
if (destinationPath == null) {
throw new BadGatewayError('The destination URL is not under the namespace of this server.');
}
if (this.path === destinationPath ||
(!('content' in this.file) &&
destinationPath.startsWith(this.path.replace(/\/?$/, () => '/')))) {
throw new ForbiddenError('The destination cannot be the same as or contained within the source.');
}
if ('owner' in this.file.properties &&
this.file.properties.owner !== user.username) {
throw new UnauthorizedError("You don't have permission to move the resource.");
}
try {
const parent = await this.adapter.getResource(new URL(this.adapter.dirname(destination.toString())), baseUrl);
if ('owner' in parent.file.properties &&
parent.file.properties.owner !== user.username) {
throw new UnauthorizedError("You don't have permission to move the resource to this destination.");
}
}
catch (e) {
if (e instanceof ResourceNotFoundError) {
throw new ResourceTreeNotCompleteError('One or more intermediate collections must be created before this resource.');
}
throw e;
}
let destinationResource;
try {
destinationResource = await this.adapter.getResource(destination, baseUrl);
if ('owner' in destinationResource.file.properties &&
destinationResource.file.properties.owner !== user.username) {
throw new UnauthorizedError("You don't have permission to modify the destination.");
}
}
catch (e) {
destinationResource = await this.adapter.newResource(destination, baseUrl);
}
if ('children' in destinationResource.file &&
destinationResource.file.children.length) {
throw new ForbiddenError('The destination is not empty.');
}
this.unsetFile();
destinationResource.setFile({
...this.file,
name: this.adapter.basename(destinationPath),
locks: {},
});
}
async getLength() {
if (!('content' in this.file)) {
return 0;
}
return this.file.content.byteLength;
}
async getEtag() {
const etag = crc32
.c(Buffer.from(`size: ${('content' in this.file ? this.file.content : Buffer.from([]))
.byteLength}; birthtime: ${this.file.properties.creationdate.getTime()}; mtime: ${this.file.properties.getlastmodified.getTime()}`, 'utf8'))
.toString(16);
return etag;
}
async getMediaType() {
return await new Promise((resolve, reject) => {
if (!('content' in this.file)) {
resolve(null);
return;
}
const mediaType = mime.getType(this.file.name);
if (!mediaType) {
resolve('application/octet-stream');
}
else if (Array.isArray(mediaType)) {
resolve(typeof mediaType[0] === 'string'
? mediaType[0]
: 'application/octet-stream');
}
else if (typeof mediaType === 'string') {
resolve(mediaType);
}
else {
resolve('application/octet-stream');
}
});
}
async getCanonicalName() {
return 'name' in this.file
? this.file.name
: this.adapter.basename(this.path);
}
async getCanonicalPath() {
if (!('content' in this.file)) {
return this.path.replace(/\/?$/, () => '/');
}
return this.path;
}
async getCanonicalUrl() {
return new URL((await this.getCanonicalPath())
.replace(/^\//, () => '')
.split('/')
.map(encodeURIComponent)
.join('/'), this.baseUrl);
}
async isCollection() {
return 'children' in this.file;
}
async getInternalMembers(_user) {
if (!('children' in this.file)) {
throw new MethodNotSupportedError('This is not a collection.');
}
const resources = [];
for (let file of this.file.children) {
resources.push(new Resource({
path: this.path.replace(/\/?$/, () => '/') + file.name,
baseUrl: this.baseUrl,
adapter: this.adapter,
}));
}
return resources;
}
}
//# sourceMappingURL=Resource.js.map