Skip to content

Commit 7386a23

Browse files
committed
deno adapter
closes #10
1 parent 8421de3 commit 7386a23

File tree

6 files changed

+282
-40
lines changed

6 files changed

+282
-40
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
What is this?
44

5-
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.
5+
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.
66

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

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

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

6770

6871
// The polyfilled (file input) version will turn into a memory adapter

src/FileSystemDirectoryHandle.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ class FileSystemDirectoryHandle extends FileSystemHandle {
4141
* @param {boolean} [options.create] create the file if don't exist
4242
* @returns {Promise<FileSystemFileHandle>}
4343
*/
44-
async getFileHandle (name, options) {
44+
async getFileHandle (name, options = {}) {
4545
if (name === '') throw new TypeError(`Name can't be an empty string.`)
4646
if (name === '.' || name === '..' || name.includes('/')) throw new TypeError(`Name contains invalid characters.`)
47+
options.create = !!options.create
4748
return new FileSystemFileHandle(await this.#adapter.getFileHandle(name, options))
4849
}
4950

src/FileSystemHandle.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class FileSystemHandle {
2020
if (options.readable) return 'granted'
2121
const handle = this.#adapter
2222
return handle.queryPermission ?
23-
handle.queryPermission(options) :
23+
await handle.queryPermission(options) :
2424
handle.writable
2525
? 'granted'
2626
: 'denied'
@@ -39,7 +39,7 @@ class FileSystemHandle {
3939
* @param {boolean} [options.recursive=false]
4040
*/
4141
async remove (options = {}) {
42-
this.#adapter.remove(options)
42+
await this.#adapter.remove(options)
4343
}
4444

4545
/**

src/adapters/deno.js

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// @ts-check
2+
3+
import { join, basename } from 'https://deno.land/[email protected]/path/mod.ts'
4+
import { errors } from '../util.js'
5+
6+
const { INVALID, GONE, MISMATCH, MOD_ERR, SYNTAX } = errors
7+
8+
// TODO:
9+
// - either depend on fetch-blob.
10+
// - push for https://github.com/denoland/deno/pull/10969
11+
// - or extend the File class like i did in that PR
12+
/** @param {string} path */
13+
async function fileFrom (path) {
14+
const e = Deno.readFileSync(path)
15+
const s = await Deno.stat(path)
16+
return new File([e], basename(path), { lastModified: Number(s.mtime) })
17+
}
18+
19+
export class Sink {
20+
/**
21+
* @param {Deno.File} fileHandle
22+
* @param {number} size
23+
*/
24+
constructor (fileHandle, size) {
25+
this.fileHandle = fileHandle
26+
this.size = size
27+
this.position = 0
28+
}
29+
async abort() {
30+
await this.fileHandle.close()
31+
}
32+
async write (chunk) {
33+
if (typeof chunk === 'object') {
34+
if (chunk.type === 'write') {
35+
if (Number.isInteger(chunk.position) && chunk.position >= 0) {
36+
this.position = chunk.position
37+
}
38+
if (!('data' in chunk)) {
39+
await this.fileHandle.close()
40+
throw new DOMException(...SYNTAX('write requires a data argument'))
41+
}
42+
chunk = chunk.data
43+
} else if (chunk.type === 'seek') {
44+
if (Number.isInteger(chunk.position) && chunk.position >= 0) {
45+
if (this.size < chunk.position) {
46+
throw new DOMException(...INVALID)
47+
}
48+
this.position = chunk.position
49+
return
50+
} else {
51+
await this.fileHandle.close()
52+
throw new DOMException(...SYNTAX('seek requires a position argument'))
53+
}
54+
} else if (chunk.type === 'truncate') {
55+
if (Number.isInteger(chunk.size) && chunk.size >= 0) {
56+
await this.fileHandle.truncate(chunk.size)
57+
this.size = chunk.size
58+
if (this.position > this.size) {
59+
this.position = this.size
60+
}
61+
return
62+
} else {
63+
await this.fileHandle.close()
64+
throw new DOMException(...SYNTAX('truncate requires a size argument'))
65+
}
66+
}
67+
}
68+
69+
if (chunk instanceof ArrayBuffer) {
70+
chunk = new Uint8Array(chunk)
71+
} else if (typeof chunk === 'string') {
72+
chunk = new TextEncoder().encode(chunk)
73+
} else if (chunk instanceof Blob) {
74+
await this.fileHandle.seek(this.position, Deno.SeekMode.Start)
75+
for await (const data of chunk.stream()) {
76+
const written = await this.fileHandle.write(data)
77+
this.position += written
78+
this.size += written
79+
}
80+
return
81+
}
82+
await this.fileHandle.seek(this.position, Deno.SeekMode.Start)
83+
const written = await this.fileHandle.write(chunk)
84+
this.position += written
85+
this.size += written
86+
}
87+
88+
async close () {
89+
await this.fileHandle.close()
90+
}
91+
}
92+
93+
export class FileHandle {
94+
#path
95+
96+
/**
97+
* @param {string} path
98+
* @param {string} name
99+
*/
100+
constructor (path, name) {
101+
this.#path = path
102+
this.name = name
103+
this.kind = 'file'
104+
}
105+
106+
async getFile () {
107+
await Deno.stat(this.#path).catch(err => {
108+
if (err.name === 'NotFound') throw new DOMException(...GONE)
109+
})
110+
return fileFrom(this.#path)
111+
}
112+
113+
isSameEntry (other) {
114+
return this.#path === this.#getPath.apply(other)
115+
}
116+
117+
#getPath() {
118+
return this.#path
119+
}
120+
121+
async createWritable () {
122+
const fileHandle = await Deno.open(this.#path, {write: true}).catch(err => {
123+
if (err.name === 'NotFound') throw new DOMException(...GONE)
124+
throw err
125+
})
126+
const { size } = await fileHandle.stat()
127+
return new Sink(fileHandle, size)
128+
}
129+
}
130+
131+
export class FolderHandle {
132+
#path = ''
133+
134+
/** @param {string} path */
135+
constructor (path, name = '') {
136+
this.name = name
137+
this.kind = 'directory'
138+
this.#path = join(path)
139+
}
140+
141+
isSameEntry (other) {
142+
return this.#path === this.#getPath.apply(other)
143+
}
144+
145+
#getPath() {
146+
return this.#path
147+
}
148+
149+
async * entries () {
150+
const dir = this.#path
151+
try {
152+
for await (const dirEntry of Deno.readDir(dir)) {
153+
const { name } = dirEntry
154+
const path = join(dir, name)
155+
const stat = await Deno.lstat(path)
156+
if (stat.isFile) {
157+
yield [name, new FileHandle(path, name)]
158+
} else if (stat.isDirectory) {
159+
yield [name, new FolderHandle(path, name)]
160+
}
161+
}
162+
} catch (err) {
163+
throw err.name === 'NotFound' ? new DOMException(...GONE) : err
164+
}
165+
}
166+
167+
/**
168+
* @param {string} name
169+
* @param {{create?: boolean}} opts
170+
*/
171+
async getDirectoryHandle (name, opts = {}) {
172+
const path = join(this.#path, name)
173+
const stat = await Deno.lstat(path).catch(err => {
174+
if (err.name !== 'NotFound') throw err
175+
})
176+
const isDirectory = stat?.isDirectory
177+
if (stat && isDirectory) return new FolderHandle(path, name)
178+
if (stat && !isDirectory) throw new DOMException(...MISMATCH)
179+
if (!opts.create) throw new DOMException(...GONE)
180+
await Deno.mkdir(path)
181+
return new FolderHandle(path, name)
182+
}
183+
184+
/**
185+
* @param {string} name
186+
* @param {{ create: any; }} opts
187+
*/
188+
async getFileHandle (name, opts) {
189+
const path = join(this.#path, name)
190+
const stat = await Deno.lstat(path).catch(err => {
191+
if (err.name !== 'NotFound') throw err
192+
})
193+
194+
const isFile = stat?.isFile
195+
if (stat && isFile) return new FileHandle(path, name)
196+
if (stat && !isFile) throw new DOMException(...MISMATCH)
197+
if (!opts.create) throw new DOMException(...GONE)
198+
const c = await Deno.open(path, {
199+
create: true,
200+
write: true,
201+
})
202+
c.close()
203+
return new FileHandle(path, name)
204+
}
205+
206+
queryPermission () {
207+
return 'granted'
208+
}
209+
210+
/**
211+
* @param {string} name
212+
* @param {{ recursive: boolean; }} opts
213+
*/
214+
async removeEntry (name, opts) {
215+
const path = join(this.#path, name)
216+
const stat = await Deno.lstat(path).catch(err => {
217+
if (err.name === 'NotFound') throw new DOMException(...GONE)
218+
throw err
219+
})
220+
221+
if (stat.isDirectory) {
222+
if (opts.recursive) {
223+
await Deno.remove(path, { recursive: true }).catch(err => {
224+
if (err.code === 'ENOTEMPTY') throw new DOMException(...MOD_ERR)
225+
throw err
226+
})
227+
} else {
228+
await Deno.remove(path).catch(() => {
229+
throw new DOMException(...MOD_ERR)
230+
})
231+
}
232+
} else {
233+
await Deno.remove(path)
234+
}
235+
}
236+
}
237+
238+
export default path => new FolderHandle(join(Deno.cwd(), path))

test/test-deno.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as fs from '../src/es6.js'
2+
import steps from './test.js'
3+
import {
4+
cleanupSandboxedFileSystem
5+
} from '../test/util.js'
6+
7+
const { getOriginPrivateDirectory } = fs
8+
9+
async function test(fs, step, root) {
10+
try {
11+
await cleanupSandboxedFileSystem(root)
12+
await step.fn(root)
13+
console.log(`[OK]: ${fs} ${step.desc}`)
14+
} catch (err) {
15+
console.log(`[ERR]: ${fs} ${step.desc}`)
16+
}
17+
}
18+
19+
async function start () {
20+
const root = await getOriginPrivateDirectory(import('../src/adapters/deno.js'), './testfolder')
21+
const memory = await getOriginPrivateDirectory(import('../src/adapters/memory.js'))
22+
23+
for (let step of steps) {
24+
if (step.desc.includes('atomic')) continue
25+
await test('server', step, root).finally()
26+
}
27+
28+
console.log('\n\n\n')
29+
setTimeout(()=>{}, 222222)
30+
31+
// for (let step of steps) {
32+
// await test('memory', step, memory).finally()
33+
// }
34+
}
35+
36+
start()

test/test-node.js

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -34,39 +34,3 @@ async function start () {
3434
}
3535

3636
start()
37-
38-
// globalThis.fs = fs
39-
40-
// async function init () {
41-
// const drivers = await Promise.allSettled([
42-
// getOriginPrivateDirectory(),
43-
// getOriginPrivateDirectory(import('../src/adapters/sandbox.js')),
44-
// getOriginPrivateDirectory(import('../src/adapters/memory.js')),
45-
// getOriginPrivateDirectory(import('../src/adapters/indexeddb.js')),
46-
// getOriginPrivateDirectory(import('../src/adapters/cache.js'))
47-
// ])
48-
// let j = 0
49-
// for (const driver of drivers) {
50-
// j++
51-
// if (driver.status === 'rejected') continue
52-
// const root = driver.value
53-
// await cleanupSandboxedFileSystem(root)
54-
// const total = performance.now()
55-
// for (var i = 0; i < tests.length; i++) {
56-
// const test = tests[i]
57-
// await cleanupSandboxedFileSystem(root)
58-
// const t = performance.now()
59-
// await test.fn(root).then(() => {
60-
// const time = (performance.now() - t).toFixed(3)
61-
// tBody.rows[i].cells[j].innerText = time + 'ms'
62-
// }, err => {
63-
// console.error(err)
64-
// tBody.rows[i].cells[j].innerText = '❌'
65-
// tBody.rows[i].cells[j].title = err.message
66-
// })
67-
// }
68-
// table.tFoot.rows[0].cells[j].innerText = (performance.now() - total).toFixed(3)
69-
// }
70-
// }
71-
72-
// init().catch(console.error)

0 commit comments

Comments
 (0)