@nephele/adapter-file-system
Version:
File system adapter for the Nephele WebDAV server.
709 lines • 27.1 kB
JavaScript
import { Readable } from 'node:stream';
import fsp from 'node:fs/promises';
import { constants } from 'node:fs';
import path from 'node:path';
import mime from 'mime';
import checkDiskSpace from 'check-disk-space';
import crc32 from 'cyclic-32';
import { BadGatewayError, ForbiddenError, MethodNotSupportedError, ResourceExistsError, ResourceNotFoundError, ResourceTreeNotCompleteError, UnauthorizedError, } from 'nephele';
import { userReadBit, userWriteBit, userExecuteBit, groupReadBit, groupWriteBit, groupExecuteBit, otherReadBit, otherWriteBit, otherExecuteBit, } from './FileSystemBits.js';
import Properties from './Properties.js';
import Lock from './Lock.js';
export default class Resource {
constructor({ adapter, baseUrl, path: myPath, collection, stats, }) {
this.collection = undefined;
this.etag = undefined;
this.stats = undefined;
this.adapter = adapter;
this.baseUrl = baseUrl;
this.path = myPath.replace(new RegExp(`${path.sep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}?$`), '');
if (collection != null) {
this.collection = collection;
}
if (stats) {
this.stats = stats;
}
}
get absolutePath() {
return `${this.adapter.root}${path.sep}${this.path}`;
}
async getLocks() {
const meta = await this.readMetadataFile();
if (meta.locks == null) {
return [];
}
return Object.entries(meta.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) {
const meta = await this.readMetadataFile();
if (meta.locks == null) {
return [];
}
return Object.entries(meta.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 (await this.isCollection()) {
return Readable.from([]);
}
const handle = await fsp.open(this.absolutePath, 'r');
const stream = handle.createReadStream(range ? range : undefined);
stream.on('error', async () => {
await handle.close();
});
stream.on('close', async () => {
await handle.close();
});
return stream;
}
async setStream(input, user) {
let exists = true;
try {
await fsp.access(path.dirname(this.absolutePath), constants.F_OK);
}
catch (e) {
throw new ResourceTreeNotCompleteError('One or more intermediate collections must be created before this resource.');
}
if (await this.isCollection()) {
throw new MethodNotSupportedError('This resource is an existing collection.');
}
try {
await fsp.access(this.absolutePath, constants.W_OK);
}
catch (e) {
exists = false;
}
if (!exists && user.uid != null) {
await fsp.writeFile(this.absolutePath, Buffer.from([]));
await fsp.chown(this.absolutePath, await this.adapter.getUid(user), await this.adapter.getGid(user));
}
this.etag = undefined;
const handle = await fsp.open(this.absolutePath, 'w');
const stream = handle.createWriteStream();
this.stats = undefined;
input.pipe(stream);
return await new Promise((resolve, reject) => {
stream.on('close', async () => {
await handle.close();
resolve();
});
stream.on('error', async (err) => {
input.destroy(err);
await handle.close();
reject(err);
});
input.on('error', async (err) => {
stream.destroy(err);
await handle.close();
reject(err);
});
});
}
async create(user) {
if (await this.exists()) {
throw new ResourceExistsError('A resource already exists here.');
}
try {
await fsp.access(path.dirname(this.absolutePath), constants.F_OK);
}
catch (e) {
throw new ResourceTreeNotCompleteError('One or more intermediate collections must be created before this resource.');
}
if (this.collection) {
await fsp.mkdir(this.absolutePath);
}
else {
await fsp.writeFile(this.absolutePath, Uint8Array.from([]));
}
if (user.uid != null) {
await fsp.chown(this.absolutePath, await this.adapter.getUid(user), await this.adapter.getGid(user));
}
}
async delete(user) {
if (!(await this.exists())) {
throw new ResourceNotFoundError("This resource couldn't be found.");
}
try {
await fsp.access(this.absolutePath, constants.W_OK);
}
catch (e) {
throw new ForbiddenError('This resource cannot be deleted.');
}
const metaFilePath = await this.getMetadataFilePath();
let metaFileExists = false;
try {
await fsp.access(metaFilePath, constants.F_OK);
metaFileExists = true;
}
catch (e) {
metaFileExists = false;
}
if (metaFileExists) {
try {
await fsp.access(metaFilePath, constants.W_OK);
}
catch (e) {
throw new ForbiddenError('This resource cannot be deleted.');
}
}
const uid = await this.adapter.getUid(user);
const gids = await this.adapter.getGids(user);
if (user.uid != null) {
if (!this.stats) {
this.stats = await this.adapter.stat(this.absolutePath);
}
if (!(this.stats.mode & otherWriteBit ||
(this.stats.uid === uid && this.stats.mode & userWriteBit) ||
(gids.includes(this.stats.gid) && this.stats.mode & groupWriteBit))) {
throw new UnauthorizedError('You do not have permission to delete this resource.');
}
}
if (metaFileExists) {
await fsp.unlink(metaFilePath);
}
if (await this.isCollection()) {
await this.deleteOrphanedMetadataFiles();
await fsp.rmdir(this.absolutePath);
}
else {
await fsp.unlink(this.absolutePath);
}
this.stats = undefined;
}
async copy(destination, baseUrl, user) {
const destinationPath = this.adapter.urlToAbsolutePath(destination, baseUrl);
if (destinationPath == null) {
throw new BadGatewayError('The destination URL is not under the namespace of this server.');
}
if (this.absolutePath === destinationPath ||
((await this.isCollection()) &&
destinationPath.startsWith(this.absolutePath.replace(new RegExp(`${path.sep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}?$`), () => path.sep)))) {
throw new ForbiddenError('The destination cannot be the same as or contained within the source.');
}
try {
await fsp.access(path.dirname(destinationPath), constants.F_OK);
}
catch (e) {
throw new ResourceTreeNotCompleteError('One or more intermediate collections must be created before this resource.');
}
const uid = await this.adapter.getUid(user);
const gids = await this.adapter.getGids(user);
if (user.uid != null) {
const dstats = await this.adapter.stat(path.dirname(destinationPath));
if (!(dstats.mode & otherReadBit ||
(dstats.uid === uid && dstats.mode & userReadBit) ||
(gids.includes(dstats.gid) && dstats.mode & groupReadBit))) {
throw new UnauthorizedError('You do not have permission to access the destination.');
}
if (!(dstats.mode & otherWriteBit ||
(dstats.uid === uid && dstats.mode & userWriteBit) ||
(gids.includes(dstats.gid) && dstats.mode & groupWriteBit))) {
throw new UnauthorizedError('You do not have permission to write to the destination.');
}
}
let metaFilePath = undefined;
if (await this.isCollection()) {
try {
const stat = await this.adapter.stat(destinationPath);
if (stat.isDirectory()) {
const metaFilePath = `${destinationPath}${path.sep}.nephelemeta`;
const contents = await fsp.readdir(destinationPath);
if (contents.length > 1 ||
(contents.length === 1 && contents[0] !== metaFilePath)) {
throw new Error('Directory not empty.');
}
try {
if (this.adapter.properties !== 'meta-files' &&
this.adapter.locks !== 'meta-files') {
throw new Error('Ignored error. Bypass metadata file deletion.');
}
await fsp.unlink(metaFilePath);
}
catch (e) {
}
await fsp.rmdir(destinationPath);
}
else {
await fsp.unlink(destinationPath);
}
}
catch (e) {
}
try {
await fsp.mkdir(destinationPath);
}
catch (e) {
const stat = await this.adapter.stat(destinationPath);
if (!stat.isDirectory()) {
throw e;
}
}
try {
if (this.adapter.properties !== 'meta-files' &&
this.adapter.locks !== 'meta-files') {
throw new Error('Ignored error. Bypass metadata file deletion.');
}
metaFilePath = `${destinationPath}${path.sep}.nephelemeta`;
try {
await fsp.unlink(metaFilePath);
}
catch (e) {
}
const meta = await this.readMetadataFile();
meta.locks = {};
await this.saveMetadataFile(meta, destinationPath, metaFilePath);
}
catch (e) {
metaFilePath = undefined;
}
}
else {
await fsp.copyFile(this.absolutePath, destinationPath);
try {
if (this.adapter.properties !== 'meta-files' &&
this.adapter.locks !== 'meta-files') {
throw new Error('Ignored error. Bypass metadata file deletion.');
}
const dirname = path.dirname(destinationPath);
const basename = path.basename(destinationPath);
metaFilePath = `${dirname}${path.sep}${basename}.nephelemeta`;
try {
await fsp.unlink(metaFilePath);
}
catch (e) {
}
const meta = await this.readMetadataFile();
meta.locks = {};
await this.saveMetadataFile(meta, destinationPath, metaFilePath);
}
catch (e) {
metaFilePath = undefined;
}
}
if (user.uid != null) {
const uid = await this.adapter.getUid(user);
const gid = await this.adapter.getGid(user);
await fsp.chown(destinationPath, uid, gid);
if (!this.stats) {
this.stats = await this.adapter.stat(this.absolutePath);
}
await fsp.chmod(destinationPath, this.stats.mode % 0o1000);
if (metaFilePath != null) {
try {
await fsp.chown(metaFilePath, uid, gid);
await fsp.chmod(metaFilePath, this.stats.mode % 0o1000);
}
catch (e) {
}
}
}
if (!this.stats) {
this.stats = await this.adapter.stat(this.absolutePath);
}
try {
await fsp.chmod(destinationPath, this.stats.mode);
}
catch (e) {
}
try {
await fsp.utimes(destinationPath, this.stats.atime, this.stats.mtime);
}
catch (e) {
}
return;
}
async move(destination, baseUrl, user) {
if (await this.isCollection()) {
throw new Error('Move called on a collection resource.');
}
const destinationPath = this.adapter.urlToAbsolutePath(destination, baseUrl);
if (destinationPath == null) {
throw new BadGatewayError('The destination URL is not under the namespace of this server.');
}
if (this.absolutePath === destinationPath ||
((await this.isCollection()) &&
destinationPath.startsWith(this.absolutePath.replace(new RegExp(`${path.sep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}?$`), () => path.sep)))) {
throw new ForbiddenError('The destination cannot be the same as or contained within the source.');
}
try {
await fsp.access(path.dirname(destinationPath), constants.F_OK);
}
catch (e) {
throw new ResourceTreeNotCompleteError('One or more intermediate collections must be created before this resource.');
}
const uid = await this.adapter.getUid(user);
const gids = await this.adapter.getGids(user);
if (user.uid != null) {
if (!this.stats) {
this.stats = await this.adapter.stat(this.absolutePath);
}
if (!(this.stats.mode & otherWriteBit ||
(this.stats.uid === uid && this.stats.mode & userWriteBit) ||
(gids.includes(this.stats.gid) && this.stats.mode & groupWriteBit))) {
throw new UnauthorizedError('You do not have permission to move this resource.');
}
const dstats = await this.adapter.stat(path.dirname(destinationPath));
if (!(dstats.mode & otherReadBit ||
(dstats.uid === uid && dstats.mode & userReadBit) ||
(gids.includes(dstats.gid) && dstats.mode & groupReadBit))) {
throw new UnauthorizedError('You do not have permission to access the destination.');
}
if (!(dstats.mode & otherWriteBit ||
(dstats.uid === uid && dstats.mode & userWriteBit) ||
(gids.includes(dstats.gid) && dstats.mode & groupWriteBit))) {
throw new UnauthorizedError('You do not have permission to write to the destination.');
}
}
const metaFilePath = await this.getMetadataFilePath();
const meta = await this.readMetadataFile();
await fsp.rename(this.absolutePath, destinationPath);
try {
if (this.adapter.properties !== 'meta-files' &&
this.adapter.locks !== 'meta-files') {
throw new Error('Ignored error. Bypass metadata file deletion.');
}
const dirname = path.dirname(destinationPath);
const basename = path.basename(destinationPath);
const destMetaFilePath = `${dirname}${path.sep}${basename}.nephelemeta`;
try {
await fsp.unlink(destMetaFilePath);
}
catch (e) {
}
meta.locks = {};
await this.saveMetadataFile(meta, destinationPath, destMetaFilePath);
try {
await fsp.unlink(metaFilePath);
}
catch (e) {
}
}
catch (e) {
}
this.stats = undefined;
}
async getLength() {
if (await this.isCollection()) {
return 0;
}
if (!this.stats) {
this.stats = await this.adapter.stat(this.absolutePath);
}
return this.stats.size;
}
async getEtag() {
if (this.etag != null) {
return this.etag;
}
if (!this.stats) {
this.stats = await this.adapter.stat(this.absolutePath);
}
let etag;
if ((await this.isCollection()) ||
this.stats.size > this.adapter.contentEtagMaxBytes) {
etag = crc32
.c(Buffer.from(`size: ${this.stats.size}; birthtime: ${this.stats.birthtimeMs}; mtime: ${this.stats.mtimeMs}`, 'utf8'))
.toString(16);
}
else {
try {
const handle = await fsp.open(this.absolutePath, 'r');
await handle.close();
}
catch (e) {
throw new Error('Resource is not accessible.');
}
try {
etag = await new Promise(async (resolve, reject) => {
const stream = (await this.getStream()).pipe(crc32.createHash({ seed: 0, table: crc32.TABLE.CASTAGNOLI }));
stream.on('error', reject);
stream.on('data', (buffer) => {
resolve(buffer.toString('hex'));
});
});
}
catch (e) {
throw new Error('Etag could not be calculated.');
}
}
this.etag = etag;
return this.etag;
}
async getMediaType() {
if (await this.isCollection()) {
return null;
}
const mediaType = mime.getType(path.basename(this.absolutePath));
if (!mediaType) {
return 'application/octet-stream';
}
else if (Array.isArray(mediaType)) {
return typeof mediaType[0] === 'string'
? mediaType[0]
: 'application/octet-stream';
}
else if (typeof mediaType === 'string') {
return mediaType;
}
else {
return 'application/octet-stream';
}
}
async getCanonicalName() {
return path.basename(this.path);
}
async getCanonicalPath() {
if (await this.isCollection()) {
return this.path.replace(new RegExp(`${path.sep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}?$`), () => path.sep);
}
return this.path;
}
async getCanonicalUrl() {
return new URL((await this.getCanonicalPath())
.split(path.sep)
.map(encodeURIComponent)
.join('/')
.replace(/^\//, () => ''), this.baseUrl);
}
async isCollection() {
if (this.collection != null) {
return this.collection;
}
try {
if (!this.stats) {
this.stats = await this.adapter.stat(this.absolutePath);
}
this.collection = this.stats.isDirectory();
return this.collection;
}
catch (e) {
return false;
}
}
async getInternalMembers(user) {
if (!(await this.isCollection())) {
throw new MethodNotSupportedError('This is not a collection.');
}
const uid = await this.adapter.getUid(user);
const gids = await this.adapter.getGids(user);
if (user.uid != null) {
if (!this.stats) {
this.stats = await this.adapter.stat(this.absolutePath);
}
if (!(this.stats.mode & otherExecuteBit ||
(this.stats.uid === uid && this.stats.mode & userExecuteBit) ||
(gids.includes(this.stats.gid) && this.stats.mode & groupExecuteBit))) {
throw new UnauthorizedError("You do not have permission to list this collection's members.");
}
}
const listing = await fsp.readdir(this.absolutePath, {
withFileTypes: true,
});
const resources = [];
for (let dir of listing) {
if ((this.adapter.properties === 'meta-files' ||
this.adapter.locks === 'meta-files') &&
dir.name.endsWith('.nephelemeta')) {
continue;
}
try {
const isDir = dir.isDirectory();
const isFile = dir.isFile();
const isLink = dir.isSymbolicLink();
if (!isDir && !isFile && !isLink) {
continue;
}
const filepath = `${this.path}${path.sep}${dir.name}`;
if (isLink && this.adapter.followLinks) {
const stats = await this.adapter.stat(`${this.absolutePath}${path.sep}${dir.name}`);
resources.push(new Resource({
path: filepath,
baseUrl: this.baseUrl,
adapter: this.adapter,
collection: stats.isDirectory(),
stats,
}));
}
else {
resources.push(new Resource({
path: filepath,
baseUrl: this.baseUrl,
adapter: this.adapter,
collection: isDir,
}));
}
}
catch (e) {
continue;
}
}
return resources;
}
async exists() {
if (this.stats && this.stats.birthtime != null) {
return true;
}
try {
await fsp.access(this.absolutePath, constants.F_OK);
}
catch (e) {
return false;
}
return true;
}
async getStats() {
if (!this.stats) {
this.stats = await this.adapter.stat(this.absolutePath);
}
return this.stats;
}
async setMode(mode) {
await fsp.chmod(this.absolutePath, mode);
await fsp.chmod(await this.getMetadataFilePath(), mode);
}
async getFreeSpace() {
const directory = (await this.isCollection())
? this.absolutePath
: path.dirname(this.absolutePath);
return (await checkDiskSpace(directory)).free;
}
async getTotalSpace() {
const directory = (await this.isCollection())
? this.absolutePath
: path.dirname(this.absolutePath);
return (await checkDiskSpace(directory)).size;
}
async getMetadataFilePath() {
if (await this.isCollection()) {
return `${this.absolutePath}${path.sep}.nephelemeta`;
}
else {
const dirname = path.dirname(this.absolutePath);
const basename = path.basename(this.absolutePath);
return `${dirname}${path.sep}${basename}.nephelemeta`;
}
}
async readMetadataFile() {
const filepath = await this.getMetadataFilePath();
let meta = {};
if (this.adapter.properties !== 'meta-files' &&
this.adapter.locks !== 'meta-files') {
return meta;
}
try {
meta = JSON.parse((await fsp.readFile(filepath)).toString());
}
catch (e) {
if (e.code !== 'ENOENT' && e.code !== 'ENOTDIR') {
throw e;
}
}
return meta;
}
async saveMetadataFile(meta, filePath, metaFilePath) {
const saveProperties = this.adapter.properties === 'meta-files';
const saveLocks = this.adapter.locks === 'meta-files';
if (!saveProperties && !saveLocks) {
return;
}
if (!saveProperties) {
delete meta.props;
}
if (!saveLocks) {
delete meta.locks;
}
if (!metaFilePath) {
metaFilePath = await this.getMetadataFilePath();
}
let exists = true;
try {
await fsp.access(path.dirname(metaFilePath), constants.F_OK);
}
catch (e) {
throw new ResourceTreeNotCompleteError('One or more intermediate collections must be created before this resource.');
}
try {
await fsp.access(metaFilePath, constants.F_OK);
}
catch (e) {
exists = false;
}
if ((meta.props == null || Object.keys(meta.props).length === 0) &&
(meta.locks == null || Object.keys(meta.locks).length === 0)) {
if (exists) {
await fsp.unlink(metaFilePath);
}
}
else {
await fsp.writeFile(metaFilePath, JSON.stringify(meta, null, 2));
try {
const stat = filePath
? await this.adapter.stat(filePath)
: await this.getStats();
await fsp.chown(metaFilePath, stat.uid, stat.gid);
await fsp.chmod(metaFilePath, stat.mode % 0o1000);
}
catch (e) {
}
}
}
async deleteOrphanedMetadataFiles() {
if (!(await this.isCollection())) {
throw new MethodNotSupportedError('This is not a collection.');
}
if (this.adapter.properties !== 'meta-files' &&
this.adapter.locks !== 'meta-files') {
return;
}
const listing = await fsp.readdir(this.absolutePath);
const files = new Set();
const metaFiles = new Set();
for (let name of listing) {
if (name === '.nephelemeta') {
continue;
}
if (name.endsWith('.nephelemeta')) {
metaFiles.add(name);
}
else {
files.add(name);
}
}
for (let name of files) {
metaFiles.delete(`${name}.nephelemeta`);
}
const orphans = Array.from(metaFiles);
for (let name of orphans) {
const orphanPath = `${this.absolutePath}${path.sep}${name}`;
await fsp.unlink(orphanPath);
}
}
}
//# sourceMappingURL=Resource.js.map