Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00e027ef71 | ||
|
|
8bc2c7d70c | ||
|
|
e99fc35e25 | ||
|
|
bbd975feb6 | ||
|
|
1bd153de5b |
+14
-7
@@ -1,14 +1,21 @@
|
|||||||
import NodeFileSystemDirectoryHandle from '../dist/NodeFileSystemDirectoryHandle.js'
|
import NodeFileSystemDirectoryHandle from '../src/NodeFileSystemDirectoryHandle.ts'
|
||||||
|
|
||||||
|
const textText="Hello World!"
|
||||||
const cwd_directory_handle = new NodeFileSystemDirectoryHandle('.')
|
const cwd_directory_handle = new NodeFileSystemDirectoryHandle('.')
|
||||||
const test_directory_handle = await cwd_directory_handle.getDirectoryHandle('test', { 'create': true })
|
const test_directory_handle = await cwd_directory_handle.getDirectoryHandle('test', { 'create': true })
|
||||||
const file_handle = await test_directory_handle.getFileHandle('test.txt', { 'create': true })
|
for (let i = 0; i < 10000; i++) {
|
||||||
const writable = await file_handle.createWritable()
|
const file_handle = await test_directory_handle.getFileHandle(i+'.txt', { 'create': true })
|
||||||
writable.write('Hello World!')
|
let writable = await file_handle.createWritable()
|
||||||
writable.close()
|
await writable.write(textText+textText)
|
||||||
|
await writable.close()
|
||||||
|
writable = await file_handle.createWritable()
|
||||||
|
await writable.write(textText)
|
||||||
|
await writable.close()
|
||||||
const file = await file_handle.getFile()
|
const file = await file_handle.getFile()
|
||||||
console.log(file.type)
|
console.log(file.webkitRelativePath)
|
||||||
console.log(await file.text())
|
const text = await file.text()
|
||||||
|
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')
|
const result = await cwd_directory_handle.getFileHandle('test/test.txt').catch(error => 'blocked path traversal')
|
||||||
if (typeof result === 'string') console.log(result)
|
if (typeof result === 'string') console.log(result)
|
||||||
else console.error('path traversal not blocked')
|
else console.error('path traversal not blocked')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as asyncfs from 'node:fs/promises';
|
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 NodeFileSystemHandle from './NodeFileSystemHandle.ts';
|
||||||
import NodeFileSystemFileHandle from './NodeFileSystemFileHandle.ts';
|
import NodeFileSystemFileHandle from './NodeFileSystemFileHandle.ts';
|
||||||
import type {
|
import type {
|
||||||
@@ -14,20 +14,23 @@ import type {
|
|||||||
|
|
||||||
export default class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle implements FileSystemDirectoryHandle {
|
export default class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle implements FileSystemDirectoryHandle {
|
||||||
declare kind: "directory";
|
declare kind: "directory";
|
||||||
constructor(fpath: string, options: FileSystemGetDirectoryOptions = {}) {
|
constructor(path: string, options: FileSystemGetDirectoryOptions & { origin?: NodeFileSystemDirectoryHandle } = {}) {
|
||||||
super()
|
super(path, options)
|
||||||
fpath = path.normalize(fpath)
|
try {
|
||||||
if (!fs.existsSync(fpath) && options.create) fs.mkdirSync(fpath)
|
const stats = fs.statSync(this.path)
|
||||||
if (!fs.lstatSync(fpath).isDirectory()) throw new Error('not a directory')
|
if (!stats.isDirectory()) throw new Error("tried to open file as directory: " + this.path)
|
||||||
this.path = fpath
|
} catch (error) {
|
||||||
|
if (options.create) fs.mkdirSync(this.path)
|
||||||
|
else throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entries(): FileSystemDirectoryHandleAsyncIterator<[string, NodeFileSystemDirectoryHandle | NodeFileSystemFileHandle]> {
|
entries(): FileSystemDirectoryHandleAsyncIterator<[string, NodeFileSystemDirectoryHandle | NodeFileSystemFileHandle]> {
|
||||||
const entries: Array<[string, NodeFileSystemDirectoryHandle | NodeFileSystemFileHandle]> = []
|
const entries: Array<[string, NodeFileSystemDirectoryHandle | NodeFileSystemFileHandle]> = []
|
||||||
fs.readdirSync(this.path).forEach(name => {
|
fs.readdirSync(this.path).forEach(name => {
|
||||||
if (fs.lstatSync(name).isDirectory()) {
|
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 {
|
} 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]
|
return entries[Symbol.asyncIterator]
|
||||||
@@ -39,37 +42,33 @@ export default class NodeFileSystemDirectoryHandle extends NodeFileSystemHandle
|
|||||||
const values: Array<NodeFileSystemDirectoryHandle | NodeFileSystemFileHandle> = [];
|
const values: Array<NodeFileSystemDirectoryHandle | NodeFileSystemFileHandle> = [];
|
||||||
fs.readdirSync(this.path).forEach(name => {
|
fs.readdirSync(this.path).forEach(name => {
|
||||||
if (fs.lstatSync(name).isDirectory()) {
|
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 {
|
} 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]
|
return values[Symbol.asyncIterator]
|
||||||
}
|
}
|
||||||
#getValidatedPath(name: string) {
|
#getValidatedPath(name: string) {
|
||||||
name = path.normalize(name)
|
name = Path.normalize(name)
|
||||||
if (name.includes(path.sep)) throw new TypeError('Invalid Filename')
|
if (name.includes(Path.sep)) throw new TypeError('Invalid Filename')
|
||||||
return path.join(this.path, name)
|
return Path.join(this.path, name)
|
||||||
}
|
}
|
||||||
async getDirectoryHandle(name: string, options: FileSystemGetDirectoryOptions = {}): Promise<NodeFileSystemDirectoryHandle> {
|
async getDirectoryHandle(name: string, options: FileSystemGetDirectoryOptions = {}): Promise<NodeFileSystemDirectoryHandle> {
|
||||||
|
Object.assign(options, { origin: this.origin || this })
|
||||||
return new NodeFileSystemDirectoryHandle(this.#getValidatedPath(name), options)
|
return new NodeFileSystemDirectoryHandle(this.#getValidatedPath(name), options)
|
||||||
}
|
}
|
||||||
async getFileHandle(name: string, options: FileSystemGetFileOptions = {}): Promise<NodeFileSystemFileHandle> {
|
async getFileHandle(name: string, options: FileSystemGetFileOptions = {}): Promise<NodeFileSystemFileHandle> {
|
||||||
|
Object.assign(options, { origin: this.origin || this })
|
||||||
return new NodeFileSystemFileHandle(this.#getValidatedPath(name), options)
|
return new NodeFileSystemFileHandle(this.#getValidatedPath(name), options)
|
||||||
}
|
}
|
||||||
async removeEntry(name: string, options: FileSystemRemoveOptions = { recursive: false }): Promise<void> {
|
async removeEntry(name: string, options: FileSystemRemoveOptions = { recursive: false }): Promise<void> {
|
||||||
return asyncfs.rm(this.#getValidatedPath(name), { recursive: options.recursive, force: options.recursive })
|
return asyncfs.rm(this.#getValidatedPath(name), { recursive: options.recursive, force: options.recursive })
|
||||||
}
|
}
|
||||||
async resolve(possibleDescendant: FileSystemHandle): Promise<string[] | null> {
|
async resolve(possibleDescendant: FileSystemHandle): Promise<string[] | null> {
|
||||||
if (typeof possibleDescendant !== typeof this) return null
|
if (!(possibleDescendant instanceof NodeFileSystemHandle)) return null
|
||||||
let thisName = this.path
|
if (!possibleDescendant.path.startsWith(this.path)) return null
|
||||||
let possibleDescendantName = possibleDescendant.name
|
return possibleDescendant.path.substring(this.path.length + 1).split(Path.sep)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
[Symbol.asyncIterator] = this.entries
|
[Symbol.asyncIterator] = this.entries
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as asyncfs from 'node:fs/promises';
|
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 NodeFileSystemHandle from './NodeFileSystemHandle.ts';
|
||||||
import NodeFileSystemWritableFileStream from './NodeFileSystemWriteableFileStream.ts';
|
import NodeFileSystemWritableFileStream from './NodeFileSystemWriteableFileStream.ts';
|
||||||
import mime from 'mime'
|
import NonBufferedFile from './NonBufferedFile.ts'
|
||||||
import type {
|
import type {
|
||||||
FileSystemFileHandle,
|
FileSystemFileHandle,
|
||||||
FileSystemGetFileOptions,
|
FileSystemGetFileOptions,
|
||||||
@@ -11,23 +11,24 @@ import type {
|
|||||||
FileSystemWritableFileStream,
|
FileSystemWritableFileStream,
|
||||||
File as WebFile
|
File as WebFile
|
||||||
} from '@sugoidogo/importable-types-web'
|
} from '@sugoidogo/importable-types-web'
|
||||||
|
import type NodeFileSystemDirectoryHandle from './NodeFileSystemDirectoryHandle.ts';
|
||||||
|
|
||||||
export default class NodeFileSystemFileHandle extends NodeFileSystemHandle implements FileSystemFileHandle {
|
export default class NodeFileSystemFileHandle extends NodeFileSystemHandle implements FileSystemFileHandle {
|
||||||
declare kind: "file";
|
declare kind: "file";
|
||||||
constructor(fpath: string, options: FileSystemGetFileOptions={}) {
|
constructor(path: string, options: FileSystemGetFileOptions & { origin?: NodeFileSystemDirectoryHandle } = {}) {
|
||||||
super()
|
super(path,options)
|
||||||
fpath = path.normalize(fpath)
|
let flags = fs.constants.O_RDWR
|
||||||
if (!fs.existsSync(fpath) && options.create) fs.writeFileSync(fpath, '')
|
if (options.create) flags |= fs.constants.O_CREAT
|
||||||
if (fs.lstatSync(fpath).isDirectory()) new Error('not a file')
|
fs.closeSync(fs.openSync(this.path, flags))
|
||||||
this.path = fpath
|
|
||||||
}
|
}
|
||||||
async createWritable(options: FileSystemCreateWritableOptions = {}): Promise<FileSystemWritableFileStream> {
|
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> {
|
async getFile(): Promise<WebFile> {
|
||||||
const stats = await asyncfs.stat(this.path)
|
const webkitRelativePathSegments=await this.origin.resolve(this)
|
||||||
const type = mime.getType(this.name.split('.').pop())
|
return new NonBufferedFile(this.path,webkitRelativePathSegments.join(Path.sep))
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,22 @@
|
|||||||
import * as path from 'node:path'
|
import * as Path from 'node:path'
|
||||||
import type {
|
import type {
|
||||||
FileSystemHandle,
|
FileSystemHandle,
|
||||||
FileSystemHandleKind
|
FileSystemHandleKind
|
||||||
} from '@sugoidogo/importable-types-web'
|
} from '@sugoidogo/importable-types-web'
|
||||||
|
import type NodeFileSystemDirectoryHandle from './NodeFileSystemDirectoryHandle';
|
||||||
|
|
||||||
export default class NodeFileSystemHandle implements FileSystemHandle {
|
export default class NodeFileSystemHandle implements FileSystemHandle {
|
||||||
kind: FileSystemHandleKind;
|
kind: FileSystemHandleKind;
|
||||||
path: string;
|
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> {
|
async isSameEntry(other: FileSystemHandle): Promise<boolean> {
|
||||||
if (typeof other != typeof this) return false
|
if (!(other instanceof NodeFileSystemHandle)) return false
|
||||||
let thisName = this.name
|
return this.path === other.path
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,45 +1,44 @@
|
|||||||
import * as fs from 'node:fs'
|
|
||||||
import * as asyncfs from 'node:fs/promises'
|
|
||||||
import * as path from 'node:path'
|
|
||||||
import { Writable } from 'node:stream'
|
import { Writable } from 'node:stream'
|
||||||
import type {
|
import type {
|
||||||
FileSystemWritableFileStream,
|
FileSystemWritableFileStream,
|
||||||
FileSystemCreateWritableOptions,
|
|
||||||
FileSystemWriteChunkType,
|
FileSystemWriteChunkType,
|
||||||
WriteParams
|
WriteParams
|
||||||
} from '@sugoidogo/importable-types-web'
|
} 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 {
|
export default class NodeFileSystemWritableFileStream extends WritableStream implements FileSystemWritableFileStream {
|
||||||
#seek_position = 0
|
#position = 0
|
||||||
#name: string
|
#fileHandle: FileHandle
|
||||||
constructor(name: string, options: FileSystemCreateWritableOptions={}) {
|
constructor(fileHandle: FileHandle) {
|
||||||
name = path.normalize(name)
|
const writeStream = fileHandle.createWriteStream({ "encoding": "binary" })
|
||||||
super(Writable.toWeb(fs.createWriteStream(name)))
|
const writeableStream = Writable.toWeb(writeStream)
|
||||||
this.#name = name
|
super(writeableStream)
|
||||||
if(!options.keepExistingData) this.truncate(0)
|
this.#fileHandle = fileHandle
|
||||||
}
|
}
|
||||||
async seek(position: number): Promise<void> {
|
async seek(position: number): Promise<void> {
|
||||||
this.#seek_position = position
|
if (this.locked) throw new Error("stream is locked")
|
||||||
|
this.#position = position
|
||||||
}
|
}
|
||||||
truncate(size: number): Promise<void> {
|
async truncate(size: number): Promise<void> {
|
||||||
return asyncfs.truncate(this.#name, size)
|
if (this.locked) throw new Error("stream is locked")
|
||||||
|
return this.#fileHandle.truncate(size)
|
||||||
}
|
}
|
||||||
async write(wdata: FileSystemWriteChunkType): Promise<void> {
|
async write(data: FileSystemWriteChunkType): Promise<void> {
|
||||||
if (wdata instanceof Blob) wdata = await wdata.bytes()
|
if (this.locked) throw new Error("stream is locked")
|
||||||
if (typeof wdata === 'string') wdata = new TextEncoder().encode(wdata)
|
let offset: number, length: number, position=this.#position
|
||||||
let wsize: number = null
|
if (isWriteParams(data)) {
|
||||||
let pseek_position = this.#seek_position
|
if (data.type === "seek") return this.seek(data.position)
|
||||||
if ((wdata as WriteParams).type) {
|
if (data.type === "truncate") return this.truncate(data.size)
|
||||||
const { type, position, size, data } = (wdata as WriteParams)
|
if (typeof data.position === "number") position = data.position
|
||||||
if (type === 'truncate') return asyncfs.truncate(this.#name, size)
|
length = data.size
|
||||||
if (position) this.#seek_position = position
|
data = data.data
|
||||||
if (type === 'seek') return
|
|
||||||
if (size) wsize = size
|
|
||||||
wdata = data
|
|
||||||
}
|
}
|
||||||
const file = await asyncfs.open(this.#name,'r+')
|
if (typeof data === 'string') data = new TextEncoder().encode(data)
|
||||||
await file.write((wdata as Uint8Array), null, wsize, this.#seek_position)
|
const { bytesWritten } = await this.#fileHandle.write(data as any, offset, length, position)
|
||||||
await file.close()
|
this.#position=position+bytesWritten
|
||||||
this.#seek_position = pseek_position
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import * as fs from "node:fs"
|
||||||
|
import * as asyncfs from "node:fs/promises"
|
||||||
|
import * as Path from "node:path"
|
||||||
|
import mime from "mime"
|
||||||
|
import type { Blob as WebBlob, File as WebFile, ReadableStream } from "@sugoidogo/importable-types-web";
|
||||||
|
import { Blob } from "node:buffer";
|
||||||
|
import { Readable } from "node:stream";
|
||||||
|
|
||||||
|
export default class NonBufferedFile implements WebFile {
|
||||||
|
webkitRelativePath: string;
|
||||||
|
#path: string
|
||||||
|
constructor(path: string, webkitRelativePath:string) {
|
||||||
|
this.#path = Path.resolve(path)
|
||||||
|
this.webkitRelativePath=webkitRelativePath
|
||||||
|
}
|
||||||
|
get lastModified() {
|
||||||
|
return fs.statSync(this.#path).mtimeMs
|
||||||
|
}
|
||||||
|
get size() {
|
||||||
|
return fs.statSync(this.#path).size
|
||||||
|
}
|
||||||
|
get type() {
|
||||||
|
return mime.getType(this.#path)
|
||||||
|
}
|
||||||
|
get name() {
|
||||||
|
return Path.basename(this.#path)
|
||||||
|
}
|
||||||
|
async bytes(): Promise<Uint8Array<ArrayBuffer>> {
|
||||||
|
return asyncfs.readFile(this.#path)
|
||||||
|
}
|
||||||
|
async arrayBuffer(): Promise<ArrayBuffer> {
|
||||||
|
const bytes = await asyncfs.readFile(this.#path)
|
||||||
|
return bytes.buffer
|
||||||
|
}
|
||||||
|
async text(): Promise<string> {
|
||||||
|
return asyncfs.readFile(this.#path,"utf8")
|
||||||
|
}
|
||||||
|
slice(start?: number, end?: number, contentType?: string): WebBlob {
|
||||||
|
const fd = fs.openSync(this.#path, fs.constants.O_RDONLY)
|
||||||
|
const buffer=new Uint8Array()
|
||||||
|
fs.readSync(fd, buffer, { "position": start, "length": end - start })
|
||||||
|
fs.closeSync(fd)
|
||||||
|
return new Blob([buffer],{"type":contentType||this.type}) as any
|
||||||
|
}
|
||||||
|
stream(): ReadableStream<Uint8Array<ArrayBuffer>> {
|
||||||
|
return Readable.toWeb(fs.createReadStream(this.#path)) as any
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user