UNPKG

@zenfs/core

Version:

A filesystem, anywhere

392 lines (391 loc) 14 kB
// SPDX-License-Identifier: LGPL-3.0-or-later import { UV, withErrno } from 'kerium'; import * as c from '../constants.js'; import { contextOf } from '../internal/contexts.js'; import { _chown, InodeFlags, isBlockDevice, isCharacterDevice } from '../internal/inode.js'; import '../polyfills.js'; /** * @internal */ export class Handle { context; path; fs; internalPath; flag; inode; _buffer; /** * Current position */ _position = 0; /** * Get the current file position. * * We emulate the following bug mentioned in the Node documentation: * * On Linux, positional writes don't work when the file is opened in append mode. * The kernel ignores the position argument and always appends the data to the end of the file. * @returns The current file position. */ get position() { return this.flag & c.O_APPEND ? this.inode.size : this._position; } set position(value) { this._position = value; } /** * Whether the file has changes which have not been written to the FS */ dirty = false; /** * Whether the file is open or closed */ closed = false; get isClosed() { return this.closed; } /** * Creates a file with `path` and, optionally, the given contents. * Note that, if contents is specified, it will be mutated by the file. */ constructor(context, path, fs, internalPath, flag, inode) { this.context = context; this.path = path; this.fs = fs; this.internalPath = internalPath; this.flag = flag; this.inode = inode; } get _isSync() { return !!(this.flag & c.O_SYNC || this.inode.flags & InodeFlags.Sync || this.fs.attributes.has('sync')); } [Symbol.dispose]() { this.closeSync(); } syncSync() { if (this.closed) throw UV('EBADF', 'sync', this.path); if (!this.dirty) return; if (!this.fs.attributes.has('no_write')) this.fs.touchSync(this.internalPath, this.inode); this.dirty = false; } /** * Default implementation maps to `syncSync`. */ datasyncSync() { return this.syncSync(); } closeSync() { if (this.closed) throw UV('EBADF', 'close', this.path); this.syncSync(); this.disposeSync(); } /** * Cleans up. This will *not* sync the file data to the FS */ disposeSync(force) { if (this.closed) throw UV('EBADF', 'close', this.path); if (this.dirty && !force) throw UV('EBUSY', 'close', this.path); this.closed = true; } truncateSync(length) { if (length < 0) throw UV('EINVAL', 'truncate', this.path); if (this.closed) throw UV('EBADF', 'truncate', this.path); if (!(this.flag & c.O_WRONLY || this.flag & c.O_RDWR)) throw UV('EBADF', 'truncate', this.path); if (this.fs.attributes.has('readonly')) throw UV('EROFS', 'truncate', this.path); if (this.inode.flags & InodeFlags.Immutable) throw UV('EPERM', 'truncate', this.path); this.dirty = true; this.inode.mtimeMs = Date.now(); this.inode.size = length; this.inode.ctimeMs = Date.now(); if (this._isSync) this.syncSync(); } /** * Write buffer to the file. * @param buffer Uint8Array containing the data to write to the file. * @param offset Offset in the buffer to start reading data from. * @param length The amount of bytes to write to the file. * @param position Offset from the beginning of the file where this data should be written. * If position is null, the data will be written at the current position. * @returns bytes written */ writeSync(buffer, offset = 0, length = buffer.byteLength - offset, position = this.position) { if (this.closed) throw UV('EBADF', 'write', this.path); if (!(this.flag & c.O_WRONLY || this.flag & c.O_RDWR)) throw UV('EBADF', 'write', this.path); if (this.fs.attributes.has('readonly')) throw UV('EROFS', 'write', this.path); if (this.inode.flags & InodeFlags.Immutable) throw UV('EPERM', 'write', this.path); this.dirty = true; const end = position + length; const slice = buffer.subarray(offset, offset + length); if (!isCharacterDevice(this.inode) && !isBlockDevice(this.inode) && end > this.inode.size) this.inode.size = end; this.inode.mtimeMs = Date.now(); this.inode.ctimeMs = Date.now(); this._position = position + slice.byteLength; this.fs.writeSync(this.internalPath, slice, position); if (this._isSync) this.syncSync(); return slice.byteLength; } /** * Read data from the file. * @param buffer The buffer that the data will be written to. * @param offset The offset within the buffer where writing will start. * @param length An integer specifying the number of bytes to read. * @param position An integer specifying where to begin reading from in the file. * If position is null, data will be read from the current file position. * @returns number of bytes written */ readSync(buffer, offset = 0, length = buffer.byteLength - offset, position = this.position) { if (this.closed) throw UV('EBADF', 'read', this.path); if (this.flag & c.O_WRONLY) throw UV('EBADF', 'read', this.path); if (!(this.inode.flags & InodeFlags.NoAtime) && !this.fs.attributes.has('no_atime')) { this.dirty = true; this.inode.atimeMs = Date.now(); } let end = position + length; if (!isCharacterDevice(this.inode) && !isBlockDevice(this.inode) && end > this.inode.size) { end = position + Math.max(this.inode.size - position, 0); } this._position = end; const uint8 = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); this.fs.readSync(this.internalPath, uint8.subarray(offset, offset + length), position, end); if (this._isSync) this.syncSync(); return end - position; } chmodSync(mode) { if (this.closed) throw UV('EBADF', 'chmod', this.path); this.dirty = true; this.inode.mode = (this.inode.mode & (mode > c.S_IFMT ? ~c.S_IFMT : c.S_IFMT)) | mode; if (this._isSync || mode > c.S_IFMT) this.syncSync(); } chownSync(uid, gid) { if (this.closed) throw UV('EBADF', 'chmod', this.path); this.dirty = true; _chown(this.inode, uid, gid); if (this._isSync) this.syncSync(); } /** * Change the file timestamps of the file. */ utimesSync(atime, mtime) { if (this.closed) throw UV('EBADF', 'utimes', this.path); this.dirty = true; this.inode.atimeMs = atime; this.inode.mtimeMs = mtime; if (this._isSync) this.syncSync(); } async [Symbol.asyncDispose]() { await this.close(); } async sync() { if (this.closed) throw UV('EBADF', 'sync', this.path); if (!this.dirty) return; if (!this.fs.attributes.has('no_write')) await this.fs.touch(this.internalPath, this.inode); this.dirty = false; } /** * Default implementation maps to `sync`. */ datasync() { return this.sync(); } async close() { if (this.closed) throw UV('EBADF', 'close', this.path); await this.sync(); this.dispose(); } /** * Cleans up. This will *not* sync the file data to the FS */ dispose(force) { if (this.closed) throw UV('EBADF', 'close', this.path); if (this.dirty && !force) throw UV('EBUSY', 'close', this.path); this.closed = true; } stat() { if (this.closed) throw UV('EBADF', 'stat', this.path); return this.inode; } async truncate(length) { if (length < 0) throw UV('EINVAL', 'truncate', this.path); if (this.closed) throw UV('EBADF', 'truncate', this.path); if (!(this.flag & c.O_WRONLY || this.flag & c.O_RDWR)) throw UV('EBADF', 'truncate', this.path); if (this.fs.attributes.has('readonly')) throw UV('EROFS', 'truncate', this.path); if (this.inode.flags & InodeFlags.Immutable) throw UV('EPERM', 'truncate', this.path); this.dirty = true; this.inode.mtimeMs = Date.now(); this.inode.size = length; this.inode.ctimeMs = Date.now(); if (this._isSync) await this.sync(); } /** * Write buffer to the file. * @param buffer Uint8Array containing the data to write to the file. * @param offset Offset in the buffer to start reading data from. * @param length The amount of bytes to write to the file. * @param position Offset from the beginning of the file where this data should be written. * If position is null, the data will be written at the current position. * @returns bytes written */ async write(buffer, offset = 0, length = buffer.byteLength - offset, position = this.position) { if (this.closed) throw UV('EBADF', 'write', this.path); if (!(this.flag & c.O_WRONLY || this.flag & c.O_RDWR)) throw UV('EBADF', 'write', this.path); if (this.fs.attributes.has('readonly')) throw UV('EROFS', 'write', this.path); if (this.inode.flags & InodeFlags.Immutable) throw UV('EPERM', 'write', this.path); this.dirty = true; const end = position + length; const slice = buffer.subarray(offset, offset + length); if (!isCharacterDevice(this.inode) && !isBlockDevice(this.inode) && end > this.inode.size) this.inode.size = end; this.inode.mtimeMs = Date.now(); this.inode.ctimeMs = Date.now(); this._position = position + slice.byteLength; await this.fs.write(this.internalPath, slice, position); if (this._isSync) await this.sync(); return slice.byteLength; } /** * Read data from the file. * @param buffer The buffer that the data will be written to. * @param offset The offset within the buffer where writing will start. * @param length An integer specifying the number of bytes to read. * @param position An integer specifying where to begin reading from in the file. * If position is null, data will be read from the current file position. * @returns number of bytes written */ async read(buffer, offset = 0, length = buffer.byteLength - offset, position = this.position) { if (this.closed) throw UV('EBADF', 'read', this.path); if (this.flag & c.O_WRONLY) throw UV('EBADF', 'read', this.path); if (!(this.inode.flags & InodeFlags.NoAtime) && !this.fs.attributes.has('no_atime')) { this.dirty = true; this.inode.atimeMs = Date.now(); } let end = position + length; if (!isCharacterDevice(this.inode) && !isBlockDevice(this.inode) && end > this.inode.size) { end = position + Math.max(this.inode.size - position, 0); } this._position = end; const uint8 = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); await this.fs.read(this.internalPath, uint8.subarray(offset, offset + length), position, end); if (this._isSync) await this.sync(); return end - position; } async chmod(mode) { if (this.closed) throw UV('EBADF', 'chmod', this.path); this.dirty = true; this.inode.mode = (this.inode.mode & (mode > c.S_IFMT ? ~c.S_IFMT : c.S_IFMT)) | mode; if (this._isSync || mode > c.S_IFMT) await this.sync(); } async chown(uid, gid) { if (this.closed) throw UV('EBADF', 'chown', this.path); this.dirty = true; _chown(this.inode, uid, gid); if (this._isSync) await this.sync(); } /** * Change the file timestamps of the file. */ async utimes(atime, mtime) { if (this.closed) throw UV('EBADF', 'utimes', this.path); this.dirty = true; this.inode.atimeMs = atime; this.inode.mtimeMs = mtime; if (this._isSync) await this.sync(); } /** * Create a stream for reading the file. */ streamRead(options) { if (this.closed) throw UV('EBADF', 'streamRead', this.path); return this.fs.streamRead(this.internalPath, options); } /** * Create a stream for writing the file. */ streamWrite(options) { if (this.closed) throw UV('EBADF', 'write', this.path); if (this.inode.flags & InodeFlags.Immutable) throw UV('EPERM', 'write', this.path); if (this.fs.attributes.has('readonly')) throw UV('EROFS', 'write', this.path); return this.fs.streamWrite(this.internalPath, options); } } // descriptors /** * @internal @hidden */ export function toFD(file) { const map = contextOf(file.context).descriptors; const fd = Math.max(map.size ? Math.max(...map.keys()) + 1 : 0, 4); map.set(fd, file); return fd; } /** * @internal @hidden */ export function fromFD($, fd) { const map = contextOf($).descriptors; const value = map.get(fd); if (!value) throw withErrno('EBADF'); return value; } export function deleteFD($, fd) { return contextOf($).descriptors.delete(fd); }