refactor
Release / release (push) Successful in 24s

This commit is contained in:
2026-02-16 15:57:22 -08:00
parent e99fc35e25
commit 8bc2c7d70c
5 changed files with 78 additions and 72 deletions
+2 -1
View File
@@ -12,8 +12,9 @@ for (let i = 0; i < 10000; i++) {
await writable.write(textText)
await writable.close()
const file = await file_handle.getFile()
console.log(file.webkitRelativePath)
const text = await file.text()
if(text!==textText) throw new Error("failed to read back written data")
if (text !== textText) throw new Error("failed to read back written data")
}
const result = await cwd_directory_handle.getFileHandle('test/test.txt').catch(error => 'blocked path traversal')
if (typeof result === 'string') console.log(result)
+22 -23
View File
@@ -1,6 +1,6 @@
import * as fs from 'node:fs';
import * as asyncfs from 'node:fs/promises';
import * as path from 'node:path';
import * as Path from 'node:path';
import NodeFileSystemHandle from './NodeFileSystemHandle.ts';
import NodeFileSystemFileHandle from './NodeFileSystemFileHandle.ts';
import type {
@@ -14,20 +14,23 @@ import type {
export default class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle implements FileSystemDirectoryHandle {
declare kind: "directory";
constructor(fpath: string, options: FileSystemGetDirectoryOptions = {}) {
super()
fpath = path.normalize(fpath)
if (!fs.existsSync(fpath) && options.create) fs.mkdirSync(fpath)
if (!fs.lstatSync(fpath).isDirectory()) throw new Error('not a directory')
this.path = fpath
constructor(path: string, options: FileSystemGetDirectoryOptions & { origin?: NodeFileSystemDirectoryHandle } = {}) {
super(path, options)
try {
const stats = fs.statSync(this.path)
if (!stats.isDirectory()) throw new Error("tried to open file as directory: " + this.path)
} catch (error) {
if (options.create) fs.mkdirSync(this.path)
else throw error
}
}
entries(): FileSystemDirectoryHandleAsyncIterator<[string, NodeFileSystemDirectoryHandle | NodeFileSystemFileHandle]> {
const entries: Array<[string, NodeFileSystemDirectoryHandle | NodeFileSystemFileHandle]> = []
fs.readdirSync(this.path).forEach(name => {
if (fs.lstatSync(name).isDirectory()) {
entries.push([name, new NodeFileSystemDirectoryHandle(path.join(this.path, name))])
entries.push([name, new NodeFileSystemDirectoryHandle(Path.join(this.path, name), { origin: this.origin || this })])
} else {
entries.push([name, new NodeFileSystemFileHandle(path.join(this.path, name))])
entries.push([name, new NodeFileSystemFileHandle(Path.join(this.path, name), { origin: this.origin || this })])
}
})
return entries[Symbol.asyncIterator]
@@ -39,37 +42,33 @@ export default class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle
const values: Array<NodeFileSystemDirectoryHandle | NodeFileSystemFileHandle> = [];
fs.readdirSync(this.path).forEach(name => {
if (fs.lstatSync(name).isDirectory()) {
values.push(new NodeFileSystemDirectoryHandle(path.join(this.path, name)))
values.push(new NodeFileSystemDirectoryHandle(Path.join(this.path, name), { origin: this.origin || this }))
} else {
values.push(new NodeFileSystemFileHandle(path.join(this.path, name)))
values.push(new NodeFileSystemFileHandle(Path.join(this.path, name), { origin: this.origin || this }))
}
})
return values[Symbol.asyncIterator]
}
#getValidatedPath(name: string) {
name = path.normalize(name)
if (name.includes(path.sep)) throw new TypeError('Invalid Filename')
return path.join(this.path, name)
name = Path.normalize(name)
if (name.includes(Path.sep)) throw new TypeError('Invalid Filename')
return Path.join(this.path, name)
}
async getDirectoryHandle(name: string, options: FileSystemGetDirectoryOptions = {}): Promise<NodeFileSystemDirectoryHandle> {
Object.assign(options, { origin: this.origin || this })
return new NodeFileSystemDirectoryHandle(this.#getValidatedPath(name), options)
}
async getFileHandle(name: string, options: FileSystemGetFileOptions = {}): Promise<NodeFileSystemFileHandle> {
Object.assign(options, { origin: this.origin || this })
return new NodeFileSystemFileHandle(this.#getValidatedPath(name), options)
}
async removeEntry(name: string, options: FileSystemRemoveOptions = { recursive: false }): Promise<void> {
return asyncfs.rm(this.#getValidatedPath(name), { recursive: options.recursive, force: options.recursive })
}
async resolve(possibleDescendant: FileSystemHandle): Promise<string[] | null> {
if (typeof possibleDescendant !== typeof this) return null
let thisName = this.path
let possibleDescendantName = possibleDescendant.name
if (!path.isAbsolute(this.path)) thisName = path.join(process.cwd(), this.path)
if (!path.isAbsolute(possibleDescendant.name)) possibleDescendantName = path.join(process.cwd(), possibleDescendant.name)
if (!possibleDescendantName.startsWith(thisName)) return null
possibleDescendantName = possibleDescendantName.substring(thisName.length - 1)
if (possibleDescendantName[0] == path.sep) possibleDescendantName = possibleDescendantName.substring(1)
return possibleDescendantName.split(path.sep)
if (!(possibleDescendant instanceof NodeFileSystemHandle)) return null
if (!possibleDescendant.path.startsWith(this.path)) return null
return possibleDescendant.path.substring(this.path.length + 1).split(Path.sep)
}
[Symbol.asyncIterator] = this.entries
}
+18 -11
View File
@@ -1,6 +1,6 @@
import * as fs from 'node:fs';
import * as asyncfs from 'node:fs/promises';
import * as path from 'node:path';
import * as Path from "node:path"
import NodeFileSystemHandle from './NodeFileSystemHandle.ts';
import NodeFileSystemWritableFileStream from './NodeFileSystemWriteableFileStream.ts';
import mime from 'mime'
@@ -11,23 +11,30 @@ import type {
FileSystemWritableFileStream,
File as WebFile
} from '@sugoidogo/importable-types-web'
import type NodeFileSystemDirectoryHandle from './NodeFileSystemDirectoryHandle.ts';
export default class NodeFileSystemFileHandle extends NodeFileSystemHandle implements FileSystemFileHandle {
declare kind: "file";
constructor(fpath: string, options: FileSystemGetFileOptions={}) {
super()
fpath = path.normalize(fpath)
if (!fs.existsSync(fpath) && options.create) fs.writeFileSync(fpath, '')
if (fs.lstatSync(fpath).isDirectory()) new Error('not a file')
this.path = fpath
constructor(path: string, options: FileSystemGetFileOptions & { origin?: NodeFileSystemDirectoryHandle } = {}) {
super(path,options)
let flags = fs.constants.O_RDWR
if (options.create) flags |= fs.constants.O_CREAT
fs.closeSync(fs.openSync(this.path, flags))
}
async createWritable(options: FileSystemCreateWritableOptions = {}): Promise<FileSystemWritableFileStream> {
return new NodeFileSystemWritableFileStream(this.path,options)
let flags = fs.constants.O_WRONLY
const fileHandle = await asyncfs.open(this.path, flags)
if (!options.keepExistingData) await fileHandle.truncate(0)
return new NodeFileSystemWritableFileStream(fileHandle)
}
async getFile(): Promise<WebFile> {
const type = mime.getType(this.path.split('.').pop())
const stats = await asyncfs.stat(this.path)
const type = mime.getType(this.name.split('.').pop())
const buffer = await asyncfs.readFile(this.path)
return new File([buffer], this.path, { "lastModified": stats.mtimeMs, "type": type }) as any //TODO Node File and Web File have some incompatibilities
const relativePathSegments = await this.origin.resolve(this)
return Object.assign(await fs.openAsBlob(this.path, { "type": type }), {
name: this.name,
webkitRelativePath: relativePathSegments.join(Path.sep),
lastModified: stats.mtimeMs
}) as any
}
}
+11 -8
View File
@@ -1,19 +1,22 @@
import * as path from 'node:path'
import * as Path from 'node:path'
import type {
FileSystemHandle,
FileSystemHandleKind
} from '@sugoidogo/importable-types-web'
import type NodeFileSystemDirectoryHandle from './NodeFileSystemDirectoryHandle';
export default class NodeFileSystemHandle implements FileSystemHandle {
kind: FileSystemHandleKind;
path: string;
get name() { return path.basename(this.path) }
origin: NodeFileSystemDirectoryHandle
get name() { return Path.basename(this.path) }
constructor(path: string, options: { origin?: NodeFileSystemDirectoryHandle } = {}) {
if (!Path.isAbsolute(path)) path = Path.resolve(path)
this.path = path
this.origin = options.origin
}
async isSameEntry(other: FileSystemHandle): Promise<boolean> {
if (typeof other != typeof this) return false
let thisName = this.name
let otherName = other.name
if (!path.isAbsolute(this.name)) thisName = path.join(process.cwd(), this.name)
if (!path.isAbsolute(other.name)) otherName = path.join(process.cwd(), other.name)
return thisName === otherName
if (!(other instanceof NodeFileSystemHandle)) return false
return this.path === other.path
}
}
+25 -29
View File
@@ -1,48 +1,44 @@
import * as fs from 'node:fs'
import * as path from 'node:path'
import { Writable } from 'node:stream'
import type {
FileSystemWritableFileStream,
FileSystemCreateWritableOptions,
FileSystemWriteChunkType,
WriteParams
} from '@sugoidogo/importable-types-web'
import type { FileHandle } from 'node:fs/promises'
function isWriteParams(data: FileSystemWriteChunkType): data is WriteParams {
return ["write", "seek", "truncate"].includes((data as WriteParams).type)
}
export default class NodeFileSystemWritableFileStream extends WritableStream implements FileSystemWritableFileStream {
#seek_position = 0
#fd: number
constructor(name: string, options: FileSystemCreateWritableOptions = {}) {
name = path.normalize(name)
let flags = 'w'
if (options.keepExistingData) flags = 'r+'
const fd = fs.openSync(name, flags)
if (!options.keepExistingData) fs.ftruncateSync(fd, 0)
super(Writable.toWeb(fs.createWriteStream(name, { "fd": fd })))
this.#fd = fd
#position = 0
#fileHandle: FileHandle
constructor(fileHandle: FileHandle) {
const writeStream = fileHandle.createWriteStream({ "encoding": "binary" })
const writeableStream = Writable.toWeb(writeStream)
super(writeableStream)
this.#fileHandle = fileHandle
}
async seek(position: number): Promise<void> {
if (this.locked) throw new Error("stream is locked")
this.#seek_position = position
this.#position = position
}
async truncate(size: number): Promise<void> {
if (this.locked) throw new Error("stream is locked")
return fs.ftruncateSync(this.#fd, size)
return this.#fileHandle.truncate(size)
}
async write(wdata: FileSystemWriteChunkType): Promise<void> {
async write(data: FileSystemWriteChunkType): Promise<void> {
if (this.locked) throw new Error("stream is locked")
if (wdata instanceof Blob) wdata = await wdata.bytes()
if (typeof wdata === 'string') wdata = new TextEncoder().encode(wdata)
let wsize: number = null
let pseek_position = this.#seek_position
if ((wdata as WriteParams).type) {
const { type, position, size, data } = (wdata as WriteParams)
if (type === 'truncate') return this.truncate(size)
if (position) this.#seek_position = position
if (type === 'seek') return
if (size) wsize = size
wdata = data
let offset: number, length: number, position=this.#position
if (isWriteParams(data)) {
if (data.type === "seek") return this.seek(data.position)
if (data.type === "truncate") return this.truncate(data.size)
if (typeof data.position === "number") position = data.position
length = data.size
data = data.data
}
fs.writeSync(this.#fd, (wdata as Uint8Array), null, wsize, this.#seek_position)
this.#seek_position = pseek_position
if (typeof data === 'string') data = new TextEncoder().encode(data)
const { bytesWritten } = await this.#fileHandle.write(data as any, offset, length, position)
this.#position=position+bytesWritten
}
}