bids-validator-deno 2.1.1__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of bids-validator-deno might be problematic. Click here for more details.
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/PKG-INFO +1 -1
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/deno.json +5 -3
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/pyproject.toml +1 -1
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/access.ts +5 -5
- bids_validator_deno-2.2.0/src/files/browser.ts +34 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/deno.test.ts +1 -1
- bids_validator_deno-2.2.0/src/files/deno.ts +99 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/filetree.test.ts +21 -3
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/filetree.ts +1 -24
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/inheritance.test.ts +1 -1
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/json.test.ts +1 -1
- bids_validator_deno-2.2.0/src/files/openers.ts +128 -0
- bids_validator_deno-2.2.0/src/files/repo.ts +141 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/tsv.test.ts +37 -35
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/tsv.ts +4 -2
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/issues/datasetIssues.test.ts +1 -1
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/issues/list.ts +0 -1
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/main.ts +8 -3
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/applyRules.ts +1 -5
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/datatypes.test.ts +1 -1
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/expressionLanguage.test.ts +27 -13
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/fixtures.test.ts +1 -1
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/tables.test.ts +1 -5
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/walk.test.ts +1 -1
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/walk.ts +21 -14
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/setup/options.test.ts +1 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/setup/options.ts +16 -2
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/local/hed-integration.test.ts +4 -4
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/regression.test.ts +4 -4
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/simple-dataset.ts +1 -1
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/utils.ts +6 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/types/filetree.ts +67 -19
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/utils/output.ts +3 -2
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/validators/bids.test.ts +1 -1
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/validators/citation.test.ts +1 -1
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/validators/filenameCase.test.ts +3 -3
- bids_validator_deno-2.2.0/src/validators/filenameCase.ts +16 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/validators/filenameValidate.test.ts +1 -1
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/validators/validateFiles.test.ts +1 -1
- bids_validator_deno-2.1.1/src/files/browser.ts +0 -65
- bids_validator_deno-2.1.1/src/files/deno.ts +0 -158
- bids_validator_deno-2.1.1/src/validators/filenameCase.ts +0 -16
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/LICENSE +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/README.md +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/pdm_build.py +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/.git-meta.json +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/bids-validator.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/access.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/browser.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/dwi.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/dwi.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/gzip.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/gzip.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/ignore.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/ignore.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/inheritance.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/json.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/nifti.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/nifti.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/streams.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/streams.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/tiff.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/files/tiff.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/issues/datasetIssues.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/issues/list.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/applyRules.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/associations.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/context.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/context.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/datatypes.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/entities.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/entities.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/expressionLanguage.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/schema/tables.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/setup/loadSchema.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/setup/loadSchema.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/setup/requestPermissions.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/summary/collectSubjectMetadata.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/summary/summary.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/summary/summary.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/README.md +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/bom-utf16.tsv +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/bom-utf8.json +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/generate-filenames.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/local/bids_examples.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/local/common.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/local/derivatives.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/local/empty_files.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/local/nifti_rules.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/local/valid_dataset.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/local/valid_filenames.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/local/valid_headers.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/nullReadBytes.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/tests/schema-expression-language.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/types/check.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/types/columns.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/types/columns.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/types/issues.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/types/schema.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/types/validation-result.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/utils/errors.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/utils/logger.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/utils/logger.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/utils/memoize.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/utils/objectPathHandler.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/validators/bids.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/validators/citation.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/validators/filenameIdentify.test.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/validators/filenameIdentify.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/validators/filenameValidate.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/validators/hed.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/validators/internal/emptyFile.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/validators/internal/unusedFile.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/validators/json.ts +0 -0
- {bids_validator_deno-2.1.1 → bids_validator_deno-2.2.0}/src/version.ts +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bids/validator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": "./src/bids-validator.ts",
|
|
6
6
|
"./main": "./src/main.ts",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@bids/schema": "jsr:@bids/schema@~1.1.0",
|
|
32
32
|
"@cliffy/command": "jsr:@effigies/cliffy-command@1.0.0-dev.8",
|
|
33
33
|
"@cliffy/table": "jsr:@effigies/cliffy-table@1.0.0-dev.5",
|
|
34
|
-
"@hed/validator": "npm:hed-validator@~4.
|
|
34
|
+
"@hed/validator": "npm:hed-validator@~4.1.4",
|
|
35
35
|
"@ignore": "npm:ignore@^7.0.5",
|
|
36
36
|
"@libs/xml": "jsr:@libs/xml@^6.0.8",
|
|
37
37
|
"@mango/nifti": "npm:@bids/nifti-reader-js@^0.6.9",
|
|
@@ -42,7 +42,9 @@
|
|
|
42
42
|
"@std/log": "jsr:@std/log@^0.224.14",
|
|
43
43
|
"@std/path": "jsr:@std/path@^1.1.2",
|
|
44
44
|
"@std/streams": "jsr:@std/streams@^1.0.12",
|
|
45
|
-
"@std/yaml": "jsr:@std/yaml@^1.0.9"
|
|
45
|
+
"@std/yaml": "jsr:@std/yaml@^1.0.9",
|
|
46
|
+
"isomorphic-git": "npm:isomorphic-git@^1.27.1",
|
|
47
|
+
"hash-wasm": "npm:hash-wasm@^4.12.0"
|
|
46
48
|
},
|
|
47
49
|
"tasks": {
|
|
48
50
|
"test": "deno test -A src/"
|
|
@@ -10,12 +10,12 @@ function IOErrorToIssue(err: { code: string; name: string }): Issue {
|
|
|
10
10
|
return { code: 'FILE_READ', subCode: err.name, issueMessage }
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function openStream(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
export async function openStream(
|
|
14
|
+
file: BIDSFile,
|
|
15
|
+
): Promise<ReadableStream<Uint8Array<ArrayBuffer>>> {
|
|
16
|
+
return file.stream().catch((err: any) => {
|
|
17
17
|
throw { location: file.path, ...IOErrorToIssue(err) }
|
|
18
|
-
}
|
|
18
|
+
})
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export async function readBytes(
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { BIDSFile, FileTree } from '../types/filetree.ts'
|
|
2
|
+
import { filesToTree } from './filetree.ts'
|
|
3
|
+
import { FileIgnoreRules, readBidsIgnore } from './ignore.ts'
|
|
4
|
+
import { BrowserFileOpener } from './openers.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Browser implement of BIDSFile wrapping native File/FileList types
|
|
8
|
+
*/
|
|
9
|
+
export class BIDSFileBrowser extends BIDSFile {
|
|
10
|
+
constructor(file: File, ignore: FileIgnoreRules, parent?: FileTree) {
|
|
11
|
+
const relativePath = file.webkitRelativePath
|
|
12
|
+
const prefixLength = relativePath.indexOf('/')
|
|
13
|
+
const opener = new BrowserFileOpener(file)
|
|
14
|
+
super(relativePath.substring(prefixLength), opener, ignore, parent)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Convert from FileList (created with webkitDirectory: true) to FileTree for validator use
|
|
20
|
+
*/
|
|
21
|
+
export async function fileListToTree(files: File[]): Promise<FileTree> {
|
|
22
|
+
const ignore = new FileIgnoreRules([])
|
|
23
|
+
const root = new FileTree('/', '/', undefined)
|
|
24
|
+
const tree = filesToTree(files.map((f) => new BIDSFileBrowser(f, ignore, root)), ignore)
|
|
25
|
+
const bidsignore = tree.get('.bidsignore')
|
|
26
|
+
if (bidsignore) {
|
|
27
|
+
try {
|
|
28
|
+
ignore.add(await readBidsIgnore(bidsignore as BIDSFile))
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.log(`Failed to read '.bidsignore' file with the following error:\n${err}`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return tree
|
|
34
|
+
}
|
|
@@ -31,7 +31,7 @@ Deno.test('Deno implementation of BIDSFile', async (t) => {
|
|
|
31
31
|
})
|
|
32
32
|
await t.step('can be read as ReadableStream', async () => {
|
|
33
33
|
const file = new BIDSFileDeno(testDir, testFilename, ignore)
|
|
34
|
-
const stream = file.stream
|
|
34
|
+
const stream = await file.stream()
|
|
35
35
|
const streamReader = stream.getReader()
|
|
36
36
|
const denoReader = readerFromStreamReader(streamReader)
|
|
37
37
|
const fileBuffer = await readAll(denoReader)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deno specific implementation for reading files
|
|
3
|
+
*/
|
|
4
|
+
import { basename, join } from '@std/path'
|
|
5
|
+
import * as posix from '@std/path/posix'
|
|
6
|
+
import { BIDSFile, type FileOpener, FileTree } from '../types/filetree.ts'
|
|
7
|
+
import { requestReadPermission } from '../setup/requestPermissions.ts'
|
|
8
|
+
import { FileIgnoreRules, readBidsIgnore } from './ignore.ts'
|
|
9
|
+
import { FsFileOpener, HTTPOpener } from './openers.ts'
|
|
10
|
+
import { resolveAnnexedFile } from './repo.ts'
|
|
11
|
+
import fs from 'node:fs'
|
|
12
|
+
|
|
13
|
+
export class BIDSFileDeno extends BIDSFile {
|
|
14
|
+
constructor(datasetPath: string, path: string, ignore?: FileIgnoreRules, parent?: FileTree) {
|
|
15
|
+
super(path, new FsFileOpener(datasetPath, path), ignore, parent)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ReadFileTreeOptions = {
|
|
20
|
+
rootPath: string
|
|
21
|
+
relativePath: string
|
|
22
|
+
ignore: FileIgnoreRules
|
|
23
|
+
prune: FileIgnoreRules
|
|
24
|
+
parent?: FileTree
|
|
25
|
+
preferredRemote?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function _readFileTree({
|
|
29
|
+
rootPath,
|
|
30
|
+
relativePath,
|
|
31
|
+
ignore,
|
|
32
|
+
prune,
|
|
33
|
+
parent,
|
|
34
|
+
preferredRemote,
|
|
35
|
+
}: ReadFileTreeOptions): Promise<FileTree> {
|
|
36
|
+
await requestReadPermission()
|
|
37
|
+
const name = basename(relativePath)
|
|
38
|
+
const tree = new FileTree(relativePath, name, parent, ignore)
|
|
39
|
+
|
|
40
|
+
// Opaque cache for passing to git operations
|
|
41
|
+
const cache = {}
|
|
42
|
+
|
|
43
|
+
for await (const dirEntry of Deno.readDir(join(rootPath, relativePath))) {
|
|
44
|
+
const thisPath = posix.join(relativePath, dirEntry.name)
|
|
45
|
+
if (prune.test(thisPath)) {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
if (dirEntry.isFile || dirEntry.isSymlink) {
|
|
49
|
+
let opener: FileOpener
|
|
50
|
+
const fullPath = join(rootPath, thisPath)
|
|
51
|
+
try {
|
|
52
|
+
const fileInfo = await Deno.stat(fullPath)
|
|
53
|
+
opener = new FsFileOpener(rootPath, thisPath, fileInfo)
|
|
54
|
+
} catch (error) {
|
|
55
|
+
try {
|
|
56
|
+
const { url, size } = await resolveAnnexedFile(fullPath, preferredRemote, { cache, fs })
|
|
57
|
+
opener = new HTTPOpener(url, size)
|
|
58
|
+
} catch (_) {
|
|
59
|
+
throw error
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
tree.files.push(new BIDSFile(thisPath, opener, ignore, tree))
|
|
63
|
+
}
|
|
64
|
+
if (dirEntry.isDirectory) {
|
|
65
|
+
const dirTree = await _readFileTree({
|
|
66
|
+
rootPath,
|
|
67
|
+
relativePath: thisPath,
|
|
68
|
+
ignore,
|
|
69
|
+
prune,
|
|
70
|
+
parent: tree,
|
|
71
|
+
preferredRemote,
|
|
72
|
+
})
|
|
73
|
+
tree.directories.push(dirTree)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return tree
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read in the target directory structure and return a FileTree
|
|
81
|
+
*/
|
|
82
|
+
export async function readFileTree(
|
|
83
|
+
rootPath: string,
|
|
84
|
+
prune?: FileIgnoreRules,
|
|
85
|
+
preferredRemote?: string,
|
|
86
|
+
): Promise<FileTree> {
|
|
87
|
+
prune ??= new FileIgnoreRules([], false)
|
|
88
|
+
const ignore = new FileIgnoreRules([])
|
|
89
|
+
const tree = await _readFileTree({rootPath, relativePath: '/', ignore, prune, preferredRemote})
|
|
90
|
+
const bidsignore = tree.get('.bidsignore')
|
|
91
|
+
if (bidsignore) {
|
|
92
|
+
try {
|
|
93
|
+
ignore.add(await readBidsIgnore(bidsignore as BIDSFile))
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.log(`Failed to read '.bidsignore' file with the following error:\n${err}`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return tree
|
|
99
|
+
}
|
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import { assert, assertEquals } from '@std/assert'
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import { filesToTree
|
|
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
|
+
}
|
|
5
23
|
|
|
6
24
|
Deno.test('FileTree generation', async (t) => {
|
|
7
25
|
await t.step('converts a basic list', async () => {
|
|
@@ -1,31 +1,8 @@
|
|
|
1
1
|
import { parse, SEPARATOR_PATTERN } from '@std/path'
|
|
2
2
|
import * as posix from '@std/path/posix'
|
|
3
|
-
import {
|
|
3
|
+
import { BIDSFile, FileTree } from '../types/filetree.ts'
|
|
4
4
|
import { FileIgnoreRules } from './ignore.ts'
|
|
5
5
|
|
|
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
|
-
|
|
29
6
|
export function filesToTree(fileList: BIDSFile[], ignore?: FileIgnoreRules): FileTree {
|
|
30
7
|
ignore = ignore ?? new FileIgnoreRules([])
|
|
31
8
|
const tree: FileTree = new FileTree('/', '/')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { assert, assertEquals, assertThrows } from '@std/assert'
|
|
2
2
|
import type { BIDSFile } from '../types/filetree.ts'
|
|
3
|
-
import { pathsToTree } from './filetree.ts'
|
|
3
|
+
import { pathsToTree } from './filetree.test.ts'
|
|
4
4
|
import { walkBack } from './inheritance.ts'
|
|
5
5
|
|
|
6
6
|
Deno.test('walkback inheritance tests', async (t) => {
|
|
@@ -3,7 +3,7 @@ import type { BIDSFile } from '../types/filetree.ts'
|
|
|
3
3
|
import type { FileIgnoreRules } from './ignore.ts'
|
|
4
4
|
import { testAsyncFileAccess } from './access.test.ts'
|
|
5
5
|
|
|
6
|
-
import { pathsToTree } from '../files/filetree.ts'
|
|
6
|
+
import { pathsToTree } from '../files/filetree.test.ts'
|
|
7
7
|
import { loadJSON } from './json.ts'
|
|
8
8
|
|
|
9
9
|
function encodeUTF16(text: string) {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BIDS file openers
|
|
3
|
+
*
|
|
4
|
+
* These classes implement stream, text and random bytes access to BIDS resources.
|
|
5
|
+
*/
|
|
6
|
+
import { join } from '@std/path'
|
|
7
|
+
import { type FileOpener } from '../types/filetree.ts'
|
|
8
|
+
import { createUTF8Stream } from './streams.ts'
|
|
9
|
+
|
|
10
|
+
export class FsFileOpener implements FileOpener {
|
|
11
|
+
path: string
|
|
12
|
+
fileInfo!: Deno.FileInfo
|
|
13
|
+
|
|
14
|
+
constructor(datasetPath: string, path: string, fileInfo?: Deno.FileInfo) {
|
|
15
|
+
this.path = join(datasetPath, path)
|
|
16
|
+
if (fileInfo) {
|
|
17
|
+
this.fileInfo = fileInfo
|
|
18
|
+
} else {
|
|
19
|
+
try {
|
|
20
|
+
this.fileInfo = Deno.statSync(this.path)
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
23
|
+
this.fileInfo = Deno.lstatSync(this.path)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get size(): number {
|
|
30
|
+
return this.fileInfo.size
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async stream(): Promise<ReadableStream<Uint8Array<ArrayBuffer>>> {
|
|
34
|
+
const handle = await this.open()
|
|
35
|
+
return handle.readable
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read the entire file and decode as utf-8 text
|
|
40
|
+
*/
|
|
41
|
+
async text(): Promise<string> {
|
|
42
|
+
const stream = await this.stream()
|
|
43
|
+
const reader = stream.pipeThrough(createUTF8Stream()).getReader()
|
|
44
|
+
const chunks: string[] = []
|
|
45
|
+
try {
|
|
46
|
+
while (true) {
|
|
47
|
+
const { done, value } = await reader.read()
|
|
48
|
+
if (done) break
|
|
49
|
+
chunks.push(value)
|
|
50
|
+
}
|
|
51
|
+
return chunks.join('')
|
|
52
|
+
} finally {
|
|
53
|
+
reader.releaseLock()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read bytes in a range efficiently from a given file
|
|
59
|
+
*
|
|
60
|
+
* Reads up to size bytes, starting at offset.
|
|
61
|
+
* If EOF is encountered, the resulting array may be smaller.
|
|
62
|
+
*/
|
|
63
|
+
async readBytes(size: number, offset = 0): Promise<Uint8Array<ArrayBuffer>> {
|
|
64
|
+
const handle = await this.open()
|
|
65
|
+
const buf = new Uint8Array(size)
|
|
66
|
+
await handle.seek(offset, Deno.SeekMode.Start)
|
|
67
|
+
const nbytes = await handle.read(buf) ?? 0
|
|
68
|
+
await handle.close()
|
|
69
|
+
return buf.subarray(0, nbytes)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async open(): Promise<Deno.FsFile> {
|
|
73
|
+
return Deno.open(this.path, { read: true, write: false })
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class BrowserFileOpener implements FileOpener {
|
|
78
|
+
file: File
|
|
79
|
+
constructor(file: File) {
|
|
80
|
+
this.file = file
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get size(): number {
|
|
84
|
+
return this.file.size
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async stream(): Promise<ReadableStream<Uint8Array<ArrayBuffer>>> {
|
|
88
|
+
return Promise.resolve(this.file.stream() as ReadableStream<Uint8Array<ArrayBuffer>>)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async text(): Promise<string> {
|
|
92
|
+
return this.file.text()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async readBytes(size: number, offset = 0): Promise<Uint8Array<ArrayBuffer>> {
|
|
96
|
+
return new Uint8Array(await this.file.slice(offset, size).arrayBuffer())
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class HTTPOpener implements FileOpener {
|
|
101
|
+
url: string
|
|
102
|
+
size: number
|
|
103
|
+
|
|
104
|
+
constructor(url: string, size: number = -1) {
|
|
105
|
+
this.url = url
|
|
106
|
+
this.size = size
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async stream(): Promise<ReadableStream<Uint8Array<ArrayBuffer>>> {
|
|
110
|
+
const response = await fetch(this.url)
|
|
111
|
+
if (!response.ok || !response.body) {
|
|
112
|
+
throw new Error(`Failed to fetch ${this.url}: ${response.status} ${response.statusText}`)
|
|
113
|
+
}
|
|
114
|
+
return response.body
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async text(): Promise<string> {
|
|
118
|
+
const response = await fetch(this.url)
|
|
119
|
+
return response.text()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async readBytes(size: number, offset = 0): Promise<Uint8Array<ArrayBuffer>> {
|
|
123
|
+
const headers = new Headers()
|
|
124
|
+
headers.append('Range', `bytes=${offset}-${offset + size - 1}`)
|
|
125
|
+
const response = await fetch(this.url, { headers })
|
|
126
|
+
return new Uint8Array(await response.arrayBuffer())
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for reading git-annex metadata
|
|
3
|
+
*/
|
|
4
|
+
import { dirname, join, parse, SEPARATOR_PATTERN } from '@std/path'
|
|
5
|
+
import { default as git } from 'isomorphic-git'
|
|
6
|
+
import { createMD5 } from 'hash-wasm'
|
|
7
|
+
|
|
8
|
+
const textDecoder = new TextDecoder('utf-8')
|
|
9
|
+
export const annexKeyRegex =
|
|
10
|
+
/^(?<hashname>[A-Z0-9]+)-s(?<size>\d+)--(?<digest>[0-9a-fA-F]+)(?<ext>\.[\w\-. ]*)?/
|
|
11
|
+
export const rmetLineRegex =
|
|
12
|
+
/^(?<timestamp>\d+(\.\d+)?)s (?<uuid>[0-9a-fA-F-]+):V \+(?<version>[^#]+)#(?<path>.+)/
|
|
13
|
+
|
|
14
|
+
type Rmet = {
|
|
15
|
+
timestamp: number
|
|
16
|
+
uuid: string
|
|
17
|
+
version: string
|
|
18
|
+
path: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function readAnnexPath(
|
|
22
|
+
filepath: string,
|
|
23
|
+
options: any,
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
const oid = await git.resolveRef({ ref: 'git-annex', ...options })
|
|
26
|
+
const { blob } = await git.readBlob({ oid, filepath, ...options })
|
|
27
|
+
return textDecoder.decode(blob)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* git-annex hashDirLower implementation based on https://git-annex.branchable.com/internals/hashing/
|
|
32
|
+
* Compute the directory path from a git-annex filename
|
|
33
|
+
*/
|
|
34
|
+
export async function hashDirLower(annexKey: string): Promise<[string, string]> {
|
|
35
|
+
const computeMD5 = await createMD5()
|
|
36
|
+
computeMD5.init()
|
|
37
|
+
computeMD5.update(annexKey)
|
|
38
|
+
const digest = computeMD5.digest('hex')
|
|
39
|
+
return [digest.slice(0, 3), digest.slice(3, 6)]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read remote metadata entries for a given annex key
|
|
44
|
+
*
|
|
45
|
+
* *.log.rmet
|
|
46
|
+
* Path: {md5(key)[0:3]}/{md5(key)[3:6]}/{key}.log.rmet
|
|
47
|
+
* Contents:
|
|
48
|
+
* <timestamp> <uuid>:V +<version>#<path>
|
|
49
|
+
*
|
|
50
|
+
* The general form is <uuid>:<key> [+-]<value> and is an append-only log
|
|
51
|
+
* We may at some point care about doing this correctly.
|
|
52
|
+
*/
|
|
53
|
+
async function readRmet(key: string, options: any): Promise<Record<string, Rmet>> {
|
|
54
|
+
const hashDirs = await hashDirLower(key)
|
|
55
|
+
const rmet = await readAnnexPath(join(...hashDirs, `${key}.log.rmet`), options)
|
|
56
|
+
const ret: Record<string, Rmet> = {}
|
|
57
|
+
for (const line of rmet.split('\n')) {
|
|
58
|
+
const match = line.match(rmetLineRegex)
|
|
59
|
+
if (match) {
|
|
60
|
+
ret[match!.groups!.uuid] = match!.groups as unknown as Rmet
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return ret
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read special remote configuration from remote.log
|
|
68
|
+
*
|
|
69
|
+
* remote.log
|
|
70
|
+
* <uuid> [<key>=<value>]...
|
|
71
|
+
* keys of interest:
|
|
72
|
+
* name
|
|
73
|
+
* type (S3)
|
|
74
|
+
* publicurl
|
|
75
|
+
* timestamp
|
|
76
|
+
*/
|
|
77
|
+
export async function readRemotes(options: any): Promise<Record<string, Record<string, string>>> {
|
|
78
|
+
const remotesText = await readAnnexPath('remote.log', options)
|
|
79
|
+
const byUUID: Record<string, Record<string, string>> = {}
|
|
80
|
+
for (const line of remotesText.split('\n')) {
|
|
81
|
+
const [uuid, ...keyvals] = line.split(' ')
|
|
82
|
+
byUUID[uuid] = Object.fromEntries(keyvals.map((kv) => kv.split('=')))
|
|
83
|
+
}
|
|
84
|
+
return byUUID
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve an annexed file location to an HTTP URL, if a public S3 remote is available
|
|
89
|
+
*/
|
|
90
|
+
export async function resolveAnnexedFile(
|
|
91
|
+
path: string,
|
|
92
|
+
remote?: string,
|
|
93
|
+
options?: any,
|
|
94
|
+
): Promise<{ url: string; size: number }> {
|
|
95
|
+
// path is known to be a symlink
|
|
96
|
+
const target = await Deno.readLink(path)
|
|
97
|
+
const { dir, base } = parse(target)
|
|
98
|
+
|
|
99
|
+
if (!options?.gitdir) {
|
|
100
|
+
const dirs = dir.split(SEPARATOR_PATTERN)
|
|
101
|
+
const gitdir = join(dirname(path), ...dirs.slice(0, dirs.indexOf('.git') + 1))
|
|
102
|
+
options = { ...options, gitdir }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const size = +base.match(annexKeyRegex)?.groups?.size!
|
|
106
|
+
|
|
107
|
+
const rmet = await readRmet(base, options)
|
|
108
|
+
const remotes = await readRemotes(options)
|
|
109
|
+
let uuid: string
|
|
110
|
+
if (remote) {
|
|
111
|
+
let matching: string | undefined
|
|
112
|
+
for (const [u, r] of Object.entries(remotes)) {
|
|
113
|
+
// Only consider public S3 remotes.
|
|
114
|
+
// This will need to be expanded for other types of remotes in future
|
|
115
|
+
if (!r?.publicurl) {
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
if (r.name === remote) {
|
|
119
|
+
matching = u
|
|
120
|
+
break
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!matching) {
|
|
124
|
+
throw new Error(`No remote named ${remote}`)
|
|
125
|
+
}
|
|
126
|
+
uuid = matching
|
|
127
|
+
} else {
|
|
128
|
+
// Take the newest remote (reverse sort timestamps, take first)
|
|
129
|
+
uuid = Object.entries(rmet).toSorted((a, b) => +b[1].timestamp - +a[1].timestamp)[0][0]
|
|
130
|
+
}
|
|
131
|
+
const { publicurl } = remotes[uuid]
|
|
132
|
+
|
|
133
|
+
if (!publicurl) {
|
|
134
|
+
throw new Error(`No publicurl found for remote ${uuid}`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const metadata = rmet[uuid]
|
|
138
|
+
const url = `${publicurl}/${metadata.path}?versionId=${metadata.version}`
|
|
139
|
+
|
|
140
|
+
return { url, size }
|
|
141
|
+
}
|