Skip to content

Commit

Permalink
deno adapter
Browse files Browse the repository at this point in the history
closes #10
  • Loading branch information
jimmywarting committed Jun 15, 2021
1 parent 8421de3 commit 7386a23
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 40 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

What is this?

This is file system that follows [native-file-system](https://wicg.github.io/native-file-system/) specification. Thanks to it we can have a unified way of handling data in all browser and even in NodeJS in a more secure way.
This is file system that follows [native-file-system](https://wicg.github.io/native-file-system/) specification. Thanks to it we can have a unified way of handling data in all browser and even in NodeJS & Deno in a more secure way.

At a high level what we're providing is several bits:

Expand Down Expand Up @@ -63,6 +63,9 @@ handle = await getOriginPrivateDirectory(import('../src/adapters/cache.js'))
handle = await getOriginPrivateDirectory(import('native-file-system-adapter/src/adapters/memory.js'))
handle = await getOriginPrivateDirectory(import('native-file-system-adapter/src/adapters/node.js'), './starting-path')

// Deno only variant:
handle = await getOriginPrivateDirectory(import('native-file-system-adapter/src/adapters/memory.js'))
handle = await getOriginPrivateDirectory(import('native-file-system-adapter/src/adapters/deno.js'), './starting-path')


// The polyfilled (file input) version will turn into a memory adapter
Expand Down
3 changes: 2 additions & 1 deletion src/FileSystemDirectoryHandle.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ class FileSystemDirectoryHandle extends FileSystemHandle {
* @param {boolean} [options.create] create the file if don't exist
* @returns {Promise<FileSystemFileHandle>}
*/
async getFileHandle (name, options) {
async getFileHandle (name, options = {}) {
if (name === '') throw new TypeError(`Name can't be an empty string.`)
if (name === '.' || name === '..' || name.includes('/')) throw new TypeError(`Name contains invalid characters.`)
options.create = !!options.create
return new FileSystemFileHandle(await this.#adapter.getFileHandle(name, options))
}

Expand Down
4 changes: 2 additions & 2 deletions src/FileSystemHandle.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class FileSystemHandle {
if (options.readable) return 'granted'
const handle = this.#adapter
return handle.queryPermission ?
handle.queryPermission(options) :
await handle.queryPermission(options) :
handle.writable
? 'granted'
: 'denied'
Expand All @@ -39,7 +39,7 @@ class FileSystemHandle {
* @param {boolean} [options.recursive=false]
*/
async remove (options = {}) {
this.#adapter.remove(options)
await this.#adapter.remove(options)
}

/**
Expand Down
238 changes: 238 additions & 0 deletions src/adapters/deno.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// @ts-check

import { join, basename } from 'https://deno.land/[email protected]/path/mod.ts'
import { errors } from '../util.js'

const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX } = errors

// TODO:
// - either depend on fetch-blob.
// - push for https://github.com/denoland/deno/pull/10969
// - or extend the File class like i did in that PR
/** @param {string} path */
async function fileFrom (path) {
const e = Deno.readFileSync(path)
const s = await Deno.stat(path)
return new File([e], basename(path), { lastModified: Number(s.mtime) })
}

export class Sink {
/**
* @param {Deno.File} fileHandle
* @param {number} size
*/
constructor (fileHandle, size) {
this.fileHandle = fileHandle
this.size = size
this.position = 0
}
async abort() {
await this.fileHandle.close()
}
async write (chunk) {
if (typeof chunk === 'object') {
if (chunk.type === 'write') {
if (Number.isInteger(chunk.position) && chunk.position >= 0) {
this.position = chunk.position
}
if (!('data' in chunk)) {
await this.fileHandle.close()
throw new DOMException(...SYNTAX('write requires a data argument'))
}
chunk = chunk.data
} else if (chunk.type === 'seek') {
if (Number.isInteger(chunk.position) && chunk.position >= 0) {
if (this.size < chunk.position) {
throw new DOMException(...INVALID)
}
this.position = chunk.position
return
} else {
await this.fileHandle.close()
throw new DOMException(...SYNTAX('seek requires a position argument'))
}
} else if (chunk.type === 'truncate') {
if (Number.isInteger(chunk.size) && chunk.size >= 0) {
await this.fileHandle.truncate(chunk.size)
this.size = chunk.size
if (this.position > this.size) {
this.position = this.size
}
return
} else {
await this.fileHandle.close()
throw new DOMException(...SYNTAX('truncate requires a size argument'))
}
}
}

if (chunk instanceof ArrayBuffer) {
chunk = new Uint8Array(chunk)
} else if (typeof chunk === 'string') {
chunk = new TextEncoder().encode(chunk)
} else if (chunk instanceof Blob) {
await this.fileHandle.seek(this.position, Deno.SeekMode.Start)
for await (const data of chunk.stream()) {
const written = await this.fileHandle.write(data)
this.position += written
this.size += written
}
return
}
await this.fileHandle.seek(this.position, Deno.SeekMode.Start)
const written = await this.fileHandle.write(chunk)
this.position += written
this.size += written
}

async close () {
await this.fileHandle.close()
}
}

export class FileHandle {
#path

/**
* @param {string} path
* @param {string} name
*/
constructor (path, name) {
this.#path = path
this.name = name
this.kind = 'file'
}

async getFile () {
await Deno.stat(this.#path).catch(err => {
if (err.name === 'NotFound') throw new DOMException(...GONE)
})
return fileFrom(this.#path)
}

isSameEntry (other) {
return this.#path === this.#getPath.apply(other)
}

#getPath() {
return this.#path
}

async createWritable () {
const fileHandle = await Deno.open(this.#path, {write: true}).catch(err => {
if (err.name === 'NotFound') throw new DOMException(...GONE)
throw err
})
const { size } = await fileHandle.stat()
return new Sink(fileHandle, size)
}
}

export class FolderHandle {
#path = ''

/** @param {string} path */
constructor (path, name = '') {
this.name = name
this.kind = 'directory'
this.#path = join(path)
}

isSameEntry (other) {
return this.#path === this.#getPath.apply(other)
}

#getPath() {
return this.#path
}

async * entries () {
const dir = this.#path
try {
for await (const dirEntry of Deno.readDir(dir)) {
const { name } = dirEntry
const path = join(dir, name)
const stat = await Deno.lstat(path)
if (stat.isFile) {
yield [name, new FileHandle(path, name)]
} else if (stat.isDirectory) {
yield [name, new FolderHandle(path, name)]
}
}
} catch (err) {
throw err.name === 'NotFound' ? new DOMException(...GONE) : err
}
}

/**
* @param {string} name
* @param {{create?: boolean}} opts
*/
async getDirectoryHandle (name, opts = {}) {
const path = join(this.#path, name)
const stat = await Deno.lstat(path).catch(err => {
if (err.name !== 'NotFound') throw err
})
const isDirectory = stat?.isDirectory
if (stat && isDirectory) return new FolderHandle(path, name)
if (stat && !isDirectory) throw new DOMException(...MISMATCH)
if (!opts.create) throw new DOMException(...GONE)
await Deno.mkdir(path)
return new FolderHandle(path, name)
}

/**
* @param {string} name
* @param {{ create: any; }} opts
*/
async getFileHandle (name, opts) {
const path = join(this.#path, name)
const stat = await Deno.lstat(path).catch(err => {
if (err.name !== 'NotFound') throw err
})

const isFile = stat?.isFile
if (stat && isFile) return new FileHandle(path, name)
if (stat && !isFile) throw new DOMException(...MISMATCH)
if (!opts.create) throw new DOMException(...GONE)
const c = await Deno.open(path, {
create: true,
write: true,
})
c.close()
return new FileHandle(path, name)
}

queryPermission () {
return 'granted'
}

/**
* @param {string} name
* @param {{ recursive: boolean; }} opts
*/
async removeEntry (name, opts) {
const path = join(this.#path, name)
const stat = await Deno.lstat(path).catch(err => {
if (err.name === 'NotFound') throw new DOMException(...GONE)
throw err
})

if (stat.isDirectory) {
if (opts.recursive) {
await Deno.remove(path, { recursive: true }).catch(err => {
if (err.code === 'ENOTEMPTY') throw new DOMException(...MOD_ERR)
throw err
})
} else {
await Deno.remove(path).catch(() => {
throw new DOMException(...MOD_ERR)
})
}
} else {
await Deno.remove(path)
}
}
}

export default path => new FolderHandle(join(Deno.cwd(), path))
36 changes: 36 additions & 0 deletions test/test-deno.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as fs from '../src/es6.js'
import steps from './test.js'
import {
cleanupSandboxedFileSystem
} from '../test/util.js'

const { getOriginPrivateDirectory } = fs

async function test(fs, step, root) {
try {
await cleanupSandboxedFileSystem(root)
await step.fn(root)
console.log(`[OK]: ${fs} ${step.desc}`)
} catch (err) {
console.log(`[ERR]: ${fs} ${step.desc}`)
}
}

async function start () {
const root = await getOriginPrivateDirectory(import('../src/adapters/deno.js'), './testfolder')
const memory = await getOriginPrivateDirectory(import('../src/adapters/memory.js'))

for (let step of steps) {
if (step.desc.includes('atomic')) continue
await test('server', step, root).finally()
}

console.log('\n\n\n')
setTimeout(()=>{}, 222222)

// for (let step of steps) {
// await test('memory', step, memory).finally()
// }
}

start()
36 changes: 0 additions & 36 deletions test/test-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,39 +34,3 @@ async function start () {
}

start()

// globalThis.fs = fs

// async function init () {
// const drivers = await Promise.allSettled([
// getOriginPrivateDirectory(),
// getOriginPrivateDirectory(import('../src/adapters/sandbox.js')),
// getOriginPrivateDirectory(import('../src/adapters/memory.js')),
// getOriginPrivateDirectory(import('../src/adapters/indexeddb.js')),
// getOriginPrivateDirectory(import('../src/adapters/cache.js'))
// ])
// let j = 0
// for (const driver of drivers) {
// j++
// if (driver.status === 'rejected') continue
// const root = driver.value
// await cleanupSandboxedFileSystem(root)
// const total = performance.now()
// for (var i = 0; i < tests.length; i++) {
// const test = tests[i]
// await cleanupSandboxedFileSystem(root)
// const t = performance.now()
// await test.fn(root).then(() => {
// const time = (performance.now() - t).toFixed(3)
// tBody.rows[i].cells[j].innerText = time + 'ms'
// }, err => {
// console.error(err)
// tBody.rows[i].cells[j].innerText = '❌'
// tBody.rows[i].cells[j].title = err.message
// })
// }
// table.tFoot.rows[0].cells[j].innerText = (performance.now() - total).toFixed(3)
// }
// }

// init().catch(console.error)

0 comments on commit 7386a23

Please sign in to comment.