Skip to content

Commit 38260e3

Browse files
authored
Merge pull request #280 from effigies/proxy-files
rf: Make BIDSFile generic wrapper, with FileOpener providing the file type handling
2 parents cfb1015 + f386df0 commit 38260e3

33 files changed

+356
-303
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Changed
2+
3+
- Refactored file access classes to be more DRY.

src/files/access.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ function IOErrorToIssue(err: { code: string; name: string }): Issue {
1010
return { code: 'FILE_READ', subCode: err.name, issueMessage }
1111
}
1212

13-
export function openStream(file: BIDSFile): ReadableStream<Uint8Array<ArrayBuffer>> {
14-
try {
15-
return file.stream
16-
} catch (err: any) {
13+
export async function openStream(
14+
file: BIDSFile,
15+
): Promise<ReadableStream<Uint8Array<ArrayBuffer>>> {
16+
return file.stream().catch((err: any) => {
1717
throw { location: file.path, ...IOErrorToIssue(err) }
18-
}
18+
})
1919
}
2020

2121
export async function readBytes(

src/files/browser.ts

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,17 @@
1-
import { type BIDSFile, FileTree } from '../types/filetree.ts'
1+
import { BIDSFile, FileTree } from '../types/filetree.ts'
22
import { filesToTree } from './filetree.ts'
33
import { FileIgnoreRules, readBidsIgnore } from './ignore.ts'
4-
import { parse, SEPARATOR_PATTERN } from '@std/path'
5-
import * as posix from '@std/path/posix'
4+
import { BrowserFileOpener } from './openers.ts'
65

76
/**
87
* Browser implement of BIDSFile wrapping native File/FileList types
98
*/
10-
export class BIDSFileBrowser implements BIDSFile {
11-
#ignore: FileIgnoreRules
12-
#file: File
13-
name: string
14-
path: string
15-
#parent!: WeakRef<FileTree>
16-
viewed: boolean = false
17-
9+
export class BIDSFileBrowser extends BIDSFile {
1810
constructor(file: File, ignore: FileIgnoreRules, parent?: FileTree) {
19-
this.#file = file
20-
this.#ignore = ignore
21-
this.name = file.name
22-
const relativePath = this.#file.webkitRelativePath
11+
const relativePath = file.webkitRelativePath
2312
const prefixLength = relativePath.indexOf('/')
24-
this.path = relativePath.substring(prefixLength)
25-
this.parent = parent ?? new FileTree('', '/', undefined)
26-
}
27-
28-
get parent(): FileTree {
29-
return this.#parent.deref() as FileTree
30-
}
31-
32-
set parent(tree: FileTree) {
33-
this.#parent = new WeakRef(tree)
34-
}
35-
36-
get size(): number {
37-
return this.#file.size
38-
}
39-
40-
get stream(): ReadableStream<Uint8Array<ArrayBuffer>> {
41-
return this.#file.stream() as ReadableStream<Uint8Array<ArrayBuffer>>
42-
}
43-
44-
get ignored(): boolean {
45-
return this.#ignore.test(this.path)
46-
}
47-
48-
text(): Promise<string> {
49-
return this.#file.text()
50-
}
51-
52-
async readBytes(size: number, offset = 0): Promise<Uint8Array<ArrayBuffer>> {
53-
return new Uint8Array(await this.#file.slice(offset, size).arrayBuffer())
13+
const opener = new BrowserFileOpener(file)
14+
super(relativePath.substring(prefixLength), opener, ignore, parent)
5415
}
5516
}
5617

src/files/deno.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Deno.test('Deno implementation of BIDSFile', async (t) => {
3131
})
3232
await t.step('can be read as ReadableStream', async () => {
3333
const file = new BIDSFileDeno(testDir, testFilename, ignore)
34-
const stream = file.stream
34+
const stream = await file.stream()
3535
const streamReader = stream.getReader()
3636
const denoReader = readerFromStreamReader(streamReader)
3737
const fileBuffer = await readAll(denoReader)

src/files/deno.ts

Lines changed: 7 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -3,105 +3,14 @@
33
*/
44
import { basename, join } from '@std/path'
55
import * as posix from '@std/path/posix'
6-
import { type BIDSFile, FileTree } from '../types/filetree.ts'
6+
import { BIDSFile, FileTree } from '../types/filetree.ts'
77
import { requestReadPermission } from '../setup/requestPermissions.ts'
88
import { FileIgnoreRules, readBidsIgnore } from './ignore.ts'
9-
import { logger } from '../utils/logger.ts'
10-
import { createUTF8Stream } from './streams.ts'
11-
export { type BIDSFile, FileTree }
12-
13-
/**
14-
* Deno implementation of BIDSFile
15-
*/
16-
export class BIDSFileDeno implements BIDSFile {
17-
#ignore: FileIgnoreRules
18-
name: string
19-
path: string
20-
#parent!: WeakRef<FileTree>
21-
#fileInfo?: Deno.FileInfo
22-
#datasetAbsPath: string
23-
viewed: boolean = false
9+
import { FsFileOpener } from './openers.ts'
2410

11+
export class BIDSFileDeno extends BIDSFile {
2512
constructor(datasetPath: string, path: string, ignore?: FileIgnoreRules, parent?: FileTree) {
26-
this.#datasetAbsPath = datasetPath
27-
this.path = path
28-
this.name = basename(path)
29-
this.#ignore = ignore ?? new FileIgnoreRules([])
30-
try {
31-
this.#fileInfo = Deno.statSync(this._getPath())
32-
} catch (error) {
33-
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
34-
this.#fileInfo = Deno.lstatSync(this._getPath())
35-
}
36-
}
37-
this.parent = parent ?? new FileTree('', '/', undefined)
38-
}
39-
40-
private _getPath(): string {
41-
return join(this.#datasetAbsPath, this.path)
42-
}
43-
44-
get parent(): FileTree {
45-
return this.#parent.deref() as FileTree
46-
}
47-
48-
set parent(tree: FileTree) {
49-
this.#parent = new WeakRef(tree)
50-
}
51-
52-
get size(): number {
53-
return this.#fileInfo ? this.#fileInfo.size : -1
54-
}
55-
56-
get stream(): ReadableStream<Uint8Array<ArrayBuffer>> {
57-
const handle = this.#openHandle()
58-
return handle.readable
59-
}
60-
61-
get ignored(): boolean {
62-
return this.#ignore.test(this.path)
63-
}
64-
65-
/**
66-
* Read the entire file and decode as utf-8 text
67-
*/
68-
async text(): Promise<string> {
69-
const reader = this.stream.pipeThrough(createUTF8Stream()).getReader()
70-
const chunks: string[] = []
71-
try {
72-
while (true) {
73-
const { done, value } = await reader.read()
74-
if (done) break
75-
chunks.push(value)
76-
}
77-
return chunks.join('')
78-
} finally {
79-
reader.releaseLock()
80-
}
81-
}
82-
83-
/**
84-
* Read bytes in a range efficiently from a given file
85-
*
86-
* Reads up to size bytes, starting at offset.
87-
* If EOF is encountered, the resulting array may be smaller.
88-
*/
89-
async readBytes(size: number, offset = 0): Promise<Uint8Array<ArrayBuffer>> {
90-
const handle = this.#openHandle()
91-
const buf = new Uint8Array(size)
92-
await handle.seek(offset, Deno.SeekMode.Start)
93-
const nbytes = await handle.read(buf) ?? 0
94-
handle.close()
95-
return buf.subarray(0, nbytes)
96-
}
97-
98-
/**
99-
* Return a Deno file handle
100-
*/
101-
#openHandle(): Deno.FsFile {
102-
// Avoid asking for write access
103-
const openOptions = { read: true, write: false }
104-
return Deno.openSync(this._getPath(), openOptions)
13+
super(path, new FsFileOpener(datasetPath, path), ignore, parent)
10514
}
10615
}
10716

@@ -122,12 +31,12 @@ async function _readFileTree(
12231
continue
12332
}
12433
if (dirEntry.isFile || dirEntry.isSymlink) {
125-
const file = new BIDSFileDeno(
126-
rootPath,
34+
const file = new BIDSFile(
12735
thisPath,
36+
new FsFileOpener(rootPath, thisPath),
12837
ignore,
38+
tree,
12939
)
130-
file.parent = tree
13140
tree.files.push(file)
13241
}
13342
if (dirEntry.isDirectory) {

src/files/filetree.test.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
11
import { assert, assertEquals } from '@std/assert'
2-
import type { FileIgnoreRules } from './ignore.ts'
3-
import type { BIDSFile, FileTree } from '../types/filetree.ts'
4-
import { filesToTree, pathsToTree } from './filetree.ts'
2+
import { FileIgnoreRules } from './ignore.ts'
3+
import { BIDSFile, type FileOpener, type FileTree } from '../types/filetree.ts'
4+
import { filesToTree } from './filetree.ts'
5+
import { asyncStreamFromString } from '../tests/utils.ts'
6+
7+
class NullFileOpener implements FileOpener {
8+
size = 0
9+
stream = () => asyncStreamFromString('')
10+
text = () => Promise.resolve('')
11+
readBytes = async (size: number, offset?: number) => new Uint8Array()
12+
}
13+
14+
export function pathToFile(path: string, ignored: boolean = false): BIDSFile {
15+
const name = path.split('/').pop() as string
16+
return new BIDSFile(path, new NullFileOpener(), ignored)
17+
}
18+
19+
export function pathsToTree(paths: string[], ignore?: string[]): FileTree {
20+
const ignoreRules = new FileIgnoreRules(ignore ?? [])
21+
return filesToTree(paths.map((path) => pathToFile(path, ignoreRules.test(path))))
22+
}
523

624
Deno.test('FileTree generation', async (t) => {
725
await t.step('converts a basic list', async () => {

src/files/filetree.ts

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,8 @@
11
import { parse, SEPARATOR_PATTERN } from '@std/path'
22
import * as posix from '@std/path/posix'
3-
import { type BIDSFile, FileTree } from '../types/filetree.ts'
3+
import { BIDSFile, FileTree } from '../types/filetree.ts'
44
import { FileIgnoreRules } from './ignore.ts'
55

6-
const nullFile = {
7-
size: 0,
8-
stream: new ReadableStream({
9-
start(controller) {
10-
controller.close()
11-
},
12-
}),
13-
text: () => Promise.resolve(''),
14-
readBytes: async (size: number, offset?: number) => new Uint8Array(),
15-
parent: new FileTree('', '/'),
16-
viewed: false,
17-
}
18-
19-
export function pathToFile(path: string, ignored: boolean = false): BIDSFile {
20-
const name = path.split('/').pop() as string
21-
return { name, path, ignored, ...nullFile }
22-
}
23-
24-
export function pathsToTree(paths: string[], ignore?: string[]): FileTree {
25-
const ignoreRules = new FileIgnoreRules(ignore ?? [])
26-
return filesToTree(paths.map((path) => pathToFile(path, ignoreRules.test(path))))
27-
}
28-
296
export function filesToTree(fileList: BIDSFile[], ignore?: FileIgnoreRules): FileTree {
307
ignore = ignore ?? new FileIgnoreRules([])
318
const tree: FileTree = new FileTree('/', '/')

src/files/inheritance.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { assert, assertEquals, assertThrows } from '@std/assert'
22
import type { BIDSFile } from '../types/filetree.ts'
3-
import { pathsToTree } from './filetree.ts'
3+
import { pathsToTree } from './filetree.test.ts'
44
import { walkBack } from './inheritance.ts'
55

66
Deno.test('walkback inheritance tests', async (t) => {

src/files/json.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { BIDSFile } from '../types/filetree.ts'
33
import type { FileIgnoreRules } from './ignore.ts'
44
import { testAsyncFileAccess } from './access.test.ts'
55

6-
import { pathsToTree } from '../files/filetree.ts'
6+
import { pathsToTree } from '../files/filetree.test.ts'
77
import { loadJSON } from './json.ts'
88

99
function encodeUTF16(text: string) {

0 commit comments

Comments
 (0)